xref: /freebsd/contrib/libarchive/libarchive/archive_util.c (revision 185becb1e1bd2657c156f78aeb52edac05ba5fb5)
1 /*-
2  * Copyright (c) 2009-2012,2014 Michihiro NAKAJIMA
3  * Copyright (c) 2003-2007 Tim Kientzle
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
16  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18  * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
19  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  */
26 
27 #include "archive_platform.h"
28 
29 #ifdef HAVE_SYS_TYPES_H
30 #include <sys/types.h>
31 #endif
32 #ifdef HAVE_ERRNO_H
33 #include <errno.h>
34 #endif
35 #ifdef HAVE_FCNTL_H
36 #include <fcntl.h>
37 #endif
38 #ifdef HAVE_STDLIB_H
39 #include <stdlib.h>
40 #endif
41 #ifdef HAVE_STRING_H
42 #include <string.h>
43 #endif
44 #if defined(_WIN32) && !defined(__CYGWIN__)
45 #include <bcrypt.h>
46 
47 /* Common in other bcrypt implementations, but missing from VS2008. */
48 #ifndef BCRYPT_SUCCESS
49 #define BCRYPT_SUCCESS(r) ((NTSTATUS)(r) == STATUS_SUCCESS)
50 #endif
51 #endif
52 #ifdef HAVE_ZLIB_H
53 #include <zlib.h>
54 #endif
55 #ifdef HAVE_LZMA_H
56 #include <lzma.h>
57 #endif
58 #ifdef HAVE_BZLIB_H
59 #include <bzlib.h>
60 #endif
61 #ifdef HAVE_LZ4_H
62 #include <lz4.h>
63 #endif
64 
65 #include "archive.h"
66 #include "archive_private.h"
67 #include "archive_random_private.h"
68 #include "archive_string.h"
69 
70 #ifndef O_CLOEXEC
71 #define O_CLOEXEC	0
72 #endif
73 
74 #if ARCHIVE_VERSION_NUMBER < 4000000
75 static int __LA_LIBC_CC archive_utility_string_sort_helper(const void *, const void *);
76 #endif
77 
78 /* Generic initialization of 'struct archive' objects. */
79 int
__archive_clean(struct archive * a)80 __archive_clean(struct archive *a)
81 {
82 	archive_string_conversion_free(a);
83 	return (ARCHIVE_OK);
84 }
85 
86 int
archive_version_number(void)87 archive_version_number(void)
88 {
89 	return (ARCHIVE_VERSION_NUMBER);
90 }
91 
92 const char *
archive_version_string(void)93 archive_version_string(void)
94 {
95 	return (ARCHIVE_VERSION_STRING);
96 }
97 
98 int
archive_errno(struct archive * a)99 archive_errno(struct archive *a)
100 {
101 	return (a->archive_error_number);
102 }
103 
104 const char *
archive_error_string(struct archive * a)105 archive_error_string(struct archive *a)
106 {
107 	if (a->error != NULL && *a->error != '\0')
108 		return (a->error);
109 	else
110 		return (NULL);
111 }
112 
113 int
archive_file_count(struct archive * a)114 archive_file_count(struct archive *a)
115 {
116 	return (a->file_count);
117 }
118 
119 int
archive_format(struct archive * a)120 archive_format(struct archive *a)
121 {
122 	return (a->archive_format);
123 }
124 
125 const char *
archive_format_name(struct archive * a)126 archive_format_name(struct archive *a)
127 {
128 	return (a->archive_format_name);
129 }
130 
131 
132 int
archive_compression(struct archive * a)133 archive_compression(struct archive *a)
134 {
135 	return archive_filter_code(a, 0);
136 }
137 
138 const char *
archive_compression_name(struct archive * a)139 archive_compression_name(struct archive *a)
140 {
141 	return archive_filter_name(a, 0);
142 }
143 
144 
145 /*
146  * Return a count of the number of compressed bytes processed.
147  */
148 la_int64_t
archive_position_compressed(struct archive * a)149 archive_position_compressed(struct archive *a)
150 {
151 	return archive_filter_bytes(a, -1);
152 }
153 
154 /*
155  * Return a count of the number of uncompressed bytes processed.
156  */
157 la_int64_t
archive_position_uncompressed(struct archive * a)158 archive_position_uncompressed(struct archive *a)
159 {
160 	return archive_filter_bytes(a, 0);
161 }
162 
163 void
archive_clear_error(struct archive * a)164 archive_clear_error(struct archive *a)
165 {
166 	archive_string_empty(&a->error_string);
167 	a->error = NULL;
168 	a->archive_error_number = 0;
169 }
170 
171 void
archive_set_error(struct archive * a,int error_number,const char * fmt,...)172 archive_set_error(struct archive *a, int error_number, const char *fmt, ...)
173 {
174 	va_list ap;
175 
176 	a->archive_error_number = error_number;
177 	if (fmt == NULL) {
178 		a->error = NULL;
179 		return;
180 	}
181 
182 	archive_string_empty(&(a->error_string));
183 	va_start(ap, fmt);
184 	archive_string_vsprintf(&(a->error_string), fmt, ap);
185 	va_end(ap);
186 	a->error = a->error_string.s;
187 }
188 
189 void
archive_copy_error(struct archive * dest,struct archive * src)190 archive_copy_error(struct archive *dest, struct archive *src)
191 {
192 	dest->archive_error_number = src->archive_error_number;
193 
194 	archive_string_copy(&dest->error_string, &src->error_string);
195 	dest->error = dest->error_string.s;
196 }
197 
198 void
__archive_errx(int retvalue,const char * msg)199 __archive_errx(int retvalue, const char *msg)
200 {
201 	static const char msg1[] = "Fatal Internal Error in libarchive: ";
202 	size_t s;
203 
204 	s = write(2, msg1, strlen(msg1));
205 	(void)s; /* UNUSED */
206 	s = write(2, msg, strlen(msg));
207 	(void)s; /* UNUSED */
208 	s = write(2, "\n", 1);
209 	(void)s; /* UNUSED */
210 	exit(retvalue);
211 }
212 
213 /*
214  * Create a temporary file
215  */
216 #if defined(_WIN32) && !defined(__CYGWIN__)
217 
218 /*
219  * Do not use Windows tmpfile() function.
220  * It will make a temporary file under the root directory
221  * and it'll cause permission error if a user who is
222  * non-Administrator creates temporary files.
223  * Also Windows version of mktemp family including _mktemp_s
224  * are not secure.
225  */
226 static int
__archive_mktempx(const char * tmpdir,wchar_t * template)227 __archive_mktempx(const char *tmpdir, wchar_t *template)
228 {
229 	static const wchar_t prefix[] = L"libarchive_";
230 	static const wchar_t suffix[] = L"XXXXXXXXXX";
231 	static const wchar_t num[] = {
232 		L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
233 		L'8', L'9', L'A', L'B', L'C', L'D', L'E', L'F',
234 		L'G', L'H', L'I', L'J', L'K', L'L', L'M', L'N',
235 		L'O', L'P', L'Q', L'R', L'S', L'T', L'U', L'V',
236 		L'W', L'X', L'Y', L'Z', L'a', L'b', L'c', L'd',
237 		L'e', L'f', L'g', L'h', L'i', L'j', L'k', L'l',
238 		L'm', L'n', L'o', L'p', L'q', L'r', L's', L't',
239 		L'u', L'v', L'w', L'x', L'y', L'z'
240 	};
241 	struct archive_wstring temp_name;
242 	wchar_t *ws;
243 	DWORD attr;
244 	wchar_t *xp, *ep;
245 	int fd;
246 	BCRYPT_ALG_HANDLE hAlg = NULL;
247 	fd = -1;
248 	ws = NULL;
249 	archive_string_init(&temp_name);
250 
251 	if (template == NULL) {
252 		/* Get a temporary directory. */
253 		if (tmpdir == NULL) {
254 			wchar_t buf[MAX_PATH + 1];
255 			wchar_t *p = buf, *buf2 = NULL;
256 			size_t l, s;
257 
258 			s = MAX_PATH + 1;
259 			l = GetTempPathW((DWORD)s, buf);
260 			if (l == 0) {
261 				la_dosmaperr(GetLastError());
262 				goto exit_tmpfile;
263 			}
264 			while (l > s) {
265 				wchar_t *tmp;
266 
267 				s = l;
268 				tmp = realloc(buf2, s * sizeof(wchar_t));
269 				if (tmp == NULL) {
270 					free(buf2);
271 					errno = ENOMEM;
272 					goto exit_tmpfile;
273 				}
274 				p = buf2 = tmp;
275 				l = GetTempPathW((DWORD)s, buf2);
276 				if (l == 0) {
277 					free(buf2);
278 					la_dosmaperr(GetLastError());
279 					goto exit_tmpfile;
280 				}
281 			}
282 			archive_wstrcpy(&temp_name, p);
283 			free(buf2);
284 		} else {
285 			if (archive_wstring_append_from_mbs(&temp_name, tmpdir,
286 			    strlen(tmpdir)) < 0)
287 				goto exit_tmpfile;
288 			if (temp_name.length == 0 ||
289 			    temp_name.s[temp_name.length-1] != L'/')
290 				archive_wstrappend_wchar(&temp_name, L'/');
291 		}
292 
293 		/* Check if temp_name is a directory. */
294 		attr = GetFileAttributesW(temp_name.s);
295 		if (attr == (DWORD)-1) {
296 			if (GetLastError() != ERROR_FILE_NOT_FOUND) {
297 				la_dosmaperr(GetLastError());
298 				goto exit_tmpfile;
299 			}
300 			ws = __la_win_permissive_name_w(temp_name.s);
301 			if (ws == NULL) {
302 				errno = EINVAL;
303 				goto exit_tmpfile;
304 			}
305 			attr = GetFileAttributesW(ws);
306 			if (attr == (DWORD)-1) {
307 				la_dosmaperr(GetLastError());
308 				goto exit_tmpfile;
309 			}
310 		}
311 		if (!(attr & FILE_ATTRIBUTE_DIRECTORY)) {
312 			errno = ENOTDIR;
313 			goto exit_tmpfile;
314 		}
315 
316 		/*
317 		 * Create a temporary file.
318 		 */
319 		archive_wstrcat(&temp_name, prefix);
320 		archive_wstrcat(&temp_name, suffix);
321 		ep = temp_name.s + archive_strlen(&temp_name);
322 		xp = ep - wcslen(suffix);
323 		template = temp_name.s;
324 	} else {
325 		xp = wcschr(template, L'X');
326 		if (xp == NULL)	/* No X, programming error */
327 			abort();
328 		for (ep = xp; *ep == L'X'; ep++)
329 			continue;
330 		if (*ep)	/* X followed by non X, programming error */
331 			abort();
332 	}
333 
334 	if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_RNG_ALGORITHM,
335 		NULL, 0))) {
336 		la_dosmaperr(GetLastError());
337 		goto exit_tmpfile;
338 	}
339 
340 	for (;;) {
341 		wchar_t *p;
342 		HANDLE h;
343 # if _WIN32_WINNT >= 0x0602 /* _WIN32_WINNT_WIN8 */
344 		CREATEFILE2_EXTENDED_PARAMETERS createExParams;
345 #endif
346 
347 		/* Generate a random file name through CryptGenRandom(). */
348 		p = xp;
349 		if (!BCRYPT_SUCCESS(BCryptGenRandom(hAlg, (PUCHAR)p,
350 		    (DWORD)(ep - p)*sizeof(wchar_t), 0))) {
351 			la_dosmaperr(GetLastError());
352 			goto exit_tmpfile;
353 		}
354 		for (; p < ep; p++)
355 			*p = num[((DWORD)*p) % (sizeof(num)/sizeof(num[0]))];
356 
357 		free(ws);
358 		ws = __la_win_permissive_name_w(template);
359 		if (ws == NULL) {
360 			errno = EINVAL;
361 			goto exit_tmpfile;
362 		}
363 		if (template == temp_name.s) {
364 			attr = FILE_ATTRIBUTE_TEMPORARY |
365 			       FILE_FLAG_DELETE_ON_CLOSE;
366 		} else {
367 			/* mkstemp */
368 			attr = FILE_ATTRIBUTE_NORMAL;
369 		}
370 # if _WIN32_WINNT >= 0x0602 /* _WIN32_WINNT_WIN8 */
371 		ZeroMemory(&createExParams, sizeof(createExParams));
372 		createExParams.dwSize = sizeof(createExParams);
373 		createExParams.dwFileAttributes = attr & 0xFFFF;
374 		createExParams.dwFileFlags = attr & 0xFFF00000;
375 		h = CreateFile2(ws,
376 		    GENERIC_READ | GENERIC_WRITE | DELETE,
377 		    0,/* Not share */
378 			CREATE_NEW,
379 			&createExParams);
380 #else
381 		h = CreateFileW(ws,
382 		    GENERIC_READ | GENERIC_WRITE | DELETE,
383 		    0,/* Not share */
384 		    NULL,
385 		    CREATE_NEW,/* Create a new file only */
386 		    attr,
387 		    NULL);
388 #endif
389 		if (h == INVALID_HANDLE_VALUE) {
390 			/* The same file already exists. retry with
391 			 * a new filename. */
392 			if (GetLastError() == ERROR_FILE_EXISTS)
393 				continue;
394 			/* Otherwise, fail creation temporary file. */
395 			la_dosmaperr(GetLastError());
396 			goto exit_tmpfile;
397 		}
398 		fd = _open_osfhandle((intptr_t)h, _O_BINARY | _O_RDWR);
399 		if (fd == -1) {
400 			la_dosmaperr(GetLastError());
401 			CloseHandle(h);
402 			goto exit_tmpfile;
403 		} else
404 			break;/* success! */
405 	}
406 exit_tmpfile:
407 	if (hAlg != NULL)
408 		BCryptCloseAlgorithmProvider(hAlg, 0);
409 	free(ws);
410 	if (template == temp_name.s)
411 		archive_wstring_free(&temp_name);
412 	return (fd);
413 }
414 
415 int
__archive_mktemp(const char * tmpdir)416 __archive_mktemp(const char *tmpdir)
417 {
418 	return __archive_mktempx(tmpdir, NULL);
419 }
420 
421 int
__archive_mkstemp(wchar_t * template)422 __archive_mkstemp(wchar_t *template)
423 {
424 	return __archive_mktempx(NULL, template);
425 }
426 
427 #else
428 
429 static int
__archive_issetugid(void)430 __archive_issetugid(void)
431 {
432 #ifdef HAVE_ISSETUGID
433 	return (issetugid());
434 #elif HAVE_GETRESUID
435 	uid_t ruid, euid, suid;
436 	gid_t rgid, egid, sgid;
437 	if (getresuid(&ruid, &euid, &suid) != 0)
438 		return (-1);
439 	if (ruid != euid || ruid != suid)
440 		return (1);
441 	if (getresgid(&rgid, &egid, &sgid) != 0)
442 		return (-1);
443 	if (rgid != egid || rgid != sgid)
444 		return (1);
445 #elif HAVE_GETEUID
446 	if (geteuid() != getuid())
447 		return (1);
448 #if HAVE_GETEGID
449 	if (getegid() != getgid())
450 		return (1);
451 #endif
452 #endif
453 	return (0);
454 }
455 
456 int
__archive_get_tempdir(struct archive_string * temppath)457 __archive_get_tempdir(struct archive_string *temppath)
458 {
459 	const char *tmp = NULL;
460 
461 	if (__archive_issetugid() == 0)
462 		tmp = getenv("TMPDIR");
463 	if (tmp == NULL)
464 #ifdef _PATH_TMP
465 		tmp = _PATH_TMP;
466 #else
467                 tmp = "/tmp";
468 #endif
469 	archive_strcpy(temppath, tmp);
470 	if (temppath->length == 0 || temppath->s[temppath->length-1] != '/')
471 		archive_strappend_char(temppath, '/');
472 	return (ARCHIVE_OK);
473 }
474 
475 #if defined(HAVE_MKSTEMP)
476 
477 /*
478  * We can use mkstemp().
479  */
480 
481 int
__archive_mktemp(const char * tmpdir)482 __archive_mktemp(const char *tmpdir)
483 {
484 	struct archive_string temp_name;
485 	int fd = -1;
486 
487 	archive_string_init(&temp_name);
488 	if (tmpdir == NULL) {
489 		if (__archive_get_tempdir(&temp_name) != ARCHIVE_OK)
490 			goto exit_tmpfile;
491 	} else {
492 		archive_strcpy(&temp_name, tmpdir);
493 		if (temp_name.length == 0 ||
494 		    temp_name.s[temp_name.length-1] != '/')
495 			archive_strappend_char(&temp_name, '/');
496 	}
497 #ifdef O_TMPFILE
498 	fd = open(temp_name.s, O_RDWR|O_CLOEXEC|O_TMPFILE|O_EXCL, 0600);
499 	if(fd >= 0)
500 		goto exit_tmpfile;
501 #endif
502 	archive_strcat(&temp_name, "libarchive_XXXXXX");
503 	fd = mkstemp(temp_name.s);
504 	if (fd < 0)
505 		goto exit_tmpfile;
506 	__archive_ensure_cloexec_flag(fd);
507 	unlink(temp_name.s);
508 exit_tmpfile:
509 	archive_string_free(&temp_name);
510 	return (fd);
511 }
512 
513 int
__archive_mkstemp(char * template)514 __archive_mkstemp(char *template)
515 {
516 	int fd = -1;
517 	fd = mkstemp(template);
518 	if (fd >= 0)
519 		__archive_ensure_cloexec_flag(fd);
520 	return (fd);
521 }
522 
523 #else /* !HAVE_MKSTEMP */
524 
525 /*
526  * We use a private routine.
527  */
528 
529 static int
__archive_mktempx(const char * tmpdir,char * template)530 __archive_mktempx(const char *tmpdir, char *template)
531 {
532         static const char num[] = {
533 		'0', '1', '2', '3', '4', '5', '6', '7',
534 		'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
535 		'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
536 		'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
537 		'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
538 		'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
539 		'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
540 		'u', 'v', 'w', 'x', 'y', 'z'
541         };
542 	struct archive_string temp_name;
543 	struct stat st;
544 	int fd;
545 	char *tp, *ep;
546 
547 	fd = -1;
548 	if (template == NULL) {
549 		archive_string_init(&temp_name);
550 		if (tmpdir == NULL) {
551 			if (__archive_get_tempdir(&temp_name) != ARCHIVE_OK)
552 				goto exit_tmpfile;
553 		} else
554 			archive_strcpy(&temp_name, tmpdir);
555 		if (temp_name.length > 0 && temp_name.s[temp_name.length-1] == '/') {
556 			temp_name.s[temp_name.length-1] = '\0';
557 			temp_name.length --;
558 		}
559 		if (la_stat(temp_name.s, &st) < 0)
560 			goto exit_tmpfile;
561 		if (!S_ISDIR(st.st_mode)) {
562 			errno = ENOTDIR;
563 			goto exit_tmpfile;
564 		}
565 		archive_strcat(&temp_name, "/libarchive_");
566 		tp = temp_name.s + archive_strlen(&temp_name);
567 		archive_strcat(&temp_name, "XXXXXXXXXX");
568 		ep = temp_name.s + archive_strlen(&temp_name);
569 		template = temp_name.s;
570 	} else {
571 		tp = strchr(template, 'X');
572 		if (tp == NULL)	/* No X, programming error */
573 			abort();
574 		for (ep = tp; *ep == 'X'; ep++)
575 			continue;
576 		if (*ep)	/* X followed by non X, programming error */
577 			abort();
578 	}
579 
580 	do {
581 		char *p;
582 
583 		p = tp;
584 		archive_random(p, ep - p);
585 		while (p < ep) {
586 			int d = *((unsigned char *)p) % sizeof(num);
587 			*p++ = num[d];
588 		}
589 		fd = open(template, O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC,
590 			  0600);
591 	} while (fd < 0 && errno == EEXIST);
592 	if (fd < 0)
593 		goto exit_tmpfile;
594 	__archive_ensure_cloexec_flag(fd);
595 	if (template == temp_name.s)
596 		unlink(temp_name.s);
597 exit_tmpfile:
598 	if (template == temp_name.s)
599 		archive_string_free(&temp_name);
600 	return (fd);
601 }
602 
603 int
__archive_mktemp(const char * tmpdir)604 __archive_mktemp(const char *tmpdir)
605 {
606 	return __archive_mktempx(tmpdir, NULL);
607 }
608 
609 int
__archive_mkstemp(char * template)610 __archive_mkstemp(char *template)
611 {
612 	return __archive_mktempx(NULL, template);
613 }
614 
615 #endif /* !HAVE_MKSTEMP */
616 #endif /* !_WIN32 || __CYGWIN__ */
617 
618 /*
619  * Set FD_CLOEXEC flag to a file descriptor if it is not set.
620  * We have to set the flag if the platform does not provide O_CLOEXEC
621  * or F_DUPFD_CLOEXEC flags.
622  *
623  * Note: This function is absolutely called after creating a new file
624  * descriptor even if the platform seemingly provides O_CLOEXEC or
625  * F_DUPFD_CLOEXEC macros because it is possible that the platform
626  * merely declares those macros, especially Linux 2.6.18 - 2.6.24 do it.
627  */
628 void
__archive_ensure_cloexec_flag(int fd)629 __archive_ensure_cloexec_flag(int fd)
630 {
631 #if defined(_WIN32) && !defined(__CYGWIN__)
632 	(void)fd; /* UNUSED */
633 #else
634 	int flags;
635 
636 	if (fd >= 0) {
637 		flags = fcntl(fd, F_GETFD);
638 		if (flags != -1 && (flags & FD_CLOEXEC) == 0)
639 			fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
640 	}
641 #endif
642 }
643 
644 #if ARCHIVE_VERSION_NUMBER < 4000000
645 /*
646  * Utility functions to sort a group of strings using quicksort.
647  */
648 static int
649 __LA_LIBC_CC
archive_utility_string_sort_helper(const void * p1,const void * p2)650 archive_utility_string_sort_helper(const void *p1, const void *p2)
651 {
652 	const char * const * const s1 = p1;
653 	const char * const * const s2 = p2;
654 
655 	return strcmp(*s1, *s2);
656 }
657 
658 int
archive_utility_string_sort(char ** strings)659 archive_utility_string_sort(char **strings)
660 {
661 	size_t size = 0;
662 	while (strings[size] != NULL)
663 		size++;
664 	qsort(strings, size, sizeof(char *),
665 	      archive_utility_string_sort_helper);
666 	return (ARCHIVE_OK);
667 }
668 #endif
669