1 /*******************************************************************************
2 3 A more direct route to the file-system than FilePath. Use this
4 if you don't need path editing features. For example, if all you
5 want is to check some path exists, using this module would likely
6 be more convenient than FilePath:
7 ---
8 if (exists ("some/file/path"))
9 ...
10 ---
11 12 These functions may be less efficient than FilePath because they
13 generally attach a null to the filename for each underlying O/S
14 call. Use Path when you need pedestrian access to the file-system,
15 and are not manipulating the path components. Use FilePath where
16 path-editing or mutation is desired.
17 18 We encourage the use of "named import" with this module, such as:
19 ---
20 import Path = ocean.io.Path;
21 22 if (Path.exists ("some/file/path"))
23 ...
24 ---
25 26 Also residing here is a lightweight path-parser, which splits a
27 filepath into constituent components. FilePath is based upon the
28 same PathParser:
29 ---
30 auto p = Path.parse ("some/file/path");
31 auto path = p.path;
32 auto name = p.name;
33 auto suffix = p.suffix;
34 ...
35 ---
36 37 Path normalization and pattern-matching is also hosted here via
38 the normalize() and pattern() functions. See the doc towards the
39 end of this module.
40 41 Copyright:
42 Copyright (c) 2008 Kris Bell.
43 Normalization & Patterns copyright (c) 2006-2009 Max Samukha,
44 Thomas Kühne, Grzegorz Adam Hankiewicz
45 Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH.
46 All rights reserved.
47 48 License:
49 Tango Dual License: 3-Clause BSD License / Academic Free License v3.0.
50 See LICENSE_TANGO.txt for details.
51 52 Version:
53 Mar 2008: Initial version$(BR)
54 Oct 2009: Added PathUtil code
55 56 *******************************************************************************/57 58 moduleocean.io.Path;
59 60 publicimportocean.core.ExceptionDefinitions : IOException, IllegalArgumentException;
61 importocean.core.Verify;
62 importocean.io.model.IFile : FileConst, FileInfo;
63 importocean.sys.Common;
64 publicimportocean.time.Time : Time, TimeSpan;
65 importocean.transition;
66 67 importcore.stdc..string : memmove;
68 69 version (unittest) importocean.core.Test;
70 71 importcore.stdc.stdio;
72 importcore.stdc..string;
73 importcore.sys.posix.utime;
74 importcore.sys.posix.dirent;
75 76 77 /*******************************************************************************
78 79 Wraps the O/S specific calls with a D API. Note that these accept
80 null-terminated strings only, which is why it's not public. We need
81 this declared first to avoid forward-reference issues.
82 83 *******************************************************************************/84 85 packagestructFS86 {
87 /***********************************************************************
88 89 TimeStamp information. Accurate to whatever the F/S supports.
90 91 ***********************************************************************/92 93 structStamps94 {
95 Timecreated; /// Time created.96 Timeaccessed; /// Last time accessed.97 Timemodified; /// Last time modified.98 }
99 100 /***********************************************************************
101 102 Some fruct glue for directory listings.
103 104 ***********************************************************************/105 106 structListing107 {
108 cstringfolder;
109 boolallFiles;
110 111 intopApply (scopeintdelegate(refFileInfo) dg)
112 {
113 char[256] tmp = void;
114 autopath = strz (folder, tmp);
115 116 returnlist (path, dg, allFiles);
117 }
118 }
119 120 /***********************************************************************
121 122 Throw an exception using the last known error.
123 124 ***********************************************************************/125 126 staticvoidexception (cstringfilename)
127 {
128 exception (filename[0..$-1] ~ ": ", SysError.lastMsg);
129 }
130 131 /***********************************************************************
132 133 Throw an IO exception.
134 135 ***********************************************************************/136 137 staticvoidexception (cstringprefix, cstringerror)
138 {
139 thrownewIOException (idup(prefix ~ error));
140 }
141 142 /***********************************************************************
143 144 Return an adjusted path such that non-empty instances always
145 have a trailing separator.
146 147 Note: Allocates memory where path is not already terminated.
148 149 ***********************************************************************/150 151 staticcstringpadded (cstringpath, charc = '/')
152 {
153 if (path.length && path[$-1] != c)
154 path = path ~ c;
155 returnpath;
156 }
157 158 /***********************************************************************
159 160 Return an adjusted path such that non-empty instances always
161 have a leading separator.
162 163 Note: Allocates memory where path is not already terminated.
164 165 ***********************************************************************/166 167 staticcstringpaddedLeading (cstringpath, charc = '/')
168 {
169 if (path.length && path[0] != c)
170 path = c ~ path;
171 returnpath;
172 }
173 174 /***********************************************************************
175 176 Return an adjusted path such that non-empty instances do not
177 have a trailing separator.
178 179 ***********************************************************************/180 181 staticcstringstripped (cstringpath, charc = '/')
182 {
183 if (path.length && path[$-1] isc)
184 path = path [0 .. $-1];
185 returnpath;
186 }
187 188 /***********************************************************************
189 190 Join a set of path specs together. A path separator is
191 potentially inserted between each of the segments.
192 193 Note: Allocates memory.
194 195 ***********************************************************************/196 197 staticmstringjoin (const(char[])[] paths...)
198 {
199 mstringresult;
200 201 if (paths.length)
202 {
203 result ~= stripped(paths[0]);
204 205 foreach (path; paths[1 .. $-1])
206 result ~= paddedLeading (stripped(path));
207 208 result ~= paddedLeading(paths[$-1]);
209 210 returnresult;
211 }
212 213 returnnull;
214 }
215 216 /***********************************************************************
217 218 Append a terminating null onto a string, cheaply where
219 feasible.
220 221 Note: Allocates memory where the dst is too small.
222 223 ***********************************************************************/224 225 staticmstringstrz (cstringsrc, mstringdst)
226 {
227 autoi = src.length + 1;
228 if (dst.length < i)
229 dst.length = i;
230 dst [0 .. i-1] = src;
231 dst[i-1] = 0;
232 returndst [0 .. i];
233 }
234 235 /***************************************************************************
236 237 Get info about this path.
238 239 ***************************************************************************/240 241 privatestaticuintgetInfo (cstringname, refstat_tstats)
242 {
243 if (posix.stat (name.ptr, &stats))
244 exception (name);
245 246 returnstats.st_mode;
247 }
248 249 /***************************************************************************
250 251 Returns:
252 whether the file or path exists.
253 254 ***************************************************************************/255 256 staticboolexists (cstringname)
257 {
258 stat_tstats = void;
259 returnposix.stat (name.ptr, &stats) is0;
260 }
261 262 /***************************************************************************
263 264 Returns:
265 the file length (in bytes.)
266 267 ***************************************************************************/268 269 staticulongfileSize (cstringname)
270 {
271 stat_tstats = void;
272 273 getInfo (name, stats);
274 returncast(ulong) stats.st_size;
275 }
276 277 /***************************************************************************
278 279 Is this file writable?
280 281 ***************************************************************************/282 283 staticboolisWritable (cstringname)
284 {
285 returnposix.access(name.ptr, W_OK) == 0;
286 }
287 288 /***************************************************************************
289 290 Returns:
291 true if the file exist and is readable for the effective uid.
292 293 ***************************************************************************/294 295 staticboolisReadable (cstringname)
296 {
297 returnposix.access(name.ptr, R_OK) == 0;
298 }
299 300 /***************************************************************************
301 302 Is this file actually a folder/directory?
303 304 ***************************************************************************/305 306 staticboolisFolder (cstringname)
307 {
308 stat_tstats = void;
309 310 return (getInfo(name, stats) & S_IFMT) isS_IFDIR;
311 }
312 313 /***************************************************************************
314 315 Is this a normal file?
316 317 ***************************************************************************/318 319 staticboolisFile (cstringname)
320 {
321 stat_tstats = void;
322 323 return (getInfo(name, stats) & S_IFMT) isS_IFREG;
324 }
325 326 /***************************************************************************
327 328 Return timestamp information.
329 330 Timestamps are returns in a format dictated by the file-system.
331 For example NTFS keeps UTC time, while FAT timestamps are based
332 on the local time.
333 334 ***************************************************************************/335 336 staticStampstimeStamps (cstringname)
337 {
338 staticTimeconvert (typeof(stat_t.st_mtime) secs)
339 {
340 returnTime.epoch1970 +
341 TimeSpan.fromSeconds(secs);
342 }
343 344 stat_tstats = void;
345 Stampstime = void;
346 347 getInfo (name, stats);
348 349 time.modified = convert (stats.st_mtime);
350 time.accessed = convert (stats.st_atime);
351 time.created = convert (stats.st_ctime);
352 returntime;
353 }
354 355 /***************************************************************************
356 357 Set the accessed and modified timestamps of the specified file.
358 359 ***************************************************************************/360 361 staticvoidtimeStamps (cstringname, Timeaccessed, Timemodified)
362 {
363 utimbuftime = void;
364 time.actime = (accessed - Time.epoch1970).seconds;
365 time.modtime = (modified - Time.epoch1970).seconds;
366 if (utime (name.ptr, &time) is -1)
367 exception (name);
368 }
369 370 /***************************************************************************
371 372 Transfer the content of another file to this one.
373 374 Returns:
375 a reference to this class on success
376 377 Throws:
378 `IOException` upon failure
379 380 Note:
381 Allocates a memory buffer.
382 383 ***************************************************************************/384 385 staticvoidcopy (cstringsource, mstringdest)
386 {
387 autosrc = posix.open (source.ptr, O_RDONLY, Octal!("640"));
388 scope (exit)
389 if (src != -1)
390 posix.close (src);
391 392 autodst = posix.open (dest.ptr, O_CREAT | O_RDWR, Octal!("660"));
393 scope (exit)
394 if (dst != -1)
395 posix.close (dst);
396 397 if (srcis -1 || dstis -1)
398 exception (source);
399 400 // copy content401 ubyte[] buf = newubyte [16 * 1024];
402 autoread = posix.read (src, buf.ptr, buf.length);
403 while (read > 0)
404 {
405 autop = buf.ptr;
406 do {
407 autowritten = posix.write (dst, p, read);
408 p += written;
409 read -= written;
410 if (writtenis -1)
411 exception (dest);
412 } while (read > 0);
413 read = posix.read (src, buf.ptr, buf.length);
414 }
415 if (readis -1)
416 exception (source);
417 418 // copy timestamps419 stat_tstats;
420 if (posix.stat (source.ptr, &stats))
421 exception (source);
422 423 utimbufutim;
424 utim.actime = stats.st_atime;
425 utim.modtime = stats.st_mtime;
426 if (utime (dest.ptr, &utim) is -1)
427 exception (dest);
428 }
429 430 /***************************************************************************
431 432 Remove the file/directory from the file-system.
433 434 Returns:
435 `true` on success - `false` otherwise.
436 437 ***************************************************************************/438 439 staticboolremove (cstringname)
440 {
441 returncore.stdc.stdio.remove(name.ptr) != -1;
442 }
443 444 /***************************************************************************
445 446 Change the name or location of a file/directory.
447 448 ***************************************************************************/449 450 staticvoidrename (cstringsrc, cstringdst)
451 {
452 if (core.stdc.stdio.rename(src.ptr, dst.ptr) is -1)
453 exception(src);
454 }
455 456 /***************************************************************************
457 458 Create a new file.
459 460 Params:
461 mode = mode for the new file (defaults to 0660)
462 463 ***************************************************************************/464 465 staticvoidcreateFile (cstringname, mode_tmode = Octal!("660"))
466 {
467 intfd;
468 469 fd = posix.open (name.ptr, O_CREAT | O_WRONLY | O_TRUNC,
470 mode);
471 if (fdis -1)
472 exception (name);
473 474 if (posix.close(fd) is -1)
475 exception (name);
476 }
477 478 /***************************************************************************
479 480 Create a new directory.
481 482 Params:
483 mode = mode for the new directory (defaults to 0777)
484 485 *************************************************************************/486 487 staticvoidcreateFolder (cstringname, mode_tmode = Octal!("777"))
488 {
489 if (posix.mkdir (name.ptr, mode))
490 exception (name);
491 }
492 493 /***************************************************************************
494 495 List the set of filenames within this folder.
496 497 Each path and filename is passed to the provided delegate,
498 along with the path prefix and whether the entry is a folder or not.
499 500 Note: Allocates and reuses a small memory buffer.
501 502 ***************************************************************************/503 504 staticintlist (cstringfolder, scopeintdelegate(refFileInfo) dg, boolall=false)
505 {
506 intret;
507 DIR* dir;
508 dirententry;
509 dirent* pentry;
510 stat_tsbuf;
511 mstringprefix;
512 mstringsfnbuf;
513 514 dir = core.sys.posix.dirent.opendir (folder.ptr);
515 if (! dir)
516 returnret;
517 518 scope (exit)
519 {
520 importcore.memory;
521 core.sys.posix.dirent.closedir (dir);
522 GC.free(sfnbuf.ptr);
523 524 // only delete when we dupped it525 if (folder[$-2] != '/')
526 GC.free(prefix.ptr);
527 }
528 529 // ensure a trailing '/' is present530 if (folder[$-2] != '/')
531 {
532 prefix = folder.dup;
533 prefix[$-1] = '/';
534 }
535 else536 prefix = folder[0 .. $-1].dup;
537 538 // prepare our filename buffer539 sfnbuf = newchar[prefix.length + 256];
540 sfnbuf[0 .. prefix.length] = prefix[];
541 542 while (true)
543 {
544 // pentry is null at end of listing, or on an error545 readdir_r (dir, &entry, &pentry);
546 if (pentryisnull)
547 break;
548 549 autolen = core.stdc..string.strlen (entry.d_name.ptr);
550 autostr = entry.d_name.ptr [0 .. len];
551 ++len; // include the null552 553 // resize the buffer as necessary ...554 if (sfnbuf.length < prefix.length + len)
555 sfnbuf.length = prefix.length + len;
556 557 sfnbuf [prefix.length .. prefix.length + len]
558 = entry.d_name.ptr [0 .. len];
559 560 // skip "..." names561 if (str.length > 3 || str != "..."[0 .. str.length])
562 {
563 FileInfoinfo = void;
564 info.bytes = 0;
565 info.name = idup(str);
566 info.path = idup(prefix);
567 info.hidden = str[0] is'.';
568 info.folder = info.system = false;
569 570 if (! stat (sfnbuf.ptr, &sbuf))
571 {
572 info.folder = (sbuf.st_mode & S_IFDIR) != 0;
573 if (info.folderisfalse)
574 {
575 if ((sbuf.st_mode & S_IFREG) is0)
576 info.system = true;
577 else578 info.bytes = cast(ulong) sbuf.st_size;
579 }
580 }
581 if (all || (info.hidden | info.system) isfalse)
582 if ((ret = dg(info)) != 0)
583 break;
584 }
585 }
586 returnret;
587 assert(false);
588 }
589 }
590 591 592 /*******************************************************************************
593 594 Parses a file path.
595 596 File paths containing non-ansi characters should be UTF-8 encoded.
597 Supporting Unicode in this manner was deemed to be more suitable
598 than providing a wchar version of PathParser, and is both consistent
599 & compatible with the approach taken with the Uri class.
600 601 Note that patterns of adjacent '.' separators are treated specially
602 in that they will be assigned to the name where there is no distinct
603 suffix. In addition, a '.' at the start of a name signifies it does
604 not belong to the suffix i.e. ".file" is a name rather than a suffix.
605 Patterns of intermediate '.' characters will otherwise be assigned
606 to the suffix, such that "file....suffix" includes the dots within
607 the suffix itself. See method ext() for a suffix without dots.
608 609 Note also that normalization of path-separators does *not* occur by
610 default. This means that usage of '\' characters should be explicitly
611 converted beforehand into '/' instead (an exception is thrown in those
612 cases where '\' is present). On-the-fly conversion is avoided because
613 (a) the provided path is considered immutable and (b) we avoid taking
614 a copy of the original path. Module FilePath exists at a higher level,
615 without such contraints.
616 617 *******************************************************************************/618 619 structPathParser620 {
621 packagemstringfp; // filepath with trailing622 packageintend_, // before any trailing 0623 ext_, // after rightmost '.'624 name_, // file/dir name625 folder_, // path before name626 suffix_; // including leftmost '.'627 628 /***********************************************************************
629 630 Parse the path spec.
631 632 ***********************************************************************/633 634 PathParserparse (mstringpath)
635 {
636 returnparse (path, path.length);
637 }
638 639 /***********************************************************************
640 641 Duplicate this path.
642 643 Note: Allocates memory for the path content.
644 645 ***********************************************************************/646 647 PathParserdup ()
648 {
649 autoret = this;
650 ret.fp = fp.dup;
651 returnret;
652 }
653 654 /***********************************************************************
655 656 Return the complete text of this filepath.
657 658 ***********************************************************************/659 660 istringtoString ()
661 {
662 returnidup(fp [0 .. end_]);
663 }
664 665 /***********************************************************************
666 667 Return the root of this path. Roots are constructs such as
668 "C:".
669 670 ***********************************************************************/671 672 mstringroot ()
673 {
674 returnfp [0 .. folder_];
675 }
676 677 /***********************************************************************
678 679 Return the file path. Paths may start and end with a "/".
680 The root path is "/" and an unspecified path is returned as
681 an empty string. Directory paths may be split such that the
682 directory name is placed into the 'name' member; directory
683 paths are treated no differently than file paths.
684 685 ***********************************************************************/686 687 mstringfolder ()
688 {
689 returnfp [folder_ .. name_];
690 }
691 692 /***********************************************************************
693 694 Returns a path representing the parent of this one. This
695 will typically return the current path component, though
696 with a special case where the name component is empty. In
697 such cases, the path is scanned for a prior segment:
698 $(UL
699 $(LI normal: /x/y/z => /x/y)
700 $(LI special: /x/y/ => /x)
701 $(LI normal: /x => /)
702 $(LI normal: / => [empty]))
703 704 Note that this returns a path suitable for splitting into
705 path and name components (there's no trailing separator).
706 707 ***********************************************************************/708 709 cstringparent ()
710 {
711 autop = path;
712 if (name.lengthis0)
713 for (inti=(cast(int) p.length) - 1; --i > 0;)
714 if (p[i] isFileConst.PathSeparatorChar)
715 {
716 p = p[0 .. i];
717 break;
718 }
719 returnFS.stripped (p);
720 }
721 722 /***********************************************************************
723 724 Pop the rightmost element off this path, stripping off a
725 trailing '/' as appropriate:
726 $(UL
727 $(LI /x/y/z => /x/y)
728 $(LI /x/y/ => /x/y (note trailing '/' in the original))
729 $(LI /x/y => /x)
730 $(LI /x => /)
731 $(LI / => [empty]))
732 733 Note that this returns a path suitable for splitting into
734 path and name components (there's no trailing separator).
735 736 ***********************************************************************/737 738 cstringpop ()
739 {
740 returnFS.stripped (path);
741 }
742 743 /***********************************************************************
744 745 Return the name of this file, or directory.
746 747 ***********************************************************************/748 749 mstringname ()
750 {
751 returnfp [name_ .. suffix_];
752 }
753 754 /***********************************************************************
755 756 Ext is the tail of the filename, rightward of the rightmost
757 '.' separator e.g. path "foo.bar" has ext "bar". Note that
758 patterns of adjacent separators are treated specially - for
759 example, ".." will wind up with no ext at all.
760 761 ***********************************************************************/762 763 mstringext ()
764 {
765 autox = suffix;
766 if (x.length)
767 {
768 if (ext_is0)
769 foreach (c; x)
770 {
771 if (cis'.')
772 ++ext_;
773 else774 break;
775 }
776 x = x [ext_ .. $];
777 }
778 returnx;
779 }
780 781 /***********************************************************************
782 783 Suffix is like ext, but includes the separator e.g. path
784 "foo.bar" has suffix ".bar".
785 786 ***********************************************************************/787 788 mstringsuffix ()
789 {
790 returnfp [suffix_ .. end_];
791 }
792 793 /***********************************************************************
794 795 Return the root + folder combination.
796 797 ***********************************************************************/798 799 mstringpath ()
800 {
801 returnfp [0 .. name_];
802 }
803 804 /***********************************************************************
805 806 Return the name + suffix combination.
807 808 ***********************************************************************/809 810 mstringfile ()
811 {
812 returnfp [name_ .. end_];
813 }
814 815 /***********************************************************************
816 817 Returns true if this path is *not* relative to the
818 current working directory.
819 820 ***********************************************************************/821 822 boolisAbsolute ()
823 {
824 return (folder_ > 0) ||
825 (folder_ < end_ && fp[folder_] isFileConst.PathSeparatorChar);
826 }
827 828 /***********************************************************************
829 830 Returns true if this FilePath is empty.
831 832 ***********************************************************************/833 834 boolisEmpty ()
835 {
836 returnend_is0;
837 }
838 839 /***********************************************************************
840 841 Returns true if this path has a parent. Note that a
842 parent is defined by the presence of a path-separator in
843 the path. This means 'foo' within "/foo" is considered a
844 child of the root.
845 846 ***********************************************************************/847 848 boolisChild ()
849 {
850 returnfolder.length > 0;
851 }
852 853 /***********************************************************************
854 855 Does this path equate to the given text? We ignore trailing
856 path-separators when testing equivalence.
857 858 ***********************************************************************/859 860 equals_topEquals (cstrings)
861 {
862 returnFS.stripped(s) == FS.stripped(toString);
863 }
864 865 /***********************************************************************
866 867 Comparison to another PathParser, to avoid falling back to
868 auto-generated one
869 870 ***********************************************************************/871 872 equals_topEquals (PathParserrhs)
873 {
874 returnFS.stripped(rhs.toString()) == FS.stripped(toString());
875 }
876 877 /***********************************************************************
878 879 Parse the path spec with explicit end point. A '\' is
880 considered illegal in the path and should be normalized
881 out before this is invoked (the content managed here is
882 considered immutable, and thus cannot be changed by this
883 function.)
884 885 ***********************************************************************/886 887 packagePathParserparse (mstringpath, size_tend)
888 {
889 end_ = cast(int) end;
890 fp = path;
891 folder_ = 0;
892 name_ = suffix_ = -1;
893 894 for (inti=end_; --i >= 0;)
895 switch (fp[i])
896 {
897 caseFileConst.FileSeparatorChar:
898 if (name_ < 0)
899 if (suffix_ < 0 && i && fp[i-1] != '.')
900 suffix_ = i;
901 break;
902 903 caseFileConst.PathSeparatorChar:
904 if (name_ < 0)
905 name_ = i + 1;
906 break;
907 908 // Windows file separators are illegal. Use909 // standard() or equivalent to convert first910 case'\\':
911 FS.exception ("unexpected '\\' character in path: ", path[0..end]);
912 break;
913 914 default:
915 break;
916 }
917 918 if (name_ < 0)
919 name_ = folder_;
920 921 if (suffix_ < 0 || suffix_isname_)
922 suffix_ = end_;
923 924 returnthis;
925 }
926 }
927 928 929 /*******************************************************************************
930 931 Does this path currently exist?
932 933 *******************************************************************************/934 935 boolexists (cstringname)
936 {
937 char[512] tmp = void;
938 returnFS.exists (FS.strz(name, tmp));
939 }
940 941 /*******************************************************************************
942 943 Returns the time of the last modification. Accurate
944 to whatever the F/S supports, and in a format dictated
945 by the file-system. For example NTFS keeps UTC time,
946 while FAT timestamps are based on the local time.
947 948 *******************************************************************************/949 950 Timemodified (cstringname)
951 {
952 returntimeStamps(name).modified;
953 }
954 955 /*******************************************************************************
956 957 Returns the time of the last access. Accurate to
958 whatever the F/S supports, and in a format dictated
959 by the file-system. For example NTFS keeps UTC time,
960 while FAT timestamps are based on the local time.
961 962 *******************************************************************************/963 964 Timeaccessed (cstringname)
965 {
966 returntimeStamps(name).accessed;
967 }
968 969 /*******************************************************************************
970 971 Returns the time of file creation. Accurate to
972 whatever the F/S supports, and in a format dictated
973 by the file-system. For example NTFS keeps UTC time,
974 while FAT timestamps are based on the local time.
975 976 *******************************************************************************/977 978 Timecreated (cstringname)
979 {
980 returntimeStamps(name).created;
981 }
982 983 /*******************************************************************************
984 985 Return the file length (in bytes.)
986 987 *******************************************************************************/988 989 ulongfileSize (cstringname)
990 {
991 char[512] tmp = void;
992 returnFS.fileSize (FS.strz(name, tmp));
993 }
994 995 /*******************************************************************************
996 997 Is this file writable?
998 999 *******************************************************************************/1000 1001 boolisWritable (cstringname)
1002 {
1003 char[512] tmp = void;
1004 returnFS.isWritable (FS.strz(name, tmp));
1005 }
1006 1007 /*******************************************************************************
1008 1009 Returns:
1010 true if the file exist and is readable for the effective
1011 uid.
1012 1013 *******************************************************************************/1014 1015 boolisReadable (cstringname)
1016 {
1017 char[512] tmp = void;
1018 returnFS.isReadable(FS.strz(name, tmp));
1019 }
1020 1021 /*******************************************************************************
1022 1023 Is this file actually a folder/directory?
1024 1025 *******************************************************************************/1026 1027 boolisFolder (cstringname)
1028 {
1029 char[512] tmp = void;
1030 returnFS.isFolder (FS.strz(name, tmp));
1031 }
1032 1033 /*******************************************************************************
1034 1035 Is this file actually a normal file?
1036 Not a directory or (on unix) a device file or link.
1037 1038 *******************************************************************************/1039 1040 boolisFile (cstringname)
1041 {
1042 char[512] tmp = void;
1043 returnFS.isFile (FS.strz(name, tmp));
1044 }
1045 1046 /*******************************************************************************
1047 1048 Return timestamp information.
1049 1050 Timestamps are returns in a format dictated by the
1051 file-system. For example NTFS keeps UTC time,
1052 while FAT timestamps are based on the local time.
1053 1054 *******************************************************************************/1055 1056 FS.StampstimeStamps (cstringname)
1057 {
1058 char[512] tmp = void;
1059 returnFS.timeStamps (FS.strz(name, tmp));
1060 }
1061 1062 /*******************************************************************************
1063 1064 Set the accessed and modified timestamps of the specified file.
1065 1066 *******************************************************************************/1067 1068 voidtimeStamps (cstringname, Timeaccessed, Timemodified)
1069 {
1070 char[512] tmp = void;
1071 FS.timeStamps (FS.strz(name, tmp), accessed, modified);
1072 }
1073 1074 /*******************************************************************************
1075 1076 Remove the file/directory from the file-system. Returns true if
1077 successful, false otherwise.
1078 1079 *******************************************************************************/1080 1081 boolremove (cstringname)
1082 {
1083 char[512] tmp = void;
1084 returnFS.remove (FS.strz(name, tmp));
1085 }
1086 1087 /*******************************************************************************
1088 1089 Remove the files and folders listed in the provided paths. Where
1090 folders are listed, they should be preceded by their contained
1091 files in order to be successfully removed. Returns a set of paths
1092 that failed to be removed (where .length is zero upon success).
1093 1094 The collate() function can be used to provide the input paths:
1095 ---
1096 remove (collate (".", "*.d", true));
1097 ---
1098 1099 Use with great caution.
1100 1101 Note: May allocate memory.
1102 1103 *******************************************************************************/1104 1105 cstring[] remove (const(char[])[] paths)
1106 {
1107 cstring[] failed;
1108 foreach (path; paths)
1109 if (! remove (path))
1110 failed ~= path;
1111 returnfailed;
1112 }
1113 1114 /*******************************************************************************
1115 1116 Create a new file.
1117 1118 *******************************************************************************/1119 1120 voidcreateFile (cstringname)
1121 {
1122 char[512] tmp = void;
1123 FS.createFile (FS.strz(name, tmp));
1124 }
1125 1126 /*******************************************************************************
1127 1128 Create a new directory.
1129 1130 *******************************************************************************/1131 1132 voidcreateFolder (cstringname)
1133 {
1134 char[512] tmp = void;
1135 FS.createFolder (FS.strz(name, tmp));
1136 }
1137 1138 /*******************************************************************************
1139 1140 Create an entire path consisting of this folder along with
1141 all parent folders. The path should not contain '.' or '..'
1142 segments, which can be removed via the normalize() function.
1143 1144 Note that each segment is created as a folder, including the
1145 trailing segment.
1146 1147 Throws: IOException upon system errors.
1148 1149 Throws: IllegalArgumentException if a segment exists but as a
1150 file instead of a folder.
1151 1152 *******************************************************************************/1153 1154 voidcreatePath (cstringpath)
1155 {
1156 voidtest (cstringsegment)
1157 {
1158 if (segment.length)
1159 {
1160 if (! exists (segment))
1161 createFolder (segment);
1162 else1163 if (! isFolder (segment))
1164 thrownewIllegalArgumentException ("Path.createPath :: file/folder conflict: " ~ idup(segment));
1165 }
1166 }
1167 1168 foreach (i, charc; path)
1169 if (cis'/')
1170 test (path [0 .. i]);
1171 test (path);
1172 }
1173 1174 /*******************************************************************************
1175 1176 Change the name or location of a file/directory.
1177 1178 *******************************************************************************/1179 1180 voidrename (cstringsrc, cstringdst)
1181 {
1182 char[512] tmp1 = void;
1183 char[512] tmp2 = void;
1184 FS.rename (FS.strz(src, tmp1), FS.strz(dst, tmp2));
1185 }
1186 1187 /*******************************************************************************
1188 1189 Transfer the content of one file to another. Throws
1190 an IOException upon failure.
1191 1192 *******************************************************************************/1193 1194 voidcopy (cstringsrc, cstringdst)
1195 {
1196 char[512] tmp1 = void;
1197 char[512] tmp2 = void;
1198 FS.copy (FS.strz(src, tmp1), FS.strz(dst, tmp2));
1199 }
1200 1201 /*******************************************************************************
1202 1203 Provides foreach support via a fruct, as in
1204 ---
1205 foreach (info; children("myfolder"))
1206 ...
1207 ---
1208 1209 Each path and filename is passed to the foreach
1210 delegate, along with the path prefix and whether
1211 the entry is a folder or not. The info construct
1212 exposes the following attributes:
1213 ---
1214 mstring path
1215 mstring name
1216 ulong bytes
1217 bool folder
1218 ---
1219 1220 Argument 'all' controls whether hidden and system
1221 files are included - these are ignored by default.
1222 1223 *******************************************************************************/1224 1225 FS.Listingchildren (cstringpath, boolall=false)
1226 {
1227 returnFS.Listing (path, all);
1228 }
1229 1230 /*******************************************************************************
1231 1232 Collate all files and folders from the given path whose name matches
1233 the given pattern. Folders will be traversed where recurse is enabled,
1234 and a set of matching names is returned as filepaths (including those
1235 folders which match the pattern.)
1236 1237 Note: Allocates memory for returned paths.
1238 1239 *******************************************************************************/1240 1241 mstring[] collate (cstringpath, cstringpattern, boolrecurse=false)
1242 {
1243 mstring[] list;
1244 1245 foreach (info; children (path))
1246 {
1247 if (info.folder && recurse)
1248 list ~= collate (join(info.path, info.name), pattern, true);
1249 1250 if (patternMatch (info.name, pattern))
1251 list ~= join (info.path, info.name);
1252 }
1253 returnlist;
1254 }
1255 1256 /*******************************************************************************
1257 1258 Join a set of path specs together. A path separator is
1259 potentially inserted between each of the segments.
1260 1261 Note: May allocate memory.
1262 1263 *******************************************************************************/1264 1265 mstringjoin (const(char[])[] paths...)
1266 {
1267 returnFS.join (paths);
1268 }
1269 1270 /*******************************************************************************
1271 1272 Convert path separators to a standard format, using '/' as
1273 the path separator. This is compatible with Uri and all of
1274 the contemporary O/S which Tango supports. Known exceptions
1275 include the Windows command-line processor, which considers
1276 '/' characters to be switches instead. Use the native()
1277 method to support that.
1278 1279 Note: Mutates the provided path.
1280 1281 *******************************************************************************/1282 1283 mstringstandard (mstringpath)
1284 {
1285 returnreplace (path, '\\', '/');
1286 }
1287 1288 /*******************************************************************************
1289 1290 Convert to native O/S path separators where that is required,
1291 such as when dealing with the Windows command-line.
1292 1293 Note: Mutates the provided path. Use this pattern to obtain a
1294 copy instead: native(path.dup);
1295 1296 *******************************************************************************/1297 1298 mstringnative (mstringpath)
1299 {
1300 returnpath;
1301 }
1302 1303 /*******************************************************************************
1304 1305 Returns a path representing the parent of this one, with a special
1306 case concerning a trailing '/':
1307 $(UL
1308 $(LI normal: /x/y/z => /x/y)
1309 $(LI normal: /x/y/ => /x/y)
1310 $(LI special: /x/y/ => /x)
1311 $(LI normal: /x => /)
1312 $(LI normal: / => empty))
1313 1314 The result can be split via parse().
1315 1316 *******************************************************************************/1317 1318 cstringparent (cstringpath)
1319 {
1320 returnpop (FS.stripped (path));
1321 }
1322 1323 /*******************************************************************************
1324 1325 Returns a path representing the parent of this one:
1326 $(UL
1327 $(LI normal: /x/y/z => /x/y)
1328 $(LI normal: /x/y/ => /x/y)
1329 $(LI normal: /x => /)
1330 $(LI normal: / => empty))
1331 1332 The result can be split via parse().
1333 1334 *******************************************************************************/1335 1336 cstringpop (cstringpath)
1337 {
1338 size_ti = path.length;
1339 while (i && path[--i] != '/') {}
1340 returnpath [0..i];
1341 }
1342 1343 /*******************************************************************************
1344 1345 Break a path into "head" and "tail" components. For example:
1346 $(UL
1347 $(LI "/a/b/c" -> "/a","b/c")
1348 $(LI "a/b/c" -> "a","b/c"))
1349 1350 *******************************************************************************/1351 1352 mstringsplit (mstringpath, outmstringhead, outmstringtail)
1353 {
1354 head = path;
1355 if (path.length > 1)
1356 foreach (i, charc; path[1..$])
1357 if (cis'/')
1358 {
1359 head = path [0 .. i+1];
1360 tail = path [i+2 .. $];
1361 break;
1362 }
1363 returnpath;
1364 }
1365 1366 /*******************************************************************************
1367 1368 Replace all path 'from' instances with 'to', in place (overwrites
1369 the provided path).
1370 1371 *******************************************************************************/1372 1373 mstringreplace (mstringpath, charfrom, charto)
1374 {
1375 foreach (refcharc; path)
1376 if (cisfrom)
1377 c = to;
1378 returnpath;
1379 }
1380 1381 /*******************************************************************************
1382 1383 Parse a path into its constituent components.
1384 1385 Note that the provided path is sliced, not duplicated.
1386 1387 *******************************************************************************/1388 1389 PathParserparse (mstringpath)
1390 {
1391 PathParserp;
1392 1393 p.parse (path);
1394 returnp;
1395 }
1396 1397 /*******************************************************************************
1398 1399 *******************************************************************************/1400 1401 unittest1402 {
1403 autop = parse ("/foo/bar/file.ext".dup);
1404 test (p == "/foo/bar/file.ext");
1405 test (p.folder == "/foo/bar/");
1406 test (p.path == "/foo/bar/");
1407 test (p.file == "file.ext");
1408 test (p.name == "file");
1409 test (p.suffix == ".ext");
1410 test (p.ext == "ext");
1411 test (p.isChild == true);
1412 test (p.isEmpty == false);
1413 test (p.isAbsolute == true);
1414 }
1415 1416 /******************************************************************************
1417 1418 Matches a pattern against a filename.
1419 1420 Some characters of pattern have special a meaning (they are
1421 $(EM meta-characters)) and $(B can't) be escaped. These are:
1422 1423 $(TABLE
1424 $(TR
1425 $(TD $(B *))
1426 $(TD Matches 0 or more instances of any character.))
1427 $(TR
1428 $(TD $(B ?))
1429 $(TD Matches exactly one instances of any character.))
1430 $(TR
1431 $(TD $(B [)$(EM chars)$(B ]))
1432 $(TD Matches one instance of any character that appears
1433 between the brackets.))
1434 $(TR
1435 $(TD $(B [!)$(EM chars)$(B ]))
1436 $(TD Matches one instance of any character that does not appear
1437 between the brackets after the exclamation mark.))
1438 )
1439 1440 Internally individual character comparisons are done calling
1441 charMatch(), so its rules apply here too. Note that path
1442 separators and dots don't stop a meta-character from matching
1443 further portions of the filename.
1444 1445 Returns: true if pattern matches filename, false otherwise.
1446 1447 Throws: Nothing.
1448 -----
1449 patternMatch("Go*.bar", "[fg]???bar"); // => false
1450 patternMatch("/foo*home/bar", "?foo*bar"); // => true
1451 patternMatch("foobar", "foo?bar"); // => true
1452 -----
1453 1454 ******************************************************************************/1455 1456 boolpatternMatch (cstringfilename, cstringpattern)
1457 {
1458 // Verify that pattern[] is valid1459 boolinbracket = false;
1460 for (autoi=0; i < pattern.length; i++)
1461 {
1462 switch (pattern[i])
1463 {
1464 case'[':
1465 verify(!inbracket);
1466 inbracket = true;
1467 break;
1468 case']':
1469 verify(inbracket);
1470 inbracket = false;
1471 break;
1472 default:
1473 break;
1474 }
1475 }
1476 1477 intpi;
1478 intni;
1479 charpc;
1480 charnc;
1481 intj;
1482 intnot;
1483 intanymatch;
1484 1485 boolcharMatch (charc1, charc2)
1486 {
1487 returnc1 == c2;
1488 }
1489 1490 ni = 0;
1491 for (pi = 0; pi < pattern.length; pi++)
1492 {
1493 pc = pattern [pi];
1494 switch (pc)
1495 {
1496 case'*':
1497 if (pi + 1 == pattern.length)
1498 gotomatch;
1499 for (j = ni; j < filename.length; j++)
1500 {
1501 if (patternMatch(filename[j .. filename.length],
1502 pattern[pi + 1 .. pattern.length]))
1503 gotomatch;
1504 }
1505 gotonomatch;
1506 1507 case'?':
1508 if (ni == filename.length)
1509 gotonomatch;
1510 ni++;
1511 break;
1512 1513 case'[':
1514 if (ni == filename.length)
1515 gotonomatch;
1516 nc = filename[ni];
1517 ni++;
1518 not = 0;
1519 pi++;
1520 if (pattern[pi] == '!')
1521 {
1522 not = 1;
1523 pi++;
1524 }
1525 anymatch = 0;
1526 while (1)
1527 {
1528 pc = pattern[pi];
1529 if (pc == ']')
1530 break;
1531 if (!anymatch && charMatch(nc, pc))
1532 anymatch = 1;
1533 pi++;
1534 }
1535 if (!(anymatch ^ not))
1536 gotonomatch;
1537 break;
1538 1539 default:
1540 if (ni == filename.length)
1541 gotonomatch;
1542 nc = filename[ni];
1543 if (!charMatch(pc, nc))
1544 gotonomatch;
1545 ni++;
1546 break;
1547 }
1548 }
1549 if (ni < filename.length)
1550 gotonomatch;
1551 1552 match:
1553 returntrue;
1554 1555 nomatch:
1556 returnfalse;
1557 }
1558 1559 /*******************************************************************************
1560 1561 *******************************************************************************/1562 1563 unittest1564 {
1565 test(!patternMatch("foo", "Foo"));
1566 1567 test(patternMatch("foo", "*"));
1568 test(patternMatch("foo.bar", "*"));
1569 test(patternMatch("foo.bar", "*.*"));
1570 test(patternMatch("foo.bar", "foo*"));
1571 test(patternMatch("foo.bar", "f*bar"));
1572 test(patternMatch("foo.bar", "f*b*r"));
1573 test(patternMatch("foo.bar", "f???bar"));
1574 test(patternMatch("foo.bar", "[fg]???bar"));
1575 test(patternMatch("foo.bar", "[!gh]*bar"));
1576 1577 test(!patternMatch("foo", "bar"));
1578 test(!patternMatch("foo", "*.*"));
1579 test(!patternMatch("foo.bar", "f*baz"));
1580 test(!patternMatch("foo.bar", "f*b*x"));
1581 test(!patternMatch("foo.bar", "[gh]???bar"));
1582 test(!patternMatch("foo.bar", "[!fg]*bar"));
1583 test(!patternMatch("foo.bar", "[fg]???baz"));
1584 }
1585 1586 /*******************************************************************************
1587 1588 Normalizes a path component.
1589 $(UL
1590 $(LI $(B .) segments are removed)
1591 $(LI <segment>$(B /..) are removed))
1592 1593 Multiple consecutive forward slashes are replaced with a single
1594 forward slash. On Windows, \ will be converted to / prior to any
1595 normalization.
1596 1597 Note that any number of .. segments at the front is ignored,
1598 unless it is an absolute path, in which case they are removed.
1599 1600 The input path is copied into either the provided buffer, or a heap
1601 allocated array if no buffer was provided. Normalization modifies
1602 this copy before returning the relevant slice.
1603 -----
1604 normalize("/home/foo/./bar/../../john/doe"); // => "/home/john/doe"
1605 -----
1606 1607 Note: Allocates memory.
1608 1609 *******************************************************************************/1610 1611 mstringnormalize (cstringin_path, mstringbuf = null)
1612 {
1613 size_tidx; // Current position1614 size_tmoveTo; // Position to move1615 boolisAbsolute; // Whether the path is absolute1616 enum {NodeStackLength = 64}
1617 mstringpath; // resulting path to return1618 1619 // Starting positions of regular path segments are pushed1620 // on this stack to avoid backward scanning when .. segments1621 // are encountered1622 size_t[NodeStackLength] nodeStack;
1623 size_tnodeStackTop;
1624 1625 // Moves the path tail starting at the current position to1626 // moveTo. Then sets the current position to moveTo.1627 voidmove ()
1628 {
1629 autolen = path.length - idx;
1630 memmove (path.ptr + moveTo, path.ptr + idx, len);
1631 path = path[0..moveTo + len];
1632 idx = moveTo;
1633 }
1634 1635 // Checks if the character at the current position is a1636 // separator. If true, normalizes the separator to '/' on1637 // Windows and advances the current position to the next1638 // character.1639 boolisSep (refsize_ti)
1640 {
1641 charc = path[i];
1642 if (c != '/')
1643 returnfalse;
1644 i++;
1645 returntrue;
1646 }
1647 1648 if (bufisnull)
1649 path = in_path.dup;
1650 else1651 path = buf[0..in_path.length] = in_path[];
1652 1653 if (idx == path.length)
1654 returnpath;
1655 1656 moveTo = idx;
1657 if (isSep(idx))
1658 {
1659 moveTo++; // preserve root separator.1660 isAbsolute = true;
1661 }
1662 1663 while (idx < path.length)
1664 {
1665 // Skip duplicate separators1666 if (isSep(idx))
1667 continue;
1668 1669 if (path[idx] == '.')
1670 {
1671 // leave the current position at the start of1672 // the segment1673 autoi = idx + 1;
1674 if (i < path.length && path[i] == '.')
1675 {
1676 i++;
1677 if (i == path.length || isSep(i))
1678 {
1679 // It is a '..' segment. If the stack is not1680 // empty, set moveTo and the current position1681 // to the start position of the last found1682 // regular segment1683 if (nodeStackTop > 0)
1684 moveTo = nodeStack[--nodeStackTop];
1685 1686 // If no regular segment start positions on the1687 // stack, drop the .. segment if it is absolute1688 // path or, otherwise, advance moveTo and the1689 // current position to the character after the1690 // '..' segment1691 else1692 if (!isAbsolute)
1693 {
1694 if (moveTo != idx)
1695 {
1696 i -= idx - moveTo;
1697 move();
1698 }
1699 moveTo = i;
1700 }
1701 1702 idx = i;
1703 continue;
1704 }
1705 }
1706 1707 // If it is '.' segment, skip it.1708 if (i == path.length || isSep(i))
1709 {
1710 idx = i;
1711 continue;
1712 }
1713 }
1714 1715 // Remove excessive '/', '.' and/or '..' preceeding the1716 // segment1717 if (moveTo != idx)
1718 move();
1719 1720 // Push the start position of the regular segment on the1721 // stack1722 verify(nodeStackTop < NodeStackLength);
1723 nodeStack[nodeStackTop++] = idx;
1724 1725 // Skip the regular segment and set moveTo to the position1726 // after the segment (including the trailing '/' if present)1727 for (; idx < path.length && !isSep(idx); idx++)
1728 {}
1729 moveTo = idx;
1730 }
1731 1732 if (moveTo != idx)
1733 move();
1734 returnpath;
1735 }
1736 1737 /*******************************************************************************
1738 1739 *******************************************************************************/1740 1741 unittest1742 {
1743 test (normalize ("") == "");
1744 test (normalize ("/home/../john/../.tango/.htaccess") == "/.tango/.htaccess");
1745 test (normalize ("/home/../john/../.tango/foo.conf") == "/.tango/foo.conf");
1746 test (normalize ("/home/john/.tango/foo.conf") == "/home/john/.tango/foo.conf");
1747 test (normalize ("/foo/bar/.htaccess") == "/foo/bar/.htaccess");
1748 test (normalize ("foo/bar/././.") == "foo/bar/");
1749 test (normalize ("././foo/././././bar") == "foo/bar");
1750 test (normalize ("/foo/../john") == "/john");
1751 test (normalize ("foo/../john") == "john");
1752 test (normalize ("foo/bar/..") == "foo/");
1753 test (normalize ("foo/bar/../john") == "foo/john");
1754 test (normalize ("foo/bar/doe/../../john") == "foo/john");
1755 test (normalize ("foo/bar/doe/../../john/../bar") == "foo/bar");
1756 test (normalize ("./foo/bar/doe") == "foo/bar/doe");
1757 test (normalize ("./foo/bar/doe/../../john/../bar") == "foo/bar");
1758 test (normalize ("./foo/bar/../../john/../bar") == "bar");
1759 test (normalize ("foo/bar/./doe/../../john") == "foo/john");
1760 test (normalize ("../../foo/bar") == "../../foo/bar");
1761 test (normalize ("../../../foo/bar") == "../../../foo/bar");
1762 test (normalize ("d/") == "d/");
1763 test (normalize ("/home/john/./foo/bar.txt") == "/home/john/foo/bar.txt");
1764 test (normalize ("/home//john") == "/home/john");
1765 1766 test (normalize("/../../bar/") == "/bar/");
1767 test (normalize("/../../bar/../baz/./") == "/baz/");
1768 test (normalize("/../../bar/boo/../baz/.bar/.") == "/bar/baz/.bar/");
1769 test (normalize("../..///.///bar/..//..//baz/.//boo/..") == "../../../baz/");
1770 test (normalize("./bar/./..boo/./..bar././/") == "bar/..boo/..bar./");
1771 test (normalize("/bar/..") == "/");
1772 test (normalize("bar/") == "bar/");
1773 test (normalize(".../") == ".../");
1774 test (normalize("///../foo") == "/foo");
1775 test (normalize("./foo") == "foo");
1776 autobuf = newchar[100];
1777 autoret = normalize("foo/bar/./baz", buf);
1778 test (ret.ptr == buf.ptr);
1779 test (ret == "foo/bar/baz");
1780 }
1781 1782 1783 /*******************************************************************************
1784 1785 *******************************************************************************/1786 1787 debug (Path)
1788 {
1789 importocean.io.Stdout;
1790 1791 voidmain()
1792 {
1793 foreach (file; collate (".", "*.d", true))
1794 Stdout (file).newline;
1795 }
1796 }