1 /*
2 * Copyright 2005-2009, Ingo Weinhold, ingo_weinhold@gmx.de.
3 * Distributed under the terms of the MIT License.
4 */
5
6 #include <errno.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <unistd.h>
11
12 #include <Directory.h>
13 #include <Entry.h>
14 #include <File.h>
15 #include <fs_attr.h>
16 #include <Mime.h>
17 #include <Node.h>
18 #include <Path.h>
19 #include <SymLink.h>
20 #include <TypeConstants.h>
21
22 #include <EntryFilter.h>
23
24
25 using BPrivate::EntryFilter;
26
27
28 static const char *kCommandName = "copyattr";
29 static const int kCopyBufferSize = 64 * 1024; // 64 KB
30
31 static int kArgc;
32 static const char *const *kArgv;
33
34 // usage
35 const char *kUsage =
36 "Usage: %s <options> <source> [ ... ] <destination>\n"
37 "\n"
38 "Copies attributes from one or more files to another, or copies one or more\n"
39 "files or directories, with all or a specified subset of their attributes, to\n"
40 "another location.\n"
41 "\n"
42 "If option \"-d\"/\"--data\" is given, the behavior is similar to \"cp -df\",\n"
43 "save that attributes are copied. That is, if more than one source file is\n"
44 "given, the destination file must be a directory. If the destination is a\n"
45 "directory (or a symlink to a directory), the source files are copied into\n"
46 "the destination directory. Entries that are in the way are removed, unless\n"
47 "they are directories. If the source is a directory too, the attributes will\n"
48 "be copied and, if recursive operation is specified, the program continues\n"
49 "copying the contents of the source directory. If the source is not a\n"
50 "directory the program aborts with an error message.\n"
51 "\n"
52 "If option \"-d\"/\"--data\" is not given, only attributes are copied.\n"
53 "Regardless of the file type of the destination, the attributes of the source\n"
54 "files are copied to it. If an attribute with the same name as one to be\n"
55 "copied already exists, it is replaced. If more than one source file is\n"
56 "specified the semantics are similar to invoking the program multiple times\n"
57 "with the same options and destination and only one source file at a time,\n"
58 "in the order the source files are given. If recursive operation is\n"
59 "specified, the program recursively copies the attributes of the directory\n"
60 "contents; if the destination file is not a directory, or for a source entry\n"
61 "there exists no destination entry, the program aborts with an error\n"
62 "message.\n"
63 "\n"
64 "Note, that the behavior of the program differs from the one shipped with\n"
65 "BeOS R5.\n"
66 "\n"
67 "Options:\n"
68 " -d, --data - Copy the data of the file(s), too.\n"
69 " -h, --help - Print this help text and exit.\n"
70 " -m, --move - If -d is given, the source files are removed after\n"
71 " being copied. Has no effect otherwise.\n"
72 " -n, --name <name> - Only copy the attribute with name <name>.\n"
73 " -r, --recursive - Copy directories recursively.\n"
74 " -t, --type <type> - Copy only the attributes of type <type>. If -n is\n"
75 " specified too, only the attribute matching the name\n"
76 " and the type is copied.\n"
77 " -x <pattern> - Exclude source entries matching <pattern>.\n"
78 " -X <pattern> - Exclude source paths matching <pattern>.\n"
79 " -v, --verbose - Print more messages.\n"
80 " -, -- - Marks the end of options. The arguments after, even\n"
81 " if starting with \"-\" are considered file names.\n"
82 "\n"
83 "Parameters:\n"
84 " <type> - One of: int, llong, string, mimestr, float, double,\n"
85 " boolean.\n"
86 ;
87
88 // supported attribute types
89 struct supported_attribute_type {
90 const char *type_name;
91 type_code type;
92 };
93
94 const supported_attribute_type kSupportedAttributeTypes[] = {
95 { "int", B_INT32_TYPE },
96 { "llong", B_INT64_TYPE },
97 { "string", B_STRING_TYPE },
98 { "mimestr", B_MIME_STRING_TYPE },
99 { "float", B_FLOAT_TYPE },
100 { "double", B_DOUBLE_TYPE },
101 { "boolean", B_BOOL_TYPE },
102 { NULL, 0 },
103 };
104
105 // AttributeFilter
106 struct AttributeFilter {
AttributeFilterAttributeFilter107 AttributeFilter()
108 :
109 fName(NULL),
110 fType(B_ANY_TYPE)
111 {
112 }
113
SetToAttributeFilter114 void SetTo(const char *name, type_code type)
115 {
116 fName = name;
117 fType = type;
118 }
119
FilterAttributeFilter120 bool Filter(const char *name, type_code type) const {
121 if (fName && strcmp(name, fName) != 0)
122 return false;
123
124 return (fType == B_ANY_TYPE || type == fType);
125 }
126
127 private:
128 const char *fName;
129 type_code fType;
130 };
131
132
133 // Parameters
134 struct Parameters {
ParametersParameters135 Parameters()
136 :
137 copy_data(false),
138 recursive(false),
139 move_files(false),
140 verbose(false)
141 {
142 }
143
144 bool copy_data;
145 bool recursive;
146 bool move_files;
147 bool verbose;
148 AttributeFilter attribute_filter;
149 EntryFilter entry_filter;
150 };
151
152
153 // print_usage
154 static void
print_usage(bool error)155 print_usage(bool error)
156 {
157 // get command name
158 const char *commandName = NULL;
159 if (kArgc > 0) {
160 if (const char *lastSlash = strchr(kArgv[0], '/'))
161 commandName = lastSlash + 1;
162 else
163 commandName = kArgv[0];
164 }
165
166 if (!commandName || strlen(commandName) == 0)
167 commandName = kCommandName;
168
169 // print usage
170 fprintf((error ? stderr : stdout), kUsage, commandName);
171 }
172
173
174 // print_usage_and_exit
175 static void
print_usage_and_exit(bool error)176 print_usage_and_exit(bool error)
177 {
178 print_usage(error);
179 exit(error ? 1 : 0);
180 }
181
182
183 // next_arg
184 static const char *
next_arg(int & argi,bool optional=false)185 next_arg(int &argi, bool optional = false)
186 {
187 if (argi >= kArgc) {
188 if (!optional)
189 print_usage_and_exit(true);
190 return NULL;
191 }
192
193 return kArgv[argi++];
194 }
195
196
197 // copy_attributes
198 static void
copy_attributes(const char * sourcePath,BNode & source,const char * destPath,BNode & destination,const Parameters & parameters)199 copy_attributes(const char *sourcePath, BNode &source, const char *destPath,
200 BNode &destination, const Parameters ¶meters)
201 {
202 char attrName[B_ATTR_NAME_LENGTH];
203 while (source.GetNextAttrName(attrName) == B_OK) {
204 // get attr info
205 attr_info attrInfo;
206 status_t error = source.GetAttrInfo(attrName, &attrInfo);
207 if (error != B_OK) {
208 fprintf(stderr, "Error: Failed to get info of attribute \"%s\" "
209 "of file \"%s\": %s\n", attrName, sourcePath, strerror(error));
210 exit(1);
211 }
212
213 // filter
214 if (!parameters.attribute_filter.Filter(attrName, attrInfo.type))
215 continue;
216
217 // copy the attribute
218 char buffer[kCopyBufferSize];
219 off_t offset = 0;
220 off_t bytesLeft = attrInfo.size;
221 // go at least once through the loop, so that empty attribute will be
222 // created as well
223 do {
224 size_t toRead = kCopyBufferSize;
225 if ((off_t)toRead > bytesLeft)
226 toRead = bytesLeft;
227
228 // read
229 ssize_t bytesRead = source.ReadAttr(attrName, attrInfo.type,
230 offset, buffer, toRead);
231 if (bytesRead < 0) {
232 fprintf(stderr, "Error: Failed to read attribute \"%s\" "
233 "of file \"%s\": %s\n", attrName, sourcePath,
234 strerror(bytesRead));
235 exit(1);
236 }
237
238 // write
239 ssize_t bytesWritten = destination.WriteAttr(attrName,
240 attrInfo.type, offset, buffer, bytesRead);
241 if (bytesWritten < 0) {
242 fprintf(stderr, "Error: Failed to write attribute \"%s\" "
243 "of file \"%s\": %s\n", attrName, destPath,
244 strerror(bytesWritten));
245 exit(1);
246 }
247
248 bytesLeft -= bytesRead;
249 offset += bytesRead;
250
251 } while (bytesLeft > 0);
252 }
253 }
254
255
256 // copy_file_data
257 static void
copy_file_data(const char * sourcePath,BFile & source,const char * destPath,BFile & destination,const Parameters & parameters)258 copy_file_data(const char *sourcePath, BFile &source, const char *destPath,
259 BFile &destination, const Parameters ¶meters)
260 {
261 char buffer[kCopyBufferSize];
262 off_t offset = 0;
263 while (true) {
264 // read
265 ssize_t bytesRead = source.ReadAt(offset, buffer, sizeof(buffer));
266 if (bytesRead < 0) {
267 fprintf(stderr, "Error: Failed to read from file \"%s\": %s\n",
268 sourcePath, strerror(bytesRead));
269 exit(1);
270 }
271
272 if (bytesRead == 0)
273 return;
274
275 // write
276 ssize_t bytesWritten = destination.WriteAt(offset, buffer, bytesRead);
277 if (bytesWritten < 0) {
278 fprintf(stderr, "Error: Failed to write to file \"%s\": %s\n",
279 destPath, strerror(bytesWritten));
280 exit(1);
281 }
282
283 offset += bytesRead;
284 }
285 }
286
287
288 // copy_entry
289 static void
copy_entry(const char * sourcePath,const char * destPath,const Parameters & parameters)290 copy_entry(const char *sourcePath, const char *destPath,
291 const Parameters ¶meters)
292 {
293 // apply entry filter
294 if (!parameters.entry_filter.Filter(sourcePath))
295 return;
296
297 // stat source
298 struct stat sourceStat;
299 if (lstat(sourcePath, &sourceStat) < 0) {
300 fprintf(stderr, "Error: Couldn't access \"%s\": %s\n", sourcePath,
301 strerror(errno));
302 exit(1);
303 }
304
305 // stat destination
306 struct stat destStat;
307 bool destExists = lstat(destPath, &destStat) == 0;
308
309 if (!destExists && !parameters.copy_data) {
310 fprintf(stderr, "Error: Destination file \"%s\" does not exist.\n",
311 destPath);
312 exit(1);
313 }
314
315 if (parameters.verbose)
316 printf("%s\n", destPath);
317
318 // check whether to delete/create the destination
319 bool unlinkDest = (destExists && parameters.copy_data);
320 bool createDest = parameters.copy_data;
321 if (destExists) {
322 if (S_ISDIR(destStat.st_mode)) {
323 if (S_ISDIR(sourceStat.st_mode)) {
324 // both are dirs; nothing to do
325 unlinkDest = false;
326 createDest = false;
327 } else if (parameters.copy_data || parameters.recursive) {
328 // destination is directory, but source isn't, and mode is
329 // not non-recursive attributes-only copy
330 fprintf(stderr, "Error: Can't copy \"%s\", since directory "
331 "\"%s\" is in the way.\n", sourcePath, destPath);
332 exit(1);
333 }
334 }
335 }
336
337 // unlink the destination
338 if (unlinkDest) {
339 if (unlink(destPath) < 0) {
340 fprintf(stderr, "Error: Failed to unlink \"%s\": %s\n", destPath,
341 strerror(errno));
342 exit(1);
343 }
344 }
345
346 // open source node
347 BNode _sourceNode;
348 BFile sourceFile;
349 BDirectory sourceDir;
350 BNode *sourceNode = NULL;
351 status_t error;
352
353 if (S_ISDIR(sourceStat.st_mode)) {
354 error = sourceDir.SetTo(sourcePath);
355 sourceNode = &sourceDir;
356 } else if (S_ISREG(sourceStat.st_mode)) {
357 error = sourceFile.SetTo(sourcePath, B_READ_ONLY);
358 sourceNode = &sourceFile;
359 } else {
360 error = _sourceNode.SetTo(sourcePath);
361 sourceNode = &_sourceNode;
362 }
363
364 if (error != B_OK) {
365 fprintf(stderr, "Error: Failed to open \"%s\": %s\n",
366 sourcePath, strerror(error));
367 exit(1);
368 }
369
370 // create the destination
371 BNode _destNode;
372 BDirectory destDir;
373 BFile destFile;
374 BSymLink destSymLink;
375 BNode *destNode = NULL;
376
377 if (createDest) {
378 if (S_ISDIR(sourceStat.st_mode)) {
379 // create dir
380 error = BDirectory().CreateDirectory(destPath, &destDir);
381 if (error != B_OK) {
382 fprintf(stderr, "Error: Failed to make directory \"%s\": %s\n",
383 destPath, strerror(error));
384 exit(1);
385 }
386
387 destNode = &destDir;
388
389 } else if (S_ISREG(sourceStat.st_mode)) {
390 // create file
391 error = BDirectory().CreateFile(destPath, &destFile);
392 if (error != B_OK) {
393 fprintf(stderr, "Error: Failed to create file \"%s\": %s\n",
394 destPath, strerror(error));
395 exit(1);
396 }
397
398 destNode = &destFile;
399
400 // copy file contents
401 copy_file_data(sourcePath, sourceFile, destPath, destFile,
402 parameters);
403
404 } else if (S_ISLNK(sourceStat.st_mode)) {
405 // read symlink
406 char linkTo[B_PATH_NAME_LENGTH + 1];
407 ssize_t bytesRead = readlink(sourcePath, linkTo,
408 sizeof(linkTo) - 1);
409 if (bytesRead < 0) {
410 fprintf(stderr, "Error: Failed to read symlink \"%s\": %s\n",
411 sourcePath, strerror(errno));
412 exit(1);
413 }
414
415 // null terminate the link contents
416 linkTo[bytesRead] = '\0';
417
418 // create symlink
419 error = BDirectory().CreateSymLink(destPath, linkTo, &destSymLink);
420 if (error != B_OK) {
421 fprintf(stderr, "Error: Failed to create symlink \"%s\": %s\n",
422 destPath, strerror(error));
423 exit(1);
424 }
425
426 destNode = &destSymLink;
427
428 } else {
429 fprintf(stderr, "Error: Source file \"%s\" has unsupported type.\n",
430 sourcePath);
431 exit(1);
432 }
433
434 // copy attributes (before setting the permissions!)
435 copy_attributes(sourcePath, *sourceNode, destPath, *destNode,
436 parameters);
437
438 // set file owner, group, permissions, times
439 destNode->SetOwner(sourceStat.st_uid);
440 destNode->SetGroup(sourceStat.st_gid);
441 destNode->SetPermissions(sourceStat.st_mode);
442 #ifdef HAIKU_TARGET_PLATFORM_HAIKU
443 destNode->SetCreationTime(sourceStat.st_crtime);
444 #endif
445 destNode->SetModificationTime(sourceStat.st_mtime);
446
447 } else {
448 // open destination node
449 error = _destNode.SetTo(destPath);
450 if (error != B_OK) {
451 fprintf(stderr, "Error: Failed to open \"%s\": %s\n",
452 destPath, strerror(error));
453 exit(1);
454 }
455
456 destNode = &_destNode;
457
458 // copy attributes
459 copy_attributes(sourcePath, *sourceNode, destPath, *destNode,
460 parameters);
461 }
462
463 // the destination node is no longer needed
464 destNode->Unset();
465
466 // recurse
467 if (parameters.recursive && S_ISDIR(sourceStat.st_mode)) {
468 char buffer[offsetof(struct dirent, d_name) + B_FILE_NAME_LENGTH];
469 dirent *entry = (dirent*)buffer;
470 while (sourceDir.GetNextDirents(entry, sizeof(buffer), 1) == 1) {
471 if (strcmp(entry->d_name, ".") == 0
472 || strcmp(entry->d_name, "..") == 0) {
473 continue;
474 }
475
476 // construct new entry paths
477 BPath sourceEntryPath;
478 error = sourceEntryPath.SetTo(sourcePath, entry->d_name);
479 if (error != B_OK) {
480 fprintf(stderr, "Error: Failed to construct entry path from "
481 "dir \"%s\" and name \"%s\": %s\n",
482 sourcePath, entry->d_name, strerror(error));
483 exit(1);
484 }
485
486 BPath destEntryPath;
487 error = destEntryPath.SetTo(destPath, entry->d_name);
488 if (error != B_OK) {
489 fprintf(stderr, "Error: Failed to construct entry path from "
490 "dir \"%s\" and name \"%s\": %s\n",
491 destPath, entry->d_name, strerror(error));
492 exit(1);
493 }
494
495 // copy the entry
496 copy_entry(sourceEntryPath.Path(), destEntryPath.Path(),
497 parameters);
498 }
499 }
500
501 // remove source in move mode
502 if (parameters.move_files) {
503 if (S_ISDIR(sourceStat.st_mode)) {
504 if (rmdir(sourcePath) < 0) {
505 fprintf(stderr, "Error: Failed to remove \"%s\": %s\n",
506 sourcePath, strerror(errno));
507 exit(1);
508 }
509
510 } else {
511 if (unlink(sourcePath) < 0) {
512 fprintf(stderr, "Error: Failed to unlink \"%s\": %s\n",
513 sourcePath, strerror(errno));
514 exit(1);
515 }
516 }
517 }
518 }
519
520 // copy_files
521 static void
copy_files(const char ** sourcePaths,int sourceCount,const char * destPath,const Parameters & parameters)522 copy_files(const char **sourcePaths, int sourceCount,
523 const char *destPath, const Parameters ¶meters)
524 {
525 // check, if destination exists
526 BEntry destEntry;
527 status_t error = destEntry.SetTo(destPath);
528 if (error != B_OK) {
529 fprintf(stderr, "Error: Couldn't access \"%s\": %s\n", destPath,
530 strerror(error));
531 exit(1);
532 }
533 bool destExists = destEntry.Exists();
534
535 // If it exists, check whether it is a directory. In case we don't copy
536 // the data, we pretend the destination is no directory, even if it is
537 // one.
538 bool destIsDir = false;
539 if (destExists && parameters.copy_data) {
540 struct stat st;
541 error = destEntry.GetStat(&st);
542 if (error != B_OK) {
543 fprintf(stderr, "Error: Failed to stat \"%s\": %s\n", destPath,
544 strerror(error));
545 exit(1);
546 }
547
548 if (S_ISDIR(st.st_mode)) {
549 destIsDir = true;
550 } else if (S_ISLNK(st.st_mode)) {
551 // a symlink -- check if it refers to a dir
552 BEntry resolvedDestEntry;
553 if (resolvedDestEntry.SetTo(destPath, true) == B_OK
554 && resolvedDestEntry.IsDirectory()) {
555 destIsDir = true;
556 }
557 }
558 }
559
560 // If we have multiple source files, the destination should be a directory,
561 // if we want to copy the file data.
562 if (sourceCount > 1 && parameters.copy_data && !destIsDir) {
563 fprintf(stderr, "Error: Destination needs to be a directory when "
564 "multiple source files are specified and option \"-d\" is "
565 "given.\n");
566 exit(1);
567 }
568
569 // iterate through the source files
570 for (int i = 0; i < sourceCount; i++) {
571 const char *sourcePath = sourcePaths[i];
572 // If the destination is a directory, we usually want to copy the
573 // sources into it. The user might have specified a source path ending
574 // in "/." or "/.." however, in which case we copy the contents of the
575 // given directory.
576 bool copySourceContentsOnly = false;
577 if (destIsDir) {
578 // skip trailing '/'s
579 int sourceLen = strlen(sourcePath);
580 while (sourceLen > 1 && sourcePath[sourceLen - 1] == '/')
581 sourceLen--;
582
583 // find the start of the leaf name
584 int leafStart = sourceLen;
585 while (leafStart > 0 && sourcePath[leafStart - 1] != '/')
586 leafStart--;
587
588 // If the path is the root directory or the leaf is "." or "..",
589 // we copy the contents only.
590 int leafLen = sourceLen - leafStart;
591 if (leafLen == 0 || (leafLen <= 2
592 && strncmp(sourcePath + leafStart, "..", leafLen) == 0)) {
593 copySourceContentsOnly = true;
594 }
595 }
596
597 if (destIsDir && !copySourceContentsOnly) {
598 // construct a usable destination entry path
599 // normalize source path
600 BPath normalizedSourcePath;
601 error = normalizedSourcePath.SetTo(sourcePath);
602 if (error != B_OK) {
603 fprintf(stderr, "Error: Invalid path \"%s\".\n", sourcePath);
604 exit(1);
605 }
606
607 BPath destEntryPath;
608 error = destEntryPath.SetTo(destPath, normalizedSourcePath.Leaf());
609 if (error != B_OK) {
610 fprintf(stderr, "Error: Failed to get destination path for "
611 "source \"%s\" and destination directory \"%s\".\n",
612 sourcePath, destPath);
613 exit(1);
614 }
615
616 copy_entry(normalizedSourcePath.Path(), destEntryPath.Path(),
617 parameters);
618 } else {
619 copy_entry(sourcePath, destPath, parameters);
620 }
621 }
622 }
623
624
625 // main
626 int
main(int argc,const char * const * argv)627 main(int argc, const char *const *argv)
628 {
629 kArgc = argc;
630 kArgv = argv;
631
632 // parameters
633 Parameters parameters;
634 const char *attributeName = NULL;
635 const char *attributeTypeString = NULL;
636 const char **files = new const char*[argc];
637 int fileCount = 0;
638
639 // parse the arguments
640 bool moreOptions = true;
641 for (int argi = 1; argi < argc; ) {
642 const char *arg = argv[argi++];
643 if (moreOptions && arg[0] == '-') {
644 if (strcmp(arg, "-d") == 0 || strcmp(arg, "--data") == 0) {
645 parameters.copy_data = true;
646
647 } else if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) {
648 print_usage_and_exit(false);
649
650 } else if (strcmp(arg, "-m") == 0 || strcmp(arg, "--move") == 0) {
651 parameters.move_files = true;
652
653 } else if (strcmp(arg, "-n") == 0 || strcmp(arg, "--name") == 0) {
654 if (attributeName) {
655 fprintf(stderr, "Error: Only one attribute name can be "
656 "specified.\n");
657 exit(1);
658 }
659
660 attributeName = next_arg(argi);
661
662 } else if (strcmp(arg, "-r") == 0
663 || strcmp(arg, "--recursive") == 0) {
664 parameters.recursive = true;
665
666 } else if (strcmp(arg, "-t") == 0 || strcmp(arg, "--type") == 0) {
667 if (attributeTypeString) {
668 fprintf(stderr, "Error: Only one attribute type can be "
669 "specified.\n");
670 exit(1);
671 }
672
673 attributeTypeString = next_arg(argi);
674
675 } else if (strcmp(arg, "-v") == 0
676 || strcmp(arg, "--verbose") == 0) {
677 parameters.verbose = true;
678
679 } else if (strcmp(arg, "-x") == 0) {
680 parameters.entry_filter.AddExcludeFilter(next_arg(argi), true);
681
682 } else if (strcmp(arg, "-X") == 0) {
683 parameters.entry_filter.AddExcludeFilter(next_arg(argi), false);
684
685 } else if (strcmp(arg, "-") == 0 || strcmp(arg, "--") == 0) {
686 moreOptions = false;
687
688 } else {
689 fprintf(stderr, "Error: Invalid option: \"%s\"\n", arg);
690 print_usage_and_exit(true);
691 }
692
693 } else {
694 // file
695 files[fileCount++] = arg;
696 }
697 }
698
699 // check parameters
700
701 // enough files
702 if (fileCount < 2) {
703 fprintf(stderr, "Error: Not enough file names specified.\n");
704 print_usage_and_exit(true);
705 }
706
707 // attribute type
708 type_code attributeType = B_ANY_TYPE;
709 if (attributeTypeString) {
710 bool found = false;
711 for (int i = 0; kSupportedAttributeTypes[i].type_name; i++) {
712 if (strcmp(attributeTypeString,
713 kSupportedAttributeTypes[i].type_name) == 0) {
714 found = true;
715 attributeType = kSupportedAttributeTypes[i].type;
716 break;
717 }
718 }
719
720 if (!found) {
721 fprintf(stderr, "Error: Unsupported attribute type: \"%s\"\n",
722 attributeTypeString);
723 exit(1);
724 }
725 }
726
727 // init the attribute filter
728 parameters.attribute_filter.SetTo(attributeName, attributeType);
729
730 // turn of move_files, if we are not copying the file data
731 parameters.move_files &= parameters.copy_data;
732
733 // do the copying
734 fileCount--;
735 const char *destination = files[fileCount];
736 files[fileCount] = NULL;
737 copy_files(files, fileCount, destination, parameters);
738 delete[] files;
739
740 return 0;
741 }
742