OSDN Git Service

* security.cc (get_file_attribute): Don't set errno.
[pf3gnuchains/pf3gnuchains3x.git] / tcl / mac / tclMacFile.c
1 /* 
2  * tclMacFile.c --
3  *
4  *      This file implements the channel drivers for Macintosh
5  *      files.  It also comtains Macintosh version of other Tcl
6  *      functions that deal with the file system.
7  *
8  * Copyright (c) 1995-1997 Sun Microsystems, Inc.
9  *
10  * See the file "license.terms" for information on usage and redistribution
11  * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
12  *
13  * RCS: @(#) $Id$
14  */
15
16 /*
17  * Note: This code eventually needs to support async I/O.  In doing this
18  * we will need to keep track of all current async I/O.  If exit to shell
19  * is called - we shouldn't exit until all asyc I/O completes.
20  */
21
22 #include "tclInt.h"
23 #include "tclPort.h"
24 #include "tclMacInt.h"
25 #include <Aliases.h>
26 #include <Errors.h>
27 #include <Processes.h>
28 #include <Strings.h>
29 #include <Types.h>
30 #include <MoreFiles.h>
31 #include <MoreFilesExtras.h>
32 #include <FSpCompat.h>
33
34 /*
35  * Static variables used by the TclpStat function.
36  */
37 static int initalized = false;
38 static long gmt_offset;
39
40 /*
41  * The variable below caches the name of the current working directory
42  * in order to avoid repeated calls to getcwd.  The string is malloc-ed.
43  * NULL means the cache needs to be refreshed.
44  */
45
46 static char *currentDir =  NULL;
47 \f
48 /*
49  *----------------------------------------------------------------------
50  *
51  * TclChdir --
52  *
53  *      Change the current working directory.
54  *
55  * Results:
56  *      The result is a standard Tcl result.  If an error occurs and 
57  *      interp isn't NULL, an error message is left in interp->result.
58  *
59  * Side effects:
60  *      The working directory for this application is changed.  Also
61  *      the cache maintained used by TclGetCwd is deallocated and
62  *      set to NULL.
63  *
64  *----------------------------------------------------------------------
65  */
66
67 int
68 TclChdir(
69     Tcl_Interp *interp,         /* If non NULL, used for error reporting. */
70     char *dirName)              /* Path to new working directory. */
71 {
72     FSSpec spec;
73     OSErr err;
74     Boolean isFolder;
75     long dirID;
76
77     if (currentDir != NULL) {
78         ckfree(currentDir);
79         currentDir = NULL;
80     }
81
82     err = FSpLocationFromPath(strlen(dirName), dirName, &spec);
83     if (err != noErr) {
84         errno = ENOENT;
85         goto chdirError;
86     }
87     
88     err = FSpGetDirectoryID(&spec, &dirID, &isFolder);
89     if (err != noErr) {
90         errno = ENOENT;
91         goto chdirError;
92     }
93
94     if (isFolder != true) {
95         errno = ENOTDIR;
96         goto chdirError;
97     }
98
99     err = FSpSetDefaultDir(&spec);
100     if (err != noErr) {
101         switch (err) {
102             case afpAccessDenied:
103                 errno = EACCES;
104                 break;
105             default:
106                 errno = ENOENT;
107         }
108         goto chdirError;
109     }
110
111     return TCL_OK;
112     chdirError:
113     if (interp != NULL) {
114         Tcl_AppendResult(interp, "couldn't change working directory to \"",
115                 dirName, "\": ", Tcl_PosixError(interp), (char *) NULL);
116     }
117     return TCL_ERROR;
118 }
119 \f
120 /*
121  *----------------------------------------------------------------------
122  *
123  * TclGetCwd --
124  *
125  *      Return the path name of the current working directory.
126  *
127  * Results:
128  *      The result is the full path name of the current working
129  *      directory, or NULL if an error occurred while figuring it
130  *      out.  If an error occurs and interp isn't NULL, an error
131  *      message is left in interp->result.
132  *
133  * Side effects:
134  *      The path name is cached to avoid having to recompute it
135  *      on future calls;  if it is already cached, the cached
136  *      value is returned.
137  *
138  *----------------------------------------------------------------------
139  */
140
141 char *
142 TclGetCwd(
143     Tcl_Interp *interp)         /* If non NULL, used for error reporting. */
144 {
145     FSSpec theSpec;
146     int length;
147     Handle pathHandle = NULL;
148     
149     if (currentDir == NULL) {
150         if (FSpGetDefaultDir(&theSpec) != noErr) {
151             if (interp != NULL) {
152                 interp->result = "error getting working directory name";
153             }
154             return NULL;
155         }
156         if (FSpPathFromLocation(&theSpec, &length, &pathHandle) != noErr) {
157             if (interp != NULL) {
158                 interp->result = "error getting working directory name";
159             }
160             return NULL;
161         }
162         HLock(pathHandle);
163         currentDir = (char *) ckalloc((unsigned) (length + 1));
164         strcpy(currentDir, *pathHandle);
165         HUnlock(pathHandle);
166         DisposeHandle(pathHandle);      
167     }
168     return currentDir;
169 }
170 \f
171 /*
172  *----------------------------------------------------------------------
173  *
174  * Tcl_WaitPid --
175  *
176  *      Fakes a call to wait pid.
177  *
178  * Results:
179  *      Always returns -1.
180  *
181  * Side effects:
182  *      None.
183  *
184  *----------------------------------------------------------------------
185  */
186
187 Tcl_Pid
188 Tcl_WaitPid(
189     Tcl_Pid pid,
190     int *statPtr,
191     int options)
192 {
193     return (Tcl_Pid) -1;
194 }
195 \f
196 /*
197  *----------------------------------------------------------------------
198  *
199  * Tcl_FindExecutable --
200  *
201  *      This procedure computes the absolute path name of the current
202  *      application, given its argv[0] value.  However, this
203  *      implementation doesn't use of need the argv[0] value.  NULL
204  *      may be passed in its place.
205  *
206  * Results:
207  *      None.
208  *
209  * Side effects:
210  *      The variable tclExecutableName gets filled in with the file
211  *      name for the application, if we figured it out.  If we couldn't
212  *      figure it out, Tcl_FindExecutable is set to NULL.
213  *
214  *----------------------------------------------------------------------
215  */
216
217 void
218 Tcl_FindExecutable(
219     char *argv0)                /* The value of the application's argv[0]. */
220 {
221     ProcessSerialNumber psn;
222     ProcessInfoRec info;
223     Str63 appName;
224     FSSpec fileSpec;
225     int pathLength;
226     Handle pathName = NULL;
227     OSErr err;
228     
229     GetCurrentProcess(&psn);
230     info.processInfoLength = sizeof(ProcessInfoRec);
231     info.processName = appName;
232     info.processAppSpec = &fileSpec;
233     GetProcessInformation(&psn, &info);
234
235     if (tclExecutableName != NULL) {
236         ckfree(tclExecutableName);
237         tclExecutableName = NULL;
238     }
239     
240     err = FSpPathFromLocation(&fileSpec, &pathLength, &pathName);
241
242     tclExecutableName = (char *) ckalloc((unsigned) pathLength + 1);
243     HLock(pathName);
244     strcpy(tclExecutableName, *pathName);
245     HUnlock(pathName);
246     DisposeHandle(pathName);    
247 }
248 \f
249 /*
250  *----------------------------------------------------------------------
251  *
252  * TclGetUserHome --
253  *
254  *      This function takes the passed in user name and finds the
255  *      corresponding home directory specified in the password file.
256  *
257  * Results:
258  *      On a Macintosh we always return a NULL.
259  *
260  * Side effects:
261  *      None.
262  *
263  *----------------------------------------------------------------------
264  */
265
266 char *
267 TclGetUserHome(
268     char *name,                 /* User name to use to find home directory. */
269     Tcl_DString *bufferPtr)     /* May be used to hold result.  Must not hold
270                                  * anything at the time of the call, and need
271                                  * not even be initialized. */
272 {
273     return NULL;
274 }
275 \f
276 /*
277  *----------------------------------------------------------------------
278  *
279  * TclMatchFiles --
280  *
281  *      This routine is used by the globbing code to search a
282  *      directory for all files which match a given pattern.
283  *
284  * Results: 
285  *      If the tail argument is NULL, then the matching files are
286  *      added to the interp->result.  Otherwise, TclDoGlob is called
287  *      recursively for each matching subdirectory.  The return value
288  *      is a standard Tcl result indicating whether an error occurred
289  *      in globbing.
290  *
291  * Side effects:
292  *      None.
293  *
294  *---------------------------------------------------------------------- */
295
296 int
297 TclMatchFiles(
298     Tcl_Interp *interp,         /* Interpreter to receive results. */
299     char *separators,           /* Directory separators to pass to TclDoGlob. */
300     Tcl_DString *dirPtr,        /* Contains path to directory to search. */
301     char *pattern,              /* Pattern to match against. */
302     char *tail)                 /* Pointer to end of pattern.  Tail must
303                                  * point to a location in pattern. */
304 {
305     char *dirName, *patternEnd = tail;
306     char savedChar;
307     int result = TCL_OK;
308     int baseLength = Tcl_DStringLength(dirPtr);
309     CInfoPBRec pb;
310     OSErr err;
311     FSSpec dirSpec;
312     Boolean isDirectory;
313     long dirID;
314     short itemIndex;
315     Str255 fileName;
316     
317
318     /*
319      * Make sure that the directory part of the name really is a
320      * directory.
321      */
322
323     dirName = dirPtr->string;
324     FSpLocationFromPath(strlen(dirName), dirName, &dirSpec);
325     err = FSpGetDirectoryID(&dirSpec, &dirID, &isDirectory);
326     if ((err != noErr) || !isDirectory) {
327         return TCL_OK;
328     }
329
330     /*
331      * Now open the directory for reading and iterate over the contents.
332      */
333
334     pb.hFileInfo.ioVRefNum = dirSpec.vRefNum;
335     pb.hFileInfo.ioDirID = dirID;
336     pb.hFileInfo.ioNamePtr = (StringPtr) fileName;
337     pb.hFileInfo.ioFDirIndex = itemIndex = 1;
338     
339     /*
340      * Clean up the end of the pattern and the tail pointer.  Leave
341      * the tail pointing to the first character after the path separator
342      * following the pattern, or NULL.  Also, ensure that the pattern
343      * is null-terminated.
344      */
345
346     if (*tail == '\\') {
347         tail++;
348     }
349     if (*tail == '\0') {
350         tail = NULL;
351     } else {
352         tail++;
353     }
354     savedChar = *patternEnd;
355     *patternEnd = '\0';
356
357     while (1) {
358         pb.hFileInfo.ioFDirIndex = itemIndex;
359         pb.hFileInfo.ioDirID = dirID;
360         err = PBGetCatInfoSync(&pb);
361         if (err != noErr) {
362             break;
363         }
364
365         /*
366          * Now check to see if the file matches.  If there are more
367          * characters to be processed, then ensure matching files are
368          * directories before calling TclDoGlob. Otherwise, just add
369          * the file to the result.
370          */
371
372         p2cstr(fileName);
373         if (Tcl_StringMatch((char *) fileName, pattern)) {
374             Tcl_DStringSetLength(dirPtr, baseLength);
375             Tcl_DStringAppend(dirPtr, (char *) fileName, -1);
376             if (tail == NULL) {
377                 if ((dirPtr->length > 1) &&
378                         (strchr(dirPtr->string+1, ':') == NULL)) {
379                     Tcl_AppendElement(interp, dirPtr->string+1);
380                 } else {
381                     Tcl_AppendElement(interp, dirPtr->string);
382                 }
383             } else if ((pb.hFileInfo.ioFlAttrib & ioDirMask) != 0) {
384                 Tcl_DStringAppend(dirPtr, ":", 1);
385                 result = TclDoGlob(interp, separators, dirPtr, tail);
386                 if (result != TCL_OK) {
387                     break;
388                 }
389             }
390         }
391         
392         itemIndex++;
393     }
394     *patternEnd = savedChar;
395
396     return result;
397 }
398 \f
399 /*
400  *----------------------------------------------------------------------
401  *
402  * TclpStat --
403  *
404  *      This function replaces the library version of stat.  The stat
405  *      function provided by most Mac compiliers is rather broken and
406  *      incomplete.
407  *
408  * Results:
409  *      See stat documentation.
410  *
411  * Side effects:
412  *      See stat documentation.
413  *
414  *----------------------------------------------------------------------
415  */
416
417 int
418 TclpStat(
419     CONST char *path,
420     struct stat *buf)
421 {
422     HFileInfo fpb;
423     HVolumeParam vpb;
424     OSErr err;
425     FSSpec fileSpec;
426     Boolean isDirectory;
427     long dirID;
428     
429     err = FSpLocationFromPath(strlen(path), path, &fileSpec);
430     if (err != noErr) {
431         errno = TclMacOSErrorToPosixError(err);
432         return -1;
433     }
434     
435     /*
436      * Fill the fpb & vpb struct up with info about file or directory.
437      */
438      
439     FSpGetDirectoryID(&fileSpec, &dirID, &isDirectory);
440     vpb.ioVRefNum = fpb.ioVRefNum = fileSpec.vRefNum;
441     vpb.ioNamePtr = fpb.ioNamePtr = fileSpec.name;
442     if (isDirectory) {
443         fpb.ioDirID = fileSpec.parID;
444     } else {
445         fpb.ioDirID = dirID;
446     }
447
448     fpb.ioFDirIndex = 0;
449     err = PBGetCatInfoSync((CInfoPBPtr)&fpb);
450     if (err == noErr) {
451         vpb.ioVolIndex = 0;
452         err = PBHGetVInfoSync((HParmBlkPtr)&vpb);
453         if (err == noErr && buf != NULL) {
454             /* 
455              * Files are always readable by everyone.
456              */
457              
458             buf->st_mode = S_IRUSR | S_IRGRP | S_IROTH;
459
460             /* 
461              * Use the Volume Info & File Info to fill out stat buf.
462              */
463             if (fpb.ioFlAttrib & 0x10) {
464                 buf->st_mode |= S_IFDIR;
465                 buf->st_nlink = 2;
466             } else {
467                 buf->st_nlink = 1;
468                 if (fpb.ioFlFndrInfo.fdFlags & 0x8000) {
469                     buf->st_mode |= S_IFLNK;
470                 } else {
471                     buf->st_mode |= S_IFREG;
472                 }
473             }
474             if ((fpb.ioFlAttrib & 0x10) || (fpb.ioFlFndrInfo.fdType == 'APPL')) {
475                 /*
476                  * Directories and applications are executable by everyone.
477                  */
478                  
479                 buf->st_mode |= S_IXUSR | S_IXGRP | S_IXOTH;
480             }
481             if ((fpb.ioFlAttrib & 0x01) == 0){
482                 /* 
483                  * If not locked, then everyone has write acces.
484                  */
485                  
486                 buf->st_mode |= S_IWUSR | S_IWGRP | S_IWOTH;
487             }
488             buf->st_ino = fpb.ioDirID;
489             buf->st_dev = fpb.ioVRefNum;
490             buf->st_uid = -1;
491             buf->st_gid = -1;
492             buf->st_rdev = 0;
493             buf->st_size = fpb.ioFlLgLen;
494             buf->st_blksize = vpb.ioVAlBlkSiz;
495             buf->st_blocks = (buf->st_size + buf->st_blksize - 1)
496                 / buf->st_blksize;
497
498             /*
499              * The times returned by the Mac file system are in the
500              * local time zone.  We convert them to GMT so that the
501              * epoch starts from GMT.  This is also consistant with
502              * what is returned from "clock seconds".
503              */
504             if (initalized == false) {
505                 MachineLocation loc;
506     
507                 ReadLocation(&loc);
508                 gmt_offset = loc.u.gmtDelta & 0x00ffffff;
509                 if (gmt_offset & 0x00800000) {
510                     gmt_offset = gmt_offset | 0xff000000;
511                 }
512                 initalized = true;
513             }
514             buf->st_atime = buf->st_mtime = fpb.ioFlMdDat - gmt_offset;
515             buf->st_ctime = fpb.ioFlCrDat - gmt_offset;
516
517         }
518     }
519
520     if (err != noErr) {
521         errno = TclMacOSErrorToPosixError(err);
522     }
523     
524     return (err == noErr ? 0 : -1);
525 }
526 \f
527 /*
528  *----------------------------------------------------------------------
529  *
530  * TclMacReadlink --
531  *
532  *      This function replaces the library version of readlink.
533  *
534  * Results:
535  *      See readlink documentation.
536  *
537  * Side effects:
538  *      None.
539  *
540  *----------------------------------------------------------------------
541  */
542
543 int
544 TclMacReadlink(
545     char *path,
546     char *buf,
547     int size)
548 {
549     HFileInfo fpb;
550     OSErr err;
551     FSSpec fileSpec;
552     Boolean isDirectory;
553     Boolean wasAlias;
554     long dirID;
555     char fileName[256];
556     char *end;
557     Handle theString = NULL;
558     int pathSize;
559
560     /*
561      * Remove ending colons if they exist.
562      */
563     while ((strlen(path) != 0) && (path[strlen(path) - 1] == ':')) {
564         path[strlen(path) - 1] = NULL;
565     }
566
567     if (strchr(path, ':') == NULL) {
568         strcpy(fileName, path);
569         path = NULL;
570     } else {
571         end = strrchr(path, ':') + 1;
572         strcpy(fileName, end);
573         *end = NULL;
574     }
575     c2pstr(fileName);
576     
577     /*
578      * Create the file spec for the directory of the file
579      * we want to look at.
580      */
581     if (path != NULL) {
582         err = FSpLocationFromPath(strlen(path), path, &fileSpec);
583         if (err != noErr) {
584             errno = EINVAL;
585             return -1;
586         }
587     } else {
588         FSMakeFSSpecCompat(0, 0, NULL, &fileSpec);
589     }
590     
591     /*
592      * Fill the fpb struct up with info about file or directory.
593      */
594     FSpGetDirectoryID(&fileSpec, &dirID, &isDirectory);
595     fpb.ioVRefNum = fileSpec.vRefNum;
596     fpb.ioDirID = dirID;
597     fpb.ioNamePtr = (StringPtr) fileName;
598
599     fpb.ioFDirIndex = 0;
600     err = PBGetCatInfoSync((CInfoPBPtr)&fpb);
601     if (err != noErr) {
602         errno = TclMacOSErrorToPosixError(err);
603         return -1;
604     } else {
605         if (fpb.ioFlAttrib & 0x10) {
606             errno = EINVAL;
607             return -1;
608         } else {
609             if (fpb.ioFlFndrInfo.fdFlags & 0x8000) {
610                 /*
611                  * The file is a link!
612                  */
613             } else {
614                 errno = EINVAL;
615                 return -1;
616             }
617         }
618     }
619     
620     /*
621      * If we are here it's really a link - now find out
622      * where it points to.
623      */
624     err = FSMakeFSSpecCompat(fileSpec.vRefNum, dirID, (StringPtr) fileName, &fileSpec);
625     if (err == noErr) {
626         err = ResolveAliasFile(&fileSpec, true, &isDirectory, &wasAlias);
627     }
628     if ((err == fnfErr) || wasAlias) {
629         err = FSpPathFromLocation(&fileSpec, &pathSize, &theString);
630         if ((err != noErr) || (pathSize > size)) {
631             DisposeHandle(theString);
632             errno = ENAMETOOLONG;
633             return -1;
634         }
635     } else {
636         errno = EINVAL;
637         return -1;
638     }
639
640     strncpy(buf, *theString, pathSize);
641     DisposeHandle(theString);
642     
643     return pathSize;
644 }
645 \f
646 /*
647  *----------------------------------------------------------------------
648  *
649  * TclpAccess --
650  *
651  *      This function replaces the library version of access.  The
652  *      access function provided by most Mac compiliers is rather 
653  *      broken or incomplete.
654  *
655  * Results:
656  *      See access documentation.
657  *
658  * Side effects:
659  *      See access documentation.
660  *
661  *----------------------------------------------------------------------
662  */
663
664 int
665 TclpAccess(
666     const char *path,
667     int mode)
668 {
669     HFileInfo fpb;
670     HVolumeParam vpb;
671     OSErr err;
672     FSSpec fileSpec;
673     Boolean isDirectory;
674     long dirID;
675     int full_mode = 0;
676
677     err = FSpLocationFromPath(strlen(path), (char *) path, &fileSpec);
678     if (err != noErr) {
679         errno = TclMacOSErrorToPosixError(err);
680         return -1;
681     }
682     
683     /*
684      * Fill the fpb & vpb struct up with info about file or directory.
685      */
686     FSpGetDirectoryID(&fileSpec, &dirID, &isDirectory);
687     vpb.ioVRefNum = fpb.ioVRefNum = fileSpec.vRefNum;
688     vpb.ioNamePtr = fpb.ioNamePtr = fileSpec.name;
689     if (isDirectory) {
690         fpb.ioDirID = fileSpec.parID;
691     } else {
692         fpb.ioDirID = dirID;
693     }
694
695     fpb.ioFDirIndex = 0;
696     err = PBGetCatInfoSync((CInfoPBPtr)&fpb);
697     if (err == noErr) {
698         vpb.ioVolIndex = 0;
699         err = PBHGetVInfoSync((HParmBlkPtr)&vpb);
700         if (err == noErr) {
701             /* 
702              * Use the Volume Info & File Info to determine
703              * access information.  If we have got this far
704              * we know the directory is searchable or the file
705              * exists.  (We have F_OK)
706              */
707
708             /*
709              * Check to see if the volume is hardware or
710              * software locked.  If so we arn't W_OK.
711              */
712             if (mode & W_OK) {
713                 if ((vpb.ioVAtrb & 0x0080) || (vpb.ioVAtrb & 0x8000)) {
714                     errno = EROFS;
715                     return -1;
716                 }
717                 if (fpb.ioFlAttrib & 0x01) {
718                     errno = EACCES;
719                     return -1;
720                 }
721             }
722             
723             /*
724              * Directories are always searchable and executable.  But only 
725              * files of type 'APPL' are executable.
726              */
727             if (!(fpb.ioFlAttrib & 0x10) && (mode & X_OK)
728                 && (fpb.ioFlFndrInfo.fdType != 'APPL')) {
729                 return -1;
730             }
731         }
732     }
733
734     if (err != noErr) {
735         errno = TclMacOSErrorToPosixError(err);
736         return -1;
737     }
738     
739     return 0;
740 }
741 \f
742 /*
743  *----------------------------------------------------------------------
744  *
745  * TclMacFOpenHack --
746  *
747  *      This function replaces fopen.  It supports paths with alises.
748  *      Note, remember to undefine the fopen macro!
749  *
750  * Results:
751  *      See fopen documentation.
752  *
753  * Side effects:
754  *      See fopen documentation.
755  *
756  *----------------------------------------------------------------------
757  */
758
759 #undef fopen
760 FILE *
761 TclMacFOpenHack(
762     const char *path,
763     const char *mode)
764 {
765     OSErr err;
766     FSSpec fileSpec;
767     Handle pathString = NULL;
768     int size;
769     FILE * f;
770     
771     err = FSpLocationFromPath(strlen(path), (char *) path, &fileSpec);
772     if ((err != noErr) && (err != fnfErr)) {
773         return NULL;
774     }
775     err = FSpPathFromLocation(&fileSpec, &size, &pathString);
776     if ((err != noErr) && (err != fnfErr)) {
777         return NULL;
778     }
779     
780     HLock(pathString);
781     f = fopen(*pathString, mode);
782     HUnlock(pathString);
783     DisposeHandle(pathString);
784     return f;
785 }
786 \f
787 /*
788  *----------------------------------------------------------------------
789  *
790  * TclMacOSErrorToPosixError --
791  *
792  *      Given a Macintosh OSErr return the appropiate POSIX error.
793  *
794  * Results:
795  *      A Posix error.
796  *
797  * Side effects:
798  *      None.
799  *
800  *----------------------------------------------------------------------
801  */
802
803 int
804 TclMacOSErrorToPosixError(
805     int error)  /* A Macintosh error. */
806 {
807     switch (error) {
808         case noErr:
809             return 0;
810         case bdNamErr:
811             return ENAMETOOLONG;
812         case afpObjectTypeErr:
813             return ENOTDIR;
814         case fnfErr:
815         case dirNFErr:
816             return ENOENT;
817         case dupFNErr:
818             return EEXIST;
819         case dirFulErr:
820         case dskFulErr:
821             return ENOSPC;
822         case fBsyErr:
823             return EBUSY;
824         case tmfoErr:
825             return ENFILE;
826         case fLckdErr:
827         case permErr:
828         case afpAccessDenied:
829             return EACCES;
830         case wPrErr:
831         case vLckdErr:
832             return EROFS;
833         case badMovErr:
834             return EINVAL;
835         case diffVolErr:
836             return EXDEV;
837         default:
838             return EINVAL;
839     }
840 }