xref: /freebsd/bin/cp/cp.c (revision eb439266b433241cec9cbef44328b16056bd6ff7)
1 /*-
2  * SPDX-License-Identifier: BSD-3-Clause
3  *
4  * Copyright (c) 1988, 1993, 1994
5  *	The Regents of the University of California.  All rights reserved.
6  *
7  * This code is derived from software contributed to Berkeley by
8  * David Hitz of Auspex Systems Inc.
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions
12  * are met:
13  * 1. Redistributions of source code must retain the above copyright
14  *    notice, this list of conditions and the following disclaimer.
15  * 2. Redistributions in binary form must reproduce the above copyright
16  *    notice, this list of conditions and the following disclaimer in the
17  *    documentation and/or other materials provided with the distribution.
18  * 3. Neither the name of the University nor the names of its contributors
19  *    may be used to endorse or promote products derived from this software
20  *    without specific prior written permission.
21  *
22  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
23  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
26  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32  * SUCH DAMAGE.
33  */
34 
35 /*
36  * Cp copies source files to target files.
37  *
38  * The global PATH_T structure "to" always contains the path to the
39  * current target file.  Since fts(3) does not change directories,
40  * this path can be either absolute or dot-relative.
41  *
42  * The basic algorithm is to initialize "to" and use fts(3) to traverse
43  * the file hierarchy rooted in the argument list.  A trivial case is the
44  * case of 'cp file1 file2'.  The more interesting case is the case of
45  * 'cp file1 file2 ... fileN dir' where the hierarchy is traversed and the
46  * path (relative to the root of the traversal) is appended to dir (stored
47  * in "to") to form the final target path.
48  */
49 
50 #include <sys/types.h>
51 #include <sys/stat.h>
52 
53 #include <assert.h>
54 #include <err.h>
55 #include <errno.h>
56 #include <fcntl.h>
57 #include <fts.h>
58 #include <limits.h>
59 #include <signal.h>
60 #include <stdbool.h>
61 #include <stdio.h>
62 #include <stdlib.h>
63 #include <string.h>
64 #include <unistd.h>
65 
66 #include "extern.h"
67 
68 static char dot[] = ".";
69 
70 #define END(buf) (buf + sizeof(buf))
71 PATH_T to = { .dir = -1, .end = to.path };
72 int Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
73 static int Hflag, Lflag, Pflag, Rflag, rflag;
74 volatile sig_atomic_t info;
75 
76 enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
77 
78 static int copy(char *[], enum op, int, struct stat *);
79 static void siginfo(int __unused);
80 
81 int
main(int argc,char * argv[])82 main(int argc, char *argv[])
83 {
84 	struct stat to_stat, tmp_stat;
85 	enum op type;
86 	int ch, fts_options, r;
87 	char *sep, *target;
88 	bool have_trailing_slash = false;
89 
90 	fts_options = FTS_NOCHDIR | FTS_PHYSICAL;
91 	while ((ch = getopt(argc, argv, "HLPRafilNnprsvx")) != -1)
92 		switch (ch) {
93 		case 'H':
94 			Hflag = 1;
95 			Lflag = Pflag = 0;
96 			break;
97 		case 'L':
98 			Lflag = 1;
99 			Hflag = Pflag = 0;
100 			break;
101 		case 'P':
102 			Pflag = 1;
103 			Hflag = Lflag = 0;
104 			break;
105 		case 'R':
106 			Rflag = 1;
107 			break;
108 		case 'a':
109 			pflag = 1;
110 			Rflag = 1;
111 			Pflag = 1;
112 			Hflag = Lflag = 0;
113 			break;
114 		case 'f':
115 			fflag = 1;
116 			iflag = nflag = 0;
117 			break;
118 		case 'i':
119 			iflag = 1;
120 			fflag = nflag = 0;
121 			break;
122 		case 'l':
123 			lflag = 1;
124 			break;
125 		case 'N':
126 			Nflag = 1;
127 			break;
128 		case 'n':
129 			nflag = 1;
130 			fflag = iflag = 0;
131 			break;
132 		case 'p':
133 			pflag = 1;
134 			break;
135 		case 'r':
136 			rflag = Lflag = 1;
137 			Hflag = Pflag = 0;
138 			break;
139 		case 's':
140 			sflag = 1;
141 			break;
142 		case 'v':
143 			vflag = 1;
144 			break;
145 		case 'x':
146 			fts_options |= FTS_XDEV;
147 			break;
148 		default:
149 			usage();
150 		}
151 	argc -= optind;
152 	argv += optind;
153 
154 	if (argc < 2)
155 		usage();
156 
157 	if (Rflag && rflag)
158 		errx(1, "the -R and -r options may not be specified together");
159 	if (lflag && sflag)
160 		errx(1, "the -l and -s options may not be specified together");
161 	if (rflag)
162 		Rflag = 1;
163 	if (Rflag) {
164 		if (Hflag)
165 			fts_options |= FTS_COMFOLLOW;
166 		if (Lflag) {
167 			fts_options &= ~FTS_PHYSICAL;
168 			fts_options |= FTS_LOGICAL;
169 		}
170 	} else if (!Pflag) {
171 		fts_options &= ~FTS_PHYSICAL;
172 		fts_options |= FTS_LOGICAL | FTS_COMFOLLOW;
173 	}
174 	(void)signal(SIGINFO, siginfo);
175 
176 	/* Save the target base in "to". */
177 	target = argv[--argc];
178 	if (*target == '\0') {
179 		target = dot;
180 	} else if ((sep = strrchr(target, '/')) != NULL && sep[1] == '\0') {
181 		have_trailing_slash = true;
182 		while (sep > target && *sep == '/')
183 			sep--;
184 		sep[1] = '\0';
185 	}
186 	/*
187 	 * Copy target into to.base, leaving room for a possible separator
188 	 * which will be appended later in the non-FILE_TO_FILE cases.
189 	 */
190 	if (strlcpy(to.base, target, sizeof(to.base) - 1) >=
191 	    sizeof(to.base) - 1)
192 		errc(1, ENAMETOOLONG, "%s", target);
193 
194 	/* Set end of argument list for fts(3). */
195 	argv[argc] = NULL;
196 
197 	/*
198 	 * Cp has two distinct cases:
199 	 *
200 	 * cp [-R] source target
201 	 * cp [-R] source1 ... sourceN directory
202 	 *
203 	 * In both cases, source can be either a file or a directory.
204 	 *
205 	 * In (1), the target becomes a copy of the source. That is, if the
206 	 * source is a file, the target will be a file, and likewise for
207 	 * directories.
208 	 *
209 	 * In (2), the real target is not directory, but "directory/source".
210 	 */
211 	r = stat(to.base, &to_stat);
212 	if (r == -1 && errno != ENOENT)
213 		err(1, "%s", target);
214 	if (r == -1 || !S_ISDIR(to_stat.st_mode)) {
215 		/*
216 		 * Case (1).  Target is not a directory.
217 		 */
218 		if (argc > 1)
219 			errc(1, ENOTDIR, "%s", target);
220 
221 		/*
222 		 * Need to detect the case:
223 		 *	cp -R dir foo
224 		 * Where dir is a directory and foo does not exist, where
225 		 * we want pathname concatenations turned on but not for
226 		 * the initial mkdir().
227 		 */
228 		if (r == -1) {
229 			if (Rflag && (Lflag || Hflag))
230 				stat(*argv, &tmp_stat);
231 			else
232 				lstat(*argv, &tmp_stat);
233 
234 			if (S_ISDIR(tmp_stat.st_mode) && Rflag)
235 				type = DIR_TO_DNE;
236 			else
237 				type = FILE_TO_FILE;
238 		} else
239 			type = FILE_TO_FILE;
240 
241 		if (have_trailing_slash && type == FILE_TO_FILE) {
242 			if (r == -1)
243 				errc(1, ENOENT, "%s", target);
244 			else
245 				errc(1, ENOTDIR, "%s", target);
246 		}
247 	} else {
248 		/*
249 		 * Case (2).  Target is a directory.
250 		 */
251 		type = FILE_TO_DIR;
252 	}
253 
254 	/*
255 	 * For DIR_TO_DNE, we could provide copy() with the to_stat we've
256 	 * already allocated on the stack here that isn't being used for
257 	 * anything.  Not doing so, though, simplifies later logic a little bit
258 	 * as we need to skip checking root_stat on the first iteration and
259 	 * ensure that we set it with the first mkdir().
260 	 */
261 	exit (copy(argv, type, fts_options, (type == DIR_TO_DNE ? NULL :
262 	    &to_stat)));
263 }
264 
265 static int
copy(char * argv[],enum op type,int fts_options,struct stat * root_stat)266 copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
267 {
268 	char rootname[NAME_MAX];
269 	struct stat created_root_stat, to_stat, *curr_stat;
270 	FTS *ftsp;
271 	FTSENT *curr;
272 	char *recpath = NULL, *sep;
273 	int atflags, dne, badcp, len, level, rval;
274 	mode_t mask, mode;
275 	bool beneath = Rflag && type != FILE_TO_FILE;
276 
277 	/*
278 	 * Keep an inverted copy of the umask, for use in correcting
279 	 * permissions on created directories when not using -p.
280 	 */
281 	mask = ~umask(0777);
282 	umask(~mask);
283 
284 	if (type == FILE_TO_FILE) {
285 		to.dir = AT_FDCWD;
286 		to.end = to.path + strlcpy(to.path, to.base, sizeof(to.path));
287 		to.base[0] = '\0';
288 	} else if (type == FILE_TO_DIR) {
289 		to.dir = open(to.base, O_DIRECTORY | O_SEARCH);
290 		if (to.dir < 0)
291 			err(1, "%s", to.base);
292 		/*
293 		 * We have previously made sure there is room for this.
294 		 */
295 		if (strcmp(to.base, "/") != 0) {
296 			sep = strchr(to.base, '\0');
297 			sep[0] = '/';
298 			sep[1] = '\0';
299 		}
300 	} else {
301 		/*
302 		 * We will create the destination directory imminently.
303 		 */
304 		to.dir = -1;
305 	}
306 
307 	level = FTS_ROOTLEVEL;
308 	if ((ftsp = fts_open(argv, fts_options, NULL)) == NULL)
309 		err(1, "fts_open");
310 	for (badcp = rval = 0;
311 	     (curr = fts_read(ftsp)) != NULL;
312 	     badcp = 0, *to.end = '\0') {
313 		curr_stat = curr->fts_statp;
314 		switch (curr->fts_info) {
315 		case FTS_NS:
316 		case FTS_DNR:
317 		case FTS_ERR:
318 			if (level > curr->fts_level) {
319 				/* leaving a directory; remove its name from to.path */
320 				if (type == DIR_TO_DNE &&
321 				    curr->fts_level == FTS_ROOTLEVEL) {
322 					/* this is actually our created root */
323 				} else {
324 					while (to.end > to.path && *to.end != '/')
325 						to.end--;
326 					assert(strcmp(to.end + (*to.end == '/'),
327 					    curr->fts_name) == 0);
328 					*to.end = '\0';
329 				}
330 				level--;
331 			}
332 			warnc(curr->fts_errno, "%s", curr->fts_path);
333 			badcp = rval = 1;
334 			continue;
335 		case FTS_DC:			/* Warn, continue. */
336 			warnx("%s: directory causes a cycle", curr->fts_path);
337 			badcp = rval = 1;
338 			continue;
339 		case FTS_D:
340 			/*
341 			 * Stash the root basename off for detecting
342 			 * recursion later.
343 			 *
344 			 * This will be essential if the root is a symlink
345 			 * and we're rolling with -L or -H.  The later
346 			 * bits will need this bit in particular.
347 			 */
348 			if (curr->fts_level == FTS_ROOTLEVEL) {
349 				strlcpy(rootname, curr->fts_name,
350 				    sizeof(rootname));
351 			}
352 			/* we must have a destination! */
353 			if (type == DIR_TO_DNE &&
354 			    curr->fts_level == FTS_ROOTLEVEL) {
355 				assert(to.dir < 0);
356 				assert(root_stat == NULL);
357 				mode = curr_stat->st_mode | S_IRWXU;
358 				/*
359 				 * Will our umask prevent us from entering
360 				 * the directory after we create it?
361 				 */
362 				if (~mask & S_IRWXU)
363 					umask(~mask & ~S_IRWXU);
364 				if (mkdir(to.base, mode) != 0) {
365 					warn("%s", to.base);
366 					fts_set(ftsp, curr, FTS_SKIP);
367 					badcp = rval = 1;
368 					if (~mask & S_IRWXU)
369 						umask(~mask);
370 					continue;
371 				}
372 				to.dir = open(to.base, O_DIRECTORY | O_SEARCH);
373 				if (to.dir < 0) {
374 					warn("%s", to.base);
375 					(void)rmdir(to.base);
376 					fts_set(ftsp, curr, FTS_SKIP);
377 					badcp = rval = 1;
378 					if (~mask & S_IRWXU)
379 						umask(~mask);
380 					continue;
381 				}
382 				if (fstat(to.dir, &created_root_stat) != 0) {
383 					warn("%s", to.base);
384 					(void)close(to.dir);
385 					(void)rmdir(to.base);
386 					fts_set(ftsp, curr, FTS_SKIP);
387 					to.dir = -1;
388 					badcp = rval = 1;
389 					if (~mask & S_IRWXU)
390 						umask(~mask);
391 					continue;
392 				}
393 				if (~mask & S_IRWXU)
394 					umask(~mask);
395 				root_stat = &created_root_stat;
396 				curr->fts_number = 1;
397 				/*
398 				 * We have previously made sure there is
399 				 * room for this.
400 				 */
401 				sep = strchr(to.base, '\0');
402 				sep[0] = '/';
403 				sep[1] = '\0';
404 			} else {
405 				/* entering a directory; append its name to to.path */
406 				len = snprintf(to.end, END(to.path) - to.end, "%s%s",
407 				    to.end > to.path ? "/" : "", curr->fts_name);
408 				if (to.end + len >= END(to.path)) {
409 					*to.end = '\0';
410 					warnc(ENAMETOOLONG, "%s%s%s%s", to.base,
411 					    to.path, to.end > to.path ? "/" : "",
412 					    curr->fts_name);
413 					fts_set(ftsp, curr, FTS_SKIP);
414 					badcp = rval = 1;
415 					continue;
416 				}
417 				to.end += len;
418 			}
419 			level++;
420 			/*
421 			 * We're on the verge of recursing on ourselves.
422 			 * Either we need to stop right here (we knowingly
423 			 * just created it), or we will in an immediate
424 			 * descendant.  Record the path of the immediate
425 			 * descendant to make our lives a little less
426 			 * complicated looking.
427 			 */
428 			if (type != FILE_TO_FILE &&
429 			    root_stat->st_dev == curr_stat->st_dev &&
430 			    root_stat->st_ino == curr_stat->st_ino) {
431 				assert(recpath == NULL);
432 				if (root_stat == &created_root_stat) {
433 					/*
434 					 * This directory didn't exist
435 					 * when we started, we created it
436 					 * as part of traversal.  Stop
437 					 * right here before we do
438 					 * something silly.
439 					 */
440 					fts_set(ftsp, curr, FTS_SKIP);
441 					continue;
442 				}
443 				if (asprintf(&recpath, "%s/%s", to.path,
444 				    rootname) < 0) {
445 					warnc(ENOMEM, NULL);
446 					fts_set(ftsp, curr, FTS_SKIP);
447 					badcp = rval = 1;
448 					continue;
449 				}
450 			}
451 			if (recpath != NULL &&
452 			    strcmp(recpath, to.path) == 0) {
453 				fts_set(ftsp, curr, FTS_SKIP);
454 				continue;
455 			}
456 			break;
457 		case FTS_DP:
458 			/*
459 			 * We are nearly finished with this directory.  If we
460 			 * didn't actually copy it, or otherwise don't need to
461 			 * change its attributes, then we are done.
462 			 *
463 			 * If -p is in effect, set all the attributes.
464 			 * Otherwise, set the correct permissions, limited
465 			 * by the umask.  Optimise by avoiding a chmod()
466 			 * if possible (which is usually the case if we
467 			 * made the directory).  Note that mkdir() does not
468 			 * honour setuid, setgid and sticky bits, but we
469 			 * normally want to preserve them on directories.
470 			 */
471 			if (curr->fts_number && pflag) {
472 				int fd = *to.path ? -1 : to.dir;
473 				if (setfile(curr_stat, fd, true))
474 					rval = 1;
475 				if (preserve_dir_acls(curr->fts_accpath,
476 				    to.path) != 0)
477 					rval = 1;
478 			} else if (curr->fts_number) {
479 				const char *path = *to.path ? to.path : dot;
480 				mode = curr_stat->st_mode;
481 				if (fchmodat(to.dir, path, mode & mask, 0) != 0) {
482 					warn("chmod: %s%s", to.base, to.path);
483 					rval = 1;
484 				}
485 			}
486 			if (level > curr->fts_level) {
487 				/* leaving a directory; remove its name from to.path */
488 				if (type == DIR_TO_DNE &&
489 				    curr->fts_level == FTS_ROOTLEVEL) {
490 					/* this is actually our created root */
491 				} else {
492 					while (to.end > to.path && *to.end != '/')
493 						to.end--;
494 					assert(strcmp(to.end + (*to.end == '/'),
495 					    curr->fts_name) == 0);
496 					*to.end = '\0';
497 				}
498 				level--;
499 			}
500 			continue;
501 		default:
502 			/* something else: append its name to to.path */
503 			if (type == FILE_TO_FILE)
504 				break;
505 			len = snprintf(to.end, END(to.path) - to.end, "%s%s",
506 			    to.end > to.path ? "/" : "", curr->fts_name);
507 			if (to.end + len >= END(to.path)) {
508 				*to.end = '\0';
509 				warnc(ENAMETOOLONG, "%s%s%s%s", to.base,
510 				    to.path, to.end > to.path ? "/" : "",
511 				    curr->fts_name);
512 				badcp = rval = 1;
513 				continue;
514 			}
515 			/* intentionally do not update to.end */
516 			break;
517 		}
518 
519 		/* Not an error but need to remember it happened. */
520 		if (to.path[0] == '\0') {
521 			/*
522 			 * This can happen in two cases:
523 			 * - DIR_TO_DNE; we created the directory and
524 			 *   populated root_stat earlier.
525 			 * - FILE_TO_DIR if a source has a trailing slash;
526 			 *   the caller populated root_stat.
527 			 */
528 			dne = false;
529 			to_stat = *root_stat;
530 		} else {
531 			atflags = beneath ? AT_RESOLVE_BENEATH : 0;
532 			if (curr->fts_info == FTS_D || curr->fts_info == FTS_SL)
533 				atflags |= AT_SYMLINK_NOFOLLOW;
534 			dne = fstatat(to.dir, to.path, &to_stat, atflags) != 0;
535 		}
536 
537 		/* Check if source and destination are identical. */
538 		if (!dne &&
539 		    to_stat.st_dev == curr_stat->st_dev &&
540 		    to_stat.st_ino == curr_stat->st_ino) {
541 			warnx("%s%s and %s are identical (not copied).",
542 			    to.base, to.path, curr->fts_path);
543 			badcp = rval = 1;
544 			if (S_ISDIR(curr_stat->st_mode))
545 				fts_set(ftsp, curr, FTS_SKIP);
546 			continue;
547 		}
548 
549 		switch (curr_stat->st_mode & S_IFMT) {
550 		case S_IFLNK:
551 			if ((fts_options & FTS_LOGICAL) ||
552 			    ((fts_options & FTS_COMFOLLOW) &&
553 			    curr->fts_level == 0)) {
554 				/*
555 				 * We asked FTS to follow links but got
556 				 * here anyway, which means the target is
557 				 * nonexistent or inaccessible.  Let
558 				 * copy_file() deal with the error.
559 				 */
560 				if (copy_file(curr, dne, beneath))
561 					badcp = rval = 1;
562 			} else {
563 				/* Copy the link. */
564 				if (copy_link(curr, dne, beneath))
565 					badcp = rval = 1;
566 			}
567 			break;
568 		case S_IFDIR:
569 			if (!Rflag) {
570 				warnx("%s is a directory (not copied).",
571 				    curr->fts_path);
572 				fts_set(ftsp, curr, FTS_SKIP);
573 				badcp = rval = 1;
574 				break;
575 			}
576 			/*
577 			 * If the directory doesn't exist, create the new
578 			 * one with the from file mode plus owner RWX bits,
579 			 * modified by the umask.  Trade-off between being
580 			 * able to write the directory (if from directory is
581 			 * 555) and not causing a permissions race.  If the
582 			 * umask blocks owner writes, we fail.
583 			 */
584 			if (dne) {
585 				mode = curr_stat->st_mode | S_IRWXU;
586 				/*
587 				 * Will our umask prevent us from entering
588 				 * the directory after we create it?
589 				 */
590 				if (~mask & S_IRWXU)
591 					umask(~mask & ~S_IRWXU);
592 				if (mkdirat(to.dir, to.path, mode) != 0) {
593 					warn("%s%s", to.base, to.path);
594 					fts_set(ftsp, curr, FTS_SKIP);
595 					badcp = rval = 1;
596 					if (~mask & S_IRWXU)
597 						umask(~mask);
598 					break;
599 				}
600 				if (~mask & S_IRWXU)
601 					umask(~mask);
602 			} else if (!S_ISDIR(to_stat.st_mode)) {
603 				warnc(ENOTDIR, "%s%s", to.base, to.path);
604 				fts_set(ftsp, curr, FTS_SKIP);
605 				badcp = rval = 1;
606 				break;
607 			}
608 			/*
609 			 * Arrange to correct directory attributes later
610 			 * (in the post-order phase) if this is a new
611 			 * directory, or if the -p flag is in effect.
612 			 * Note that fts_number may already be set if this
613 			 * is the newly created destination directory.
614 			 */
615 			curr->fts_number |= pflag || dne;
616 			break;
617 		case S_IFBLK:
618 		case S_IFCHR:
619 			if (Rflag && !sflag) {
620 				if (copy_special(curr_stat, dne, beneath))
621 					badcp = rval = 1;
622 			} else {
623 				if (copy_file(curr, dne, beneath))
624 					badcp = rval = 1;
625 			}
626 			break;
627 		case S_IFSOCK:
628 			warnx("%s is a socket (not copied).",
629 			    curr->fts_path);
630 			break;
631 		case S_IFIFO:
632 			if (Rflag && !sflag) {
633 				if (copy_fifo(curr_stat, dne, beneath))
634 					badcp = rval = 1;
635 			} else {
636 				if (copy_file(curr, dne, beneath))
637 					badcp = rval = 1;
638 			}
639 			break;
640 		default:
641 			if (copy_file(curr, dne, beneath))
642 				badcp = rval = 1;
643 			break;
644 		}
645 		if (vflag && !badcp)
646 			(void)printf("%s -> %s%s\n", curr->fts_path, to.base, to.path);
647 	}
648 	assert(level == FTS_ROOTLEVEL);
649 	if (errno)
650 		err(1, "fts_read");
651 	(void)fts_close(ftsp);
652 	if (to.dir != AT_FDCWD && to.dir >= 0)
653 		(void)close(to.dir);
654 	free(recpath);
655 	return (rval);
656 }
657 
658 static void
siginfo(int sig __unused)659 siginfo(int sig __unused)
660 {
661 
662 	info = 1;
663 }
664