1 /*
2  * Implements the file command for jim
3  *
4  * (c) 2008 Steve Bennett <steveb@workware.net.au>
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  *
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above
13  *    copyright notice, this list of conditions and the following
14  *    disclaimer in the documentation and/or other materials
15  *    provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE JIM TCL PROJECT ``AS IS'' AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
19  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20  * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
21  * JIM TCL PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
28  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  *
30  * The views and conclusions contained in the software and documentation
31  * are those of the authors and should not be interpreted as representing
32  * official policies, either expressed or implied, of the Jim Tcl Project.
33  *
34  * Based on code originally from Tcl 6.7:
35  *
36  * Copyright 1987-1991 Regents of the University of California
37  * Permission to use, copy, modify, and distribute this
38  * software and its documentation for any purpose and without
39  * fee is hereby granted, provided that the above copyright
40  * notice appear in all copies.  The University of California
41  * makes no representations about the suitability of this
42  * software for any purpose.  It is provided "as is" without
43  * express or implied warranty.
44  */
45 
46 #include <limits.h>
47 #include <stdlib.h>
48 #include <string.h>
49 #include <stdio.h>
50 #include <errno.h>
51 
52 #include <jimautoconf.h>
53 #include <jim-subcmd.h>
54 #include <jimiocompat.h>
55 
56 #ifdef HAVE_UTIMES
57 #include <sys/time.h>
58 #endif
59 #ifdef HAVE_UNISTD_H
60 #include <unistd.h>
61 #elif defined(_MSC_VER)
62 #include <direct.h>
63 #define F_OK 0
64 #define W_OK 2
65 #define R_OK 4
66 #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
67 #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
68 #endif
69 
70 # ifndef MAXPATHLEN
71 # define MAXPATHLEN JIM_PATH_LEN
72 # endif
73 
74 #if defined(__MINGW32__) || defined(__MSYS__) || defined(_MSC_VER)
75 #define ISWINDOWS 1
76 #else
77 #define ISWINDOWS 0
78 #endif
79 
80 /* extract nanosecond resolution mtime from struct stat */
81 #if defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
82     #define STAT_MTIME_US(STAT) ((STAT).st_mtimespec.tv_sec * 1000000ll + (STAT).st_mtimespec.tv_nsec / 1000)
83 #elif defined(HAVE_STRUCT_STAT_ST_MTIM)
84     #define STAT_MTIME_US(STAT) ((STAT).st_mtim.tv_sec * 1000000ll + (STAT).st_mtim.tv_nsec / 1000)
85 #endif
86 
87 /*
88  *----------------------------------------------------------------------
89  *
90  * JimGetFileType --
91  *
92  *  Given a mode word, returns a string identifying the type of a
93  *  file.
94  *
95  * Results:
96  *  A static text string giving the file type from mode.
97  *
98  * Side effects:
99  *  None.
100  *
101  *----------------------------------------------------------------------
102  */
103 
JimGetFileType(int mode)104 static const char *JimGetFileType(int mode)
105 {
106     if (S_ISREG(mode)) {
107         return "file";
108     }
109     else if (S_ISDIR(mode)) {
110         return "directory";
111     }
112 #ifdef S_ISCHR
113     else if (S_ISCHR(mode)) {
114         return "characterSpecial";
115     }
116 #endif
117 #ifdef S_ISBLK
118     else if (S_ISBLK(mode)) {
119         return "blockSpecial";
120     }
121 #endif
122 #ifdef S_ISFIFO
123     else if (S_ISFIFO(mode)) {
124         return "fifo";
125     }
126 #endif
127 #ifdef S_ISLNK
128     else if (S_ISLNK(mode)) {
129         return "link";
130     }
131 #endif
132 #ifdef S_ISSOCK
133     else if (S_ISSOCK(mode)) {
134         return "socket";
135     }
136 #endif
137     return "unknown";
138 }
139 
140 /*
141  *----------------------------------------------------------------------
142  *
143  * StoreStatData --
144  *
145  *  This is a utility procedure that breaks out the fields of a
146  *  "stat" structure and stores them in textual form into the
147  *  elements of an associative array.
148  *
149  * Results:
150  *  Returns a standard Tcl return value.  If an error occurs then
151  *  a message is left in interp->result.
152  *
153  * Side effects:
154  *  Elements of the associative array given by "varName" are modified.
155  *
156  *----------------------------------------------------------------------
157  */
AppendStatElement(Jim_Interp * interp,Jim_Obj * listObj,const char * key,jim_wide value)158 static void AppendStatElement(Jim_Interp *interp, Jim_Obj *listObj, const char *key, jim_wide value)
159 {
160     Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, key, -1));
161     Jim_ListAppendElement(interp, listObj, Jim_NewIntObj(interp, value));
162 }
163 
StoreStatData(Jim_Interp * interp,Jim_Obj * varName,const jim_stat_t * sb)164 static int StoreStatData(Jim_Interp *interp, Jim_Obj *varName, const jim_stat_t *sb)
165 {
166     /* Just use a list to store the data */
167     Jim_Obj *listObj = Jim_NewListObj(interp, NULL, 0);
168 
169     AppendStatElement(interp, listObj, "dev", sb->st_dev);
170     AppendStatElement(interp, listObj, "ino", sb->st_ino);
171     AppendStatElement(interp, listObj, "mode", sb->st_mode);
172     AppendStatElement(interp, listObj, "nlink", sb->st_nlink);
173     AppendStatElement(interp, listObj, "uid", sb->st_uid);
174     AppendStatElement(interp, listObj, "gid", sb->st_gid);
175     AppendStatElement(interp, listObj, "size", sb->st_size);
176     AppendStatElement(interp, listObj, "atime", sb->st_atime);
177     AppendStatElement(interp, listObj, "mtime", sb->st_mtime);
178     AppendStatElement(interp, listObj, "ctime", sb->st_ctime);
179 #ifdef STAT_MTIME_US
180     AppendStatElement(interp, listObj, "mtimeus", STAT_MTIME_US(*sb));
181 #endif
182     Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, "type", -1));
183     Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, JimGetFileType((int)sb->st_mode), -1));
184 
185     /* Was a variable specified? */
186     if (varName) {
187         Jim_Obj *objPtr;
188         objPtr = Jim_GetVariable(interp, varName, JIM_NONE);
189 
190         if (objPtr) {
191             Jim_Obj *objv[2];
192 
193             objv[0] = objPtr;
194             objv[1] = listObj;
195 
196             objPtr = Jim_DictMerge(interp, 2, objv);
197             if (objPtr == NULL) {
198                 /* This message matches the one from Tcl */
199                 Jim_SetResultFormatted(interp, "can't set \"%#s(dev)\": variable isn't array", varName);
200                 Jim_FreeNewObj(interp, listObj);
201                 return JIM_ERR;
202             }
203 
204             Jim_InvalidateStringRep(objPtr);
205 
206             Jim_FreeNewObj(interp, listObj);
207             listObj = objPtr;
208         }
209         Jim_SetVariable(interp, varName, listObj);
210     }
211 
212     /* And also return the value */
213     Jim_SetResult(interp, listObj);
214 
215     return JIM_OK;
216 }
217 
218 /**
219  * Give a path of length 'len', returns the length of the path
220  * with any trailing slashes removed.
221  */
JimPathLenNoTrailingSlashes(const char * path,int len)222 static int JimPathLenNoTrailingSlashes(const char *path, int len)
223 {
224     int i;
225     for (i = len; i > 1 && path[i - 1] == '/'; i--) {
226         /* Trailing slash, so remove it */
227         if (ISWINDOWS && path[i - 2] == ':') {
228             /* But on windows, we won't remove the trailing slash from c:/ */
229             break;
230         }
231     }
232     return i;
233 }
234 
235 /**
236  * Give a path in objPtr, returns a new path with any trailing slash removed.
237  * Use Jim_DecrRefCount() on the returned object (which may be identical to objPtr).
238  */
JimStripTrailingSlashes(Jim_Interp * interp,Jim_Obj * objPtr)239 static Jim_Obj *JimStripTrailingSlashes(Jim_Interp *interp, Jim_Obj *objPtr)
240 {
241     int len = Jim_Length(objPtr);
242     const char *path = Jim_String(objPtr);
243     int i = JimPathLenNoTrailingSlashes(path, len);
244     if (i != len) {
245         objPtr = Jim_NewStringObj(interp, path, i);
246     }
247     Jim_IncrRefCount(objPtr);
248     return objPtr;
249 }
250 
file_cmd_dirname(Jim_Interp * interp,int argc,Jim_Obj * const * argv)251 static int file_cmd_dirname(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
252 {
253     Jim_Obj *objPtr = JimStripTrailingSlashes(interp, argv[0]);
254     const char *path = Jim_String(objPtr);
255     const char *p = strrchr(path, '/');
256 
257     if (!p) {
258         Jim_SetResultString(interp, ".", -1);
259     }
260     else if (p[1] == 0) {
261         /* Trailing slash so do nothing */
262         Jim_SetResult(interp, objPtr);
263     }
264     else if (p == path) {
265         Jim_SetResultString(interp, "/", -1);
266     }
267     else if (ISWINDOWS && p[-1] == ':') {
268         /* z:/dir => z:/ */
269         Jim_SetResultString(interp, path, p - path + 1);
270     }
271     else {
272         /* Strip any trailing slashes from the result */
273         int len = JimPathLenNoTrailingSlashes(path, p - path);
274         Jim_SetResultString(interp, path, len);
275     }
276     Jim_DecrRefCount(interp, objPtr);
277     return JIM_OK;
278 }
279 
file_cmd_split(Jim_Interp * interp,int argc,Jim_Obj * const * argv)280 static int file_cmd_split(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
281 {
282     Jim_Obj *listObj = Jim_NewListObj(interp, NULL, 0);
283     const char *path = Jim_String(argv[0]);
284 
285     if (*path == '/') {
286         Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, "/", 1));
287     }
288 
289     while (1) {
290         /* Remove leading slashes */
291         while (*path == '/') {
292             path++;
293         }
294         if (*path) {
295             const char *pt = strchr(path, '/');
296             if (pt) {
297                 Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, path, pt - path));
298                 path = pt;
299                 continue;
300             }
301             Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, path, -1));
302         }
303         break;
304     }
305     Jim_SetResult(interp, listObj);
306     return JIM_OK;
307 }
308 
file_cmd_rootname(Jim_Interp * interp,int argc,Jim_Obj * const * argv)309 static int file_cmd_rootname(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
310 {
311     const char *path = Jim_String(argv[0]);
312     const char *lastSlash = strrchr(path, '/');
313     const char *p = strrchr(path, '.');
314 
315     if (p == NULL || (lastSlash != NULL && lastSlash > p)) {
316         Jim_SetResult(interp, argv[0]);
317     }
318     else {
319         Jim_SetResultString(interp, path, p - path);
320     }
321     return JIM_OK;
322 }
323 
file_cmd_extension(Jim_Interp * interp,int argc,Jim_Obj * const * argv)324 static int file_cmd_extension(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
325 {
326     Jim_Obj *objPtr = JimStripTrailingSlashes(interp, argv[0]);
327     const char *path = Jim_String(objPtr);
328     const char *lastSlash = strrchr(path, '/');
329     const char *p = strrchr(path, '.');
330 
331     if (p == NULL || (lastSlash != NULL && lastSlash >= p)) {
332         p = "";
333     }
334     Jim_SetResultString(interp, p, -1);
335     Jim_DecrRefCount(interp, objPtr);
336     return JIM_OK;
337 }
338 
file_cmd_tail(Jim_Interp * interp,int argc,Jim_Obj * const * argv)339 static int file_cmd_tail(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
340 {
341     Jim_Obj *objPtr = JimStripTrailingSlashes(interp, argv[0]);
342     const char *path = Jim_String(objPtr);
343     const char *lastSlash = strrchr(path, '/');
344 
345     if (lastSlash) {
346         Jim_SetResultString(interp, lastSlash + 1, -1);
347     }
348     else {
349         Jim_SetResult(interp, objPtr);
350     }
351     Jim_DecrRefCount(interp, objPtr);
352     return JIM_OK;
353 }
354 
file_cmd_normalize(Jim_Interp * interp,int argc,Jim_Obj * const * argv)355 static int file_cmd_normalize(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
356 {
357 #ifdef HAVE_REALPATH
358     const char *path = Jim_String(argv[0]);
359     char *newname = Jim_Alloc(MAXPATHLEN + 1);
360 
361     if (realpath(path, newname)) {
362         Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, newname, -1));
363         return JIM_OK;
364     }
365     else {
366         Jim_Free(newname);
367         Jim_SetResultFormatted(interp, "can't normalize \"%#s\": %s", argv[0], strerror(errno));
368         return JIM_ERR;
369     }
370 #else
371     Jim_SetResultString(interp, "Not implemented", -1);
372     return JIM_ERR;
373 #endif
374 }
375 
file_cmd_join(Jim_Interp * interp,int argc,Jim_Obj * const * argv)376 static int file_cmd_join(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
377 {
378     int i;
379     char *newname = Jim_Alloc(MAXPATHLEN + 1);
380     char *last = newname;
381 
382     *newname = 0;
383 
384     /* Simple implementation for now */
385     for (i = 0; i < argc; i++) {
386         int len;
387         const char *part = Jim_GetString(argv[i], &len);
388 
389         if (*part == '/') {
390             /* Absolute component, so go back to the start */
391             last = newname;
392         }
393         else if (ISWINDOWS && strchr(part, ':')) {
394             /* Absolute component on mingw, so go back to the start */
395             last = newname;
396         }
397         else if (part[0] == '.') {
398             if (part[1] == '/') {
399                 part += 2;
400                 len -= 2;
401             }
402             else if (part[1] == 0 && last != newname) {
403                 /* Adding '.' to an existing path does nothing */
404                 continue;
405             }
406         }
407 
408         /* Add a slash if needed */
409         if (last != newname && last[-1] != '/') {
410             *last++ = '/';
411         }
412 
413         if (len) {
414             if (last + len - newname >= MAXPATHLEN) {
415                 Jim_Free(newname);
416                 Jim_SetResultString(interp, "Path too long", -1);
417                 return JIM_ERR;
418             }
419             memcpy(last, part, len);
420             last += len;
421         }
422 
423         /* Remove a slash if needed */
424         if (last > newname + 1 && last[-1] == '/') {
425             /* but on on Windows, leave the trailing slash on "c:/ " */
426             if (!ISWINDOWS || !(last > newname + 2 && last[-2] == ':')) {
427                 *--last = 0;
428             }
429         }
430     }
431 
432     *last = 0;
433 
434     /* Probably need to handle some special cases ... */
435 
436     Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, newname, last - newname));
437 
438     return JIM_OK;
439 }
440 
file_access(Jim_Interp * interp,Jim_Obj * filename,int mode)441 static int file_access(Jim_Interp *interp, Jim_Obj *filename, int mode)
442 {
443     Jim_SetResultBool(interp, access(Jim_String(filename), mode) != -1);
444 
445     return JIM_OK;
446 }
447 
file_cmd_readable(Jim_Interp * interp,int argc,Jim_Obj * const * argv)448 static int file_cmd_readable(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
449 {
450     return file_access(interp, argv[0], R_OK);
451 }
452 
file_cmd_writable(Jim_Interp * interp,int argc,Jim_Obj * const * argv)453 static int file_cmd_writable(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
454 {
455     return file_access(interp, argv[0], W_OK);
456 }
457 
file_cmd_executable(Jim_Interp * interp,int argc,Jim_Obj * const * argv)458 static int file_cmd_executable(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
459 {
460 #ifdef X_OK
461     return file_access(interp, argv[0], X_OK);
462 #else
463     /* If no X_OK, just assume true. */
464     Jim_SetResultBool(interp, 1);
465     return JIM_OK;
466 #endif
467 }
468 
file_cmd_exists(Jim_Interp * interp,int argc,Jim_Obj * const * argv)469 static int file_cmd_exists(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
470 {
471     return file_access(interp, argv[0], F_OK);
472 }
473 
file_cmd_delete(Jim_Interp * interp,int argc,Jim_Obj * const * argv)474 static int file_cmd_delete(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
475 {
476     int force = Jim_CompareStringImmediate(interp, argv[0], "-force");
477 
478     if (force || Jim_CompareStringImmediate(interp, argv[0], "--")) {
479         argc++;
480         argv--;
481     }
482 
483     while (argc--) {
484         const char *path = Jim_String(argv[0]);
485 
486         if (unlink(path) == -1 && errno != ENOENT) {
487             if (rmdir(path) == -1) {
488                 /* Maybe try using the script helper */
489                 if (!force || Jim_EvalPrefix(interp, "file delete force", 1, argv) != JIM_OK) {
490                     Jim_SetResultFormatted(interp, "couldn't delete file \"%s\": %s", path,
491                         strerror(errno));
492                     return JIM_ERR;
493                 }
494             }
495         }
496         argv++;
497     }
498     return JIM_OK;
499 }
500 
501 #ifdef HAVE_MKDIR_ONE_ARG
502 #define MKDIR_DEFAULT(PATHNAME) mkdir(PATHNAME)
503 #else
504 #define MKDIR_DEFAULT(PATHNAME) mkdir(PATHNAME, 0755)
505 #endif
506 
507 /**
508  * Create directory, creating all intermediate paths if necessary.
509  *
510  * Returns 0 if OK or -1 on failure (and sets errno)
511  *
512  * Note: The path may be modified.
513  */
mkdir_all(char * path)514 static int mkdir_all(char *path)
515 {
516     int ok = 1;
517 
518     /* First time just try to make the dir */
519     goto first;
520 
521     while (ok--) {
522         /* Must have failed the first time, so recursively make the parent and try again */
523         {
524             char *slash = strrchr(path, '/');
525 
526             if (slash && slash != path) {
527                 *slash = 0;
528                 if (mkdir_all(path) != 0) {
529                     return -1;
530                 }
531                 *slash = '/';
532             }
533         }
534       first:
535         if (MKDIR_DEFAULT(path) == 0) {
536             return 0;
537         }
538         if (errno == ENOENT) {
539             /* Create the parent and try again */
540             continue;
541         }
542         /* Maybe it already exists as a directory */
543         if (errno == EEXIST) {
544             jim_stat_t sb;
545 
546             if (Jim_Stat(path, &sb) == 0 && S_ISDIR(sb.st_mode)) {
547                 return 0;
548             }
549             /* Restore errno */
550             errno = EEXIST;
551         }
552         /* Failed */
553         break;
554     }
555     return -1;
556 }
557 
file_cmd_mkdir(Jim_Interp * interp,int argc,Jim_Obj * const * argv)558 static int file_cmd_mkdir(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
559 {
560     while (argc--) {
561         char *path = Jim_StrDup(Jim_String(argv[0]));
562         int rc = mkdir_all(path);
563 
564         Jim_Free(path);
565         if (rc != 0) {
566             Jim_SetResultFormatted(interp, "can't create directory \"%#s\": %s", argv[0],
567                 strerror(errno));
568             return JIM_ERR;
569         }
570         argv++;
571     }
572     return JIM_OK;
573 }
574 
file_cmd_tempfile(Jim_Interp * interp,int argc,Jim_Obj * const * argv)575 static int file_cmd_tempfile(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
576 {
577     int fd = Jim_MakeTempFile(interp, (argc >= 1) ? Jim_String(argv[0]) : NULL, 0);
578 
579     if (fd < 0) {
580         return JIM_ERR;
581     }
582     close(fd);
583 
584     return JIM_OK;
585 }
586 
file_cmd_rename(Jim_Interp * interp,int argc,Jim_Obj * const * argv)587 static int file_cmd_rename(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
588 {
589     const char *source;
590     const char *dest;
591     int force = 0;
592 
593     if (argc == 3) {
594         if (!Jim_CompareStringImmediate(interp, argv[0], "-force")) {
595             return -1;
596         }
597         force++;
598         argv++;
599         argc--;
600     }
601 
602     source = Jim_String(argv[0]);
603     dest = Jim_String(argv[1]);
604 
605     if (!force && access(dest, F_OK) == 0) {
606         Jim_SetResultFormatted(interp, "error renaming \"%#s\" to \"%#s\": target exists", argv[0],
607             argv[1]);
608         return JIM_ERR;
609     }
610 #if ISWINDOWS
611     if (access(dest, F_OK) == 0) {
612         /* Windows won't rename over an existing file */
613         remove(dest);
614     }
615 #endif
616     if (rename(source, dest) != 0) {
617         Jim_SetResultFormatted(interp, "error renaming \"%#s\" to \"%#s\": %s", argv[0], argv[1],
618             strerror(errno));
619         return JIM_ERR;
620     }
621 
622     return JIM_OK;
623 }
624 
625 #if defined(HAVE_LINK) && defined(HAVE_SYMLINK)
file_cmd_link(Jim_Interp * interp,int argc,Jim_Obj * const * argv)626 static int file_cmd_link(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
627 {
628     int ret;
629     const char *source;
630     const char *dest;
631     static const char * const options[] = { "-hard", "-symbolic", NULL };
632     enum { OPT_HARD, OPT_SYMBOLIC, };
633     int option = OPT_HARD;
634 
635     if (argc == 3) {
636         if (Jim_GetEnum(interp, argv[0], options, &option, NULL, JIM_ENUM_ABBREV | JIM_ERRMSG) != JIM_OK) {
637             return JIM_ERR;
638         }
639         argv++;
640         argc--;
641     }
642 
643     dest = Jim_String(argv[0]);
644     source = Jim_String(argv[1]);
645 
646     if (option == OPT_HARD) {
647         ret = link(source, dest);
648     }
649     else {
650         ret = symlink(source, dest);
651     }
652 
653     if (ret != 0) {
654         Jim_SetResultFormatted(interp, "error linking \"%#s\" to \"%#s\": %s", argv[0], argv[1],
655             strerror(errno));
656         return JIM_ERR;
657     }
658 
659     return JIM_OK;
660 }
661 #endif
662 
file_stat(Jim_Interp * interp,Jim_Obj * filename,jim_stat_t * sb)663 static int file_stat(Jim_Interp *interp, Jim_Obj *filename, jim_stat_t *sb)
664 {
665     const char *path = Jim_String(filename);
666 
667     if (Jim_Stat(path, sb) == -1) {
668         Jim_SetResultFormatted(interp, "could not read \"%#s\": %s", filename, strerror(errno));
669         return JIM_ERR;
670     }
671     return JIM_OK;
672 }
673 
674 #ifdef HAVE_LSTAT
file_lstat(Jim_Interp * interp,Jim_Obj * filename,jim_stat_t * sb)675 static int file_lstat(Jim_Interp *interp, Jim_Obj *filename, jim_stat_t *sb)
676 {
677     const char *path = Jim_String(filename);
678 
679     if (lstat(path, sb) == -1) {
680         Jim_SetResultFormatted(interp, "could not read \"%#s\": %s", filename, strerror(errno));
681         return JIM_ERR;
682     }
683     return JIM_OK;
684 }
685 #else
686 #define file_lstat file_stat
687 #endif
688 
file_cmd_atime(Jim_Interp * interp,int argc,Jim_Obj * const * argv)689 static int file_cmd_atime(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
690 {
691     jim_stat_t sb;
692 
693     if (file_stat(interp, argv[0], &sb) != JIM_OK) {
694         return JIM_ERR;
695     }
696     Jim_SetResultInt(interp, sb.st_atime);
697     return JIM_OK;
698 }
699 
700 /**
701  * Set file atime/mtime to the given time in microseconds since the epoch.
702  */
JimSetFileTimes(Jim_Interp * interp,const char * filename,jim_wide us)703 static int JimSetFileTimes(Jim_Interp *interp, const char *filename, jim_wide us)
704 {
705 #ifdef HAVE_UTIMES
706     struct timeval times[2];
707 
708     times[1].tv_sec = times[0].tv_sec = us / 1000000;
709     times[1].tv_usec = times[0].tv_usec = us % 1000000;
710 
711     if (utimes(filename, times) != 0) {
712         Jim_SetResultFormatted(interp, "can't set time on \"%s\": %s", filename, strerror(errno));
713         return JIM_ERR;
714     }
715     return JIM_OK;
716 #else
717     Jim_SetResultString(interp, "Not implemented", -1);
718     return JIM_ERR;
719 #endif
720 }
721 
file_cmd_mtime(Jim_Interp * interp,int argc,Jim_Obj * const * argv)722 static int file_cmd_mtime(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
723 {
724     jim_stat_t sb;
725 
726     if (argc == 2) {
727         jim_wide secs;
728         if (Jim_GetWide(interp, argv[1], &secs) != JIM_OK) {
729             return JIM_ERR;
730         }
731         return JimSetFileTimes(interp, Jim_String(argv[0]), secs * 1000000);
732     }
733     if (file_stat(interp, argv[0], &sb) != JIM_OK) {
734         return JIM_ERR;
735     }
736     Jim_SetResultInt(interp, sb.st_mtime);
737     return JIM_OK;
738 }
739 
740 #ifdef STAT_MTIME_US
file_cmd_mtimeus(Jim_Interp * interp,int argc,Jim_Obj * const * argv)741 static int file_cmd_mtimeus(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
742 {
743     jim_stat_t sb;
744 
745     if (argc == 2) {
746         jim_wide us;
747         if (Jim_GetWide(interp, argv[1], &us) != JIM_OK) {
748             return JIM_ERR;
749         }
750         return JimSetFileTimes(interp, Jim_String(argv[0]), us);
751     }
752     if (file_stat(interp, argv[0], &sb) != JIM_OK) {
753         return JIM_ERR;
754     }
755     Jim_SetResultInt(interp, STAT_MTIME_US(sb));
756     return JIM_OK;
757 }
758 #endif
759 
file_cmd_copy(Jim_Interp * interp,int argc,Jim_Obj * const * argv)760 static int file_cmd_copy(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
761 {
762     return Jim_EvalPrefix(interp, "file copy", argc, argv);
763 }
764 
file_cmd_size(Jim_Interp * interp,int argc,Jim_Obj * const * argv)765 static int file_cmd_size(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
766 {
767     jim_stat_t sb;
768 
769     if (file_stat(interp, argv[0], &sb) != JIM_OK) {
770         return JIM_ERR;
771     }
772     Jim_SetResultInt(interp, sb.st_size);
773     return JIM_OK;
774 }
775 
file_cmd_isdirectory(Jim_Interp * interp,int argc,Jim_Obj * const * argv)776 static int file_cmd_isdirectory(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
777 {
778     jim_stat_t sb;
779     int ret = 0;
780 
781     if (file_stat(interp, argv[0], &sb) == JIM_OK) {
782         ret = S_ISDIR(sb.st_mode);
783     }
784     Jim_SetResultInt(interp, ret);
785     return JIM_OK;
786 }
787 
file_cmd_isfile(Jim_Interp * interp,int argc,Jim_Obj * const * argv)788 static int file_cmd_isfile(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
789 {
790     jim_stat_t sb;
791     int ret = 0;
792 
793     if (file_stat(interp, argv[0], &sb) == JIM_OK) {
794         ret = S_ISREG(sb.st_mode);
795     }
796     Jim_SetResultInt(interp, ret);
797     return JIM_OK;
798 }
799 
800 #ifdef HAVE_GETEUID
file_cmd_owned(Jim_Interp * interp,int argc,Jim_Obj * const * argv)801 static int file_cmd_owned(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
802 {
803     jim_stat_t sb;
804     int ret = 0;
805 
806     if (file_stat(interp, argv[0], &sb) == JIM_OK) {
807         ret = (geteuid() == sb.st_uid);
808     }
809     Jim_SetResultInt(interp, ret);
810     return JIM_OK;
811 }
812 #endif
813 
814 #if defined(HAVE_READLINK)
file_cmd_readlink(Jim_Interp * interp,int argc,Jim_Obj * const * argv)815 static int file_cmd_readlink(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
816 {
817     const char *path = Jim_String(argv[0]);
818     char *linkValue = Jim_Alloc(MAXPATHLEN + 1);
819 
820     int linkLength = readlink(path, linkValue, MAXPATHLEN);
821 
822     if (linkLength == -1) {
823         Jim_Free(linkValue);
824         Jim_SetResultFormatted(interp, "could not read link \"%#s\": %s", argv[0], strerror(errno));
825         return JIM_ERR;
826     }
827     linkValue[linkLength] = 0;
828     Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, linkValue, linkLength));
829     return JIM_OK;
830 }
831 #endif
832 
file_cmd_type(Jim_Interp * interp,int argc,Jim_Obj * const * argv)833 static int file_cmd_type(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
834 {
835     jim_stat_t sb;
836 
837     if (file_lstat(interp, argv[0], &sb) != JIM_OK) {
838         return JIM_ERR;
839     }
840     Jim_SetResultString(interp, JimGetFileType((int)sb.st_mode), -1);
841     return JIM_OK;
842 }
843 
844 #ifdef HAVE_LSTAT
file_cmd_lstat(Jim_Interp * interp,int argc,Jim_Obj * const * argv)845 static int file_cmd_lstat(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
846 {
847     jim_stat_t sb;
848 
849     if (file_lstat(interp, argv[0], &sb) != JIM_OK) {
850         return JIM_ERR;
851     }
852     return StoreStatData(interp, argc == 2 ? argv[1] : NULL, &sb);
853 }
854 #else
855 #define file_cmd_lstat file_cmd_stat
856 #endif
857 
file_cmd_stat(Jim_Interp * interp,int argc,Jim_Obj * const * argv)858 static int file_cmd_stat(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
859 {
860     jim_stat_t sb;
861 
862     if (file_stat(interp, argv[0], &sb) != JIM_OK) {
863         return JIM_ERR;
864     }
865     return StoreStatData(interp, argc == 2 ? argv[1] : NULL, &sb);
866 }
867 
868 static const jim_subcmd_type file_command_table[] = {
869     {   "atime",
870         "name",
871         file_cmd_atime,
872         1,
873         1,
874         /* Description: Last access time */
875     },
876     {   "mtime",
877         "name ?time?",
878         file_cmd_mtime,
879         1,
880         2,
881         /* Description: Get or set last modification time */
882     },
883 #ifdef STAT_MTIME_US
884     {   "mtimeus",
885         "name ?time?",
886         file_cmd_mtimeus,
887         1,
888         2,
889         /* Description: Get or set last modification time in microseconds */
890     },
891 #endif
892     {   "copy",
893         "?-force? source dest",
894         file_cmd_copy,
895         2,
896         3,
897         /* Description: Copy source file to destination file */
898     },
899     {   "dirname",
900         "name",
901         file_cmd_dirname,
902         1,
903         1,
904         /* Description: Directory part of the name */
905     },
906     {   "rootname",
907         "name",
908         file_cmd_rootname,
909         1,
910         1,
911         /* Description: Name without any extension */
912     },
913     {   "extension",
914         "name",
915         file_cmd_extension,
916         1,
917         1,
918         /* Description: Last extension including the dot */
919     },
920     {   "tail",
921         "name",
922         file_cmd_tail,
923         1,
924         1,
925         /* Description: Last component of the name */
926     },
927     {   "split",
928         "name",
929         file_cmd_split,
930         1,
931         1,
932         /* Description: Split path into components as a list */
933     },
934     {   "normalize",
935         "name",
936         file_cmd_normalize,
937         1,
938         1,
939         /* Description: Normalized path of name */
940     },
941     {   "join",
942         "name ?name ...?",
943         file_cmd_join,
944         1,
945         -1,
946         /* Description: Join multiple path components */
947     },
948     {   "readable",
949         "name",
950         file_cmd_readable,
951         1,
952         1,
953         /* Description: Is file readable */
954     },
955     {   "writable",
956         "name",
957         file_cmd_writable,
958         1,
959         1,
960         /* Description: Is file writable */
961     },
962     {   "executable",
963         "name",
964         file_cmd_executable,
965         1,
966         1,
967         /* Description: Is file executable */
968     },
969     {   "exists",
970         "name",
971         file_cmd_exists,
972         1,
973         1,
974         /* Description: Does file exist */
975     },
976     {   "delete",
977         "?-force|--? name ...",
978         file_cmd_delete,
979         1,
980         -1,
981         /* Description: Deletes the files or directories (must be empty unless -force) */
982     },
983     {   "mkdir",
984         "dir ...",
985         file_cmd_mkdir,
986         1,
987         -1,
988         /* Description: Creates the directories */
989     },
990     {   "tempfile",
991         "?template?",
992         file_cmd_tempfile,
993         0,
994         1,
995         /* Description: Creates a temporary filename */
996     },
997     {   "rename",
998         "?-force? source dest",
999         file_cmd_rename,
1000         2,
1001         3,
1002         /* Description: Renames a file */
1003     },
1004 #if defined(HAVE_LINK) && defined(HAVE_SYMLINK)
1005     {   "link",
1006         "?-symbolic|-hard? newname target",
1007         file_cmd_link,
1008         2,
1009         3,
1010         /* Description: Creates a hard or soft link */
1011     },
1012 #endif
1013 #if defined(HAVE_READLINK)
1014     {   "readlink",
1015         "name",
1016         file_cmd_readlink,
1017         1,
1018         1,
1019         /* Description: Value of the symbolic link */
1020     },
1021 #endif
1022     {   "size",
1023         "name",
1024         file_cmd_size,
1025         1,
1026         1,
1027         /* Description: Size of file */
1028     },
1029     {   "stat",
1030         "name ?var?",
1031         file_cmd_stat,
1032         1,
1033         2,
1034         /* Description: Returns results of stat, and may store in var array */
1035     },
1036     {   "lstat",
1037         "name ?var?",
1038         file_cmd_lstat,
1039         1,
1040         2,
1041         /* Description: Returns results of lstat, and may store in var array */
1042     },
1043     {   "type",
1044         "name",
1045         file_cmd_type,
1046         1,
1047         1,
1048         /* Description: Returns type of the file */
1049     },
1050 #ifdef HAVE_GETEUID
1051     {   "owned",
1052         "name",
1053         file_cmd_owned,
1054         1,
1055         1,
1056         /* Description: Returns 1 if owned by the current owner */
1057     },
1058 #endif
1059     {   "isdirectory",
1060         "name",
1061         file_cmd_isdirectory,
1062         1,
1063         1,
1064         /* Description: Returns 1 if name is a directory */
1065     },
1066     {   "isfile",
1067         "name",
1068         file_cmd_isfile,
1069         1,
1070         1,
1071         /* Description: Returns 1 if name is a file */
1072     },
1073     {
1074         NULL
1075     }
1076 };
1077 
Jim_CdCmd(Jim_Interp * interp,int argc,Jim_Obj * const * argv)1078 static int Jim_CdCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
1079 {
1080     const char *path;
1081 
1082     if (argc != 2) {
1083         Jim_WrongNumArgs(interp, 1, argv, "dirname");
1084         return JIM_ERR;
1085     }
1086 
1087     path = Jim_String(argv[1]);
1088 
1089     if (chdir(path) != 0) {
1090         Jim_SetResultFormatted(interp, "couldn't change working directory to \"%s\": %s", path,
1091             strerror(errno));
1092         return JIM_ERR;
1093     }
1094     return JIM_OK;
1095 }
1096 
Jim_PwdCmd(Jim_Interp * interp,int argc,Jim_Obj * const * argv)1097 static int Jim_PwdCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
1098 {
1099     char *cwd = Jim_Alloc(MAXPATHLEN);
1100 
1101     if (getcwd(cwd, MAXPATHLEN) == NULL) {
1102         Jim_SetResultString(interp, "Failed to get pwd", -1);
1103         Jim_Free(cwd);
1104         return JIM_ERR;
1105     }
1106     else if (ISWINDOWS) {
1107         /* Try to keep backslashes out of paths */
1108         char *p = cwd;
1109         while ((p = strchr(p, '\\')) != NULL) {
1110             *p++ = '/';
1111         }
1112     }
1113 
1114     Jim_SetResultString(interp, cwd, -1);
1115 
1116     Jim_Free(cwd);
1117     return JIM_OK;
1118 }
1119 
Jim_fileInit(Jim_Interp * interp)1120 int Jim_fileInit(Jim_Interp *interp)
1121 {
1122     Jim_PackageProvideCheck(interp, "file");
1123     Jim_CreateCommand(interp, "file", Jim_SubCmdProc, (void *)file_command_table, NULL);
1124     Jim_CreateCommand(interp, "pwd", Jim_PwdCmd, NULL, NULL);
1125     Jim_CreateCommand(interp, "cd", Jim_CdCmd, NULL, NULL);
1126     return JIM_OK;
1127 }
1128