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 module ocean.io.Path; 59 60 public import ocean.core.ExceptionDefinitions : IOException, IllegalArgumentException; 61 import ocean.core.Verify; 62 import ocean.io.model.IFile : FileConst, FileInfo; 63 import ocean.sys.Common; 64 public import ocean.time.Time : Time, TimeSpan; 65 import ocean.transition; 66 67 import core.stdc..string : memmove; 68 69 version (unittest) import ocean.core.Test; 70 71 import core.stdc.stdio; 72 import core.stdc..string; 73 import core.sys.posix.utime; 74 import core.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 package struct FS 86 { 87 /*********************************************************************** 88 89 TimeStamp information. Accurate to whatever the F/S supports. 90 91 ***********************************************************************/ 92 93 struct Stamps 94 { 95 Time created; /// Time created. 96 Time accessed; /// Last time accessed. 97 Time modified; /// Last time modified. 98 } 99 100 /*********************************************************************** 101 102 Some fruct glue for directory listings. 103 104 ***********************************************************************/ 105 106 struct Listing 107 { 108 cstring folder; 109 bool allFiles; 110 111 int opApply (scope int delegate(ref FileInfo) dg) 112 { 113 char[256] tmp = void; 114 auto path = strz (folder, tmp); 115 116 return list (path, dg, allFiles); 117 } 118 } 119 120 /*********************************************************************** 121 122 Throw an exception using the last known error. 123 124 ***********************************************************************/ 125 126 static void exception (cstring filename) 127 { 128 exception (filename[0..$-1] ~ ": ", SysError.lastMsg); 129 } 130 131 /*********************************************************************** 132 133 Throw an IO exception. 134 135 ***********************************************************************/ 136 137 static void exception (cstring prefix, cstring error) 138 { 139 throw new IOException (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 static cstring padded (cstring path, char c = '/') 152 { 153 if (path.length && path[$-1] != c) 154 path = path ~ c; 155 return path; 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 static cstring paddedLeading (cstring path, char c = '/') 168 { 169 if (path.length && path[0] != c) 170 path = c ~ path; 171 return path; 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 static cstring stripped (cstring path, char c = '/') 182 { 183 if (path.length && path[$-1] is c) 184 path = path [0 .. $-1]; 185 return path; 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 static mstring join (const(char[])[] paths...) 198 { 199 mstring result; 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 return result; 211 } 212 213 return null; 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 static mstring strz (cstring src, mstring dst) 226 { 227 auto i = src.length + 1; 228 if (dst.length < i) 229 dst.length = i; 230 dst [0 .. i-1] = src; 231 dst[i-1] = 0; 232 return dst [0 .. i]; 233 } 234 235 /*************************************************************************** 236 237 Get info about this path. 238 239 ***************************************************************************/ 240 241 private static uint getInfo (cstring name, ref stat_t stats) 242 { 243 if (posix.stat (name.ptr, &stats)) 244 exception (name); 245 246 return stats.st_mode; 247 } 248 249 /*************************************************************************** 250 251 Returns: 252 whether the file or path exists. 253 254 ***************************************************************************/ 255 256 static bool exists (cstring name) 257 { 258 stat_t stats = void; 259 return posix.stat (name.ptr, &stats) is 0; 260 } 261 262 /*************************************************************************** 263 264 Returns: 265 the file length (in bytes.) 266 267 ***************************************************************************/ 268 269 static ulong fileSize (cstring name) 270 { 271 stat_t stats = void; 272 273 getInfo (name, stats); 274 return cast(ulong) stats.st_size; 275 } 276 277 /*************************************************************************** 278 279 Is this file writable? 280 281 ***************************************************************************/ 282 283 static bool isWritable (cstring name) 284 { 285 return posix.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 static bool isReadable (cstring name) 296 { 297 return posix.access(name.ptr, R_OK) == 0; 298 } 299 300 /*************************************************************************** 301 302 Is this file actually a folder/directory? 303 304 ***************************************************************************/ 305 306 static bool isFolder (cstring name) 307 { 308 stat_t stats = void; 309 310 return (getInfo(name, stats) & S_IFMT) is S_IFDIR; 311 } 312 313 /*************************************************************************** 314 315 Is this a normal file? 316 317 ***************************************************************************/ 318 319 static bool isFile (cstring name) 320 { 321 stat_t stats = void; 322 323 return (getInfo(name, stats) & S_IFMT) is S_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 static Stamps timeStamps (cstring name) 337 { 338 static Time convert (typeof(stat_t.st_mtime) secs) 339 { 340 return Time.epoch1970 + 341 TimeSpan.fromSeconds(secs); 342 } 343 344 stat_t stats = void; 345 Stamps time = 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 return time; 353 } 354 355 /*************************************************************************** 356 357 Set the accessed and modified timestamps of the specified file. 358 359 ***************************************************************************/ 360 361 static void timeStamps (cstring name, Time accessed, Time modified) 362 { 363 utimbuf time = 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 static void copy (cstring source, mstring dest) 386 { 387 auto src = posix.open (source.ptr, O_RDONLY, Octal!("640")); 388 scope (exit) 389 if (src != -1) 390 posix.close (src); 391 392 auto dst = posix.open (dest.ptr, O_CREAT | O_RDWR, Octal!("660")); 393 scope (exit) 394 if (dst != -1) 395 posix.close (dst); 396 397 if (src is -1 || dst is -1) 398 exception (source); 399 400 // copy content 401 ubyte[] buf = new ubyte [16 * 1024]; 402 auto read = posix.read (src, buf.ptr, buf.length); 403 while (read > 0) 404 { 405 auto p = buf.ptr; 406 do { 407 auto written = posix.write (dst, p, read); 408 p += written; 409 read -= written; 410 if (written is -1) 411 exception (dest); 412 } while (read > 0); 413 read = posix.read (src, buf.ptr, buf.length); 414 } 415 if (read is -1) 416 exception (source); 417 418 // copy timestamps 419 stat_t stats; 420 if (posix.stat (source.ptr, &stats)) 421 exception (source); 422 423 utimbuf utim; 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 static bool remove (cstring name) 440 { 441 return core.stdc.stdio.remove(name.ptr) != -1; 442 } 443 444 /*************************************************************************** 445 446 Change the name or location of a file/directory. 447 448 ***************************************************************************/ 449 450 static void rename (cstring src, cstring dst) 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 static void createFile (cstring name, mode_t mode = Octal!("660")) 466 { 467 int fd; 468 469 fd = posix.open (name.ptr, O_CREAT | O_WRONLY | O_TRUNC, 470 mode); 471 if (fd is -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 static void createFolder (cstring name, mode_t mode = 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 static int list (cstring folder, scope int delegate(ref FileInfo) dg, bool all=false) 505 { 506 int ret; 507 DIR* dir; 508 dirent entry; 509 dirent* pentry; 510 stat_t sbuf; 511 mstring prefix; 512 mstring sfnbuf; 513 514 dir = core.sys.posix.dirent.opendir (folder.ptr); 515 if (! dir) 516 return ret; 517 518 scope (exit) 519 { 520 import core.memory; 521 core.sys.posix.dirent.closedir (dir); 522 GC.free(sfnbuf.ptr); 523 524 // only delete when we dupped it 525 if (folder[$-2] != '/') 526 GC.free(prefix.ptr); 527 } 528 529 // ensure a trailing '/' is present 530 if (folder[$-2] != '/') 531 { 532 prefix = folder.dup; 533 prefix[$-1] = '/'; 534 } 535 else 536 prefix = folder[0 .. $-1].dup; 537 538 // prepare our filename buffer 539 sfnbuf = new char[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 error 545 readdir_r (dir, &entry, &pentry); 546 if (pentry is null) 547 break; 548 549 auto len = core.stdc..string.strlen (entry.d_name.ptr); 550 auto str = entry.d_name.ptr [0 .. len]; 551 ++len; // include the null 552 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 "..." names 561 if (str.length > 3 || str != "..."[0 .. str.length]) 562 { 563 FileInfo info = 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.folder is false) 574 { 575 if ((sbuf.st_mode & S_IFREG) is 0) 576 info.system = true; 577 else 578 info.bytes = cast(ulong) sbuf.st_size; 579 } 580 } 581 if (all || (info.hidden | info.system) is false) 582 if ((ret = dg(info)) != 0) 583 break; 584 } 585 } 586 return ret; 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 struct PathParser 620 { 621 package mstring fp; // filepath with trailing 622 package int end_, // before any trailing 0 623 ext_, // after rightmost '.' 624 name_, // file/dir name 625 folder_, // path before name 626 suffix_; // including leftmost '.' 627 628 /*********************************************************************** 629 630 Parse the path spec. 631 632 ***********************************************************************/ 633 634 PathParser parse (mstring path) 635 { 636 return parse (path, path.length); 637 } 638 639 /*********************************************************************** 640 641 Duplicate this path. 642 643 Note: Allocates memory for the path content. 644 645 ***********************************************************************/ 646 647 PathParser dup () 648 { 649 auto ret = this; 650 ret.fp = fp.dup; 651 return ret; 652 } 653 654 /*********************************************************************** 655 656 Return the complete text of this filepath. 657 658 ***********************************************************************/ 659 660 istring toString () 661 { 662 return idup(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 mstring root () 673 { 674 return fp [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 mstring folder () 688 { 689 return fp [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 cstring parent () 710 { 711 auto p = path; 712 if (name.length is 0) 713 for (int i=(cast(int) p.length) - 1; --i > 0;) 714 if (p[i] is FileConst.PathSeparatorChar) 715 { 716 p = p[0 .. i]; 717 break; 718 } 719 return FS.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 cstring pop () 739 { 740 return FS.stripped (path); 741 } 742 743 /*********************************************************************** 744 745 Return the name of this file, or directory. 746 747 ***********************************************************************/ 748 749 mstring name () 750 { 751 return fp [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 mstring ext () 764 { 765 auto x = suffix; 766 if (x.length) 767 { 768 if (ext_ is 0) 769 foreach (c; x) 770 { 771 if (c is '.') 772 ++ext_; 773 else 774 break; 775 } 776 x = x [ext_ .. $]; 777 } 778 return x; 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 mstring suffix () 789 { 790 return fp [suffix_ .. end_]; 791 } 792 793 /*********************************************************************** 794 795 Return the root + folder combination. 796 797 ***********************************************************************/ 798 799 mstring path () 800 { 801 return fp [0 .. name_]; 802 } 803 804 /*********************************************************************** 805 806 Return the name + suffix combination. 807 808 ***********************************************************************/ 809 810 mstring file () 811 { 812 return fp [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 bool isAbsolute () 823 { 824 return (folder_ > 0) || 825 (folder_ < end_ && fp[folder_] is FileConst.PathSeparatorChar); 826 } 827 828 /*********************************************************************** 829 830 Returns true if this FilePath is empty. 831 832 ***********************************************************************/ 833 834 bool isEmpty () 835 { 836 return end_ is 0; 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 bool isChild () 849 { 850 return folder.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_t opEquals (cstring s) 861 { 862 return FS.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_t opEquals (PathParser rhs) 873 { 874 return FS.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 package PathParser parse (mstring path, size_t end) 888 { 889 end_ = cast(int) end; 890 fp = path; 891 folder_ = 0; 892 name_ = suffix_ = -1; 893 894 for (int i=end_; --i >= 0;) 895 switch (fp[i]) 896 { 897 case FileConst.FileSeparatorChar: 898 if (name_ < 0) 899 if (suffix_ < 0 && i && fp[i-1] != '.') 900 suffix_ = i; 901 break; 902 903 case FileConst.PathSeparatorChar: 904 if (name_ < 0) 905 name_ = i + 1; 906 break; 907 908 // Windows file separators are illegal. Use 909 // standard() or equivalent to convert first 910 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_ is name_) 922 suffix_ = end_; 923 924 return this; 925 } 926 } 927 928 929 /******************************************************************************* 930 931 Does this path currently exist? 932 933 *******************************************************************************/ 934 935 bool exists (cstring name) 936 { 937 char[512] tmp = void; 938 return FS.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 Time modified (cstring name) 951 { 952 return timeStamps(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 Time accessed (cstring name) 965 { 966 return timeStamps(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 Time created (cstring name) 979 { 980 return timeStamps(name).created; 981 } 982 983 /******************************************************************************* 984 985 Return the file length (in bytes.) 986 987 *******************************************************************************/ 988 989 ulong fileSize (cstring name) 990 { 991 char[512] tmp = void; 992 return FS.fileSize (FS.strz(name, tmp)); 993 } 994 995 /******************************************************************************* 996 997 Is this file writable? 998 999 *******************************************************************************/ 1000 1001 bool isWritable (cstring name) 1002 { 1003 char[512] tmp = void; 1004 return FS.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 bool isReadable (cstring name) 1016 { 1017 char[512] tmp = void; 1018 return FS.isReadable(FS.strz(name, tmp)); 1019 } 1020 1021 /******************************************************************************* 1022 1023 Is this file actually a folder/directory? 1024 1025 *******************************************************************************/ 1026 1027 bool isFolder (cstring name) 1028 { 1029 char[512] tmp = void; 1030 return FS.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 bool isFile (cstring name) 1041 { 1042 char[512] tmp = void; 1043 return FS.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.Stamps timeStamps (cstring name) 1057 { 1058 char[512] tmp = void; 1059 return FS.timeStamps (FS.strz(name, tmp)); 1060 } 1061 1062 /******************************************************************************* 1063 1064 Set the accessed and modified timestamps of the specified file. 1065 1066 *******************************************************************************/ 1067 1068 void timeStamps (cstring name, Time accessed, Time modified) 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 bool remove (cstring name) 1082 { 1083 char[512] tmp = void; 1084 return FS.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 return failed; 1112 } 1113 1114 /******************************************************************************* 1115 1116 Create a new file. 1117 1118 *******************************************************************************/ 1119 1120 void createFile (cstring name) 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 void createFolder (cstring name) 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 void createPath (cstring path) 1155 { 1156 void test (cstring segment) 1157 { 1158 if (segment.length) 1159 { 1160 if (! exists (segment)) 1161 createFolder (segment); 1162 else 1163 if (! isFolder (segment)) 1164 throw new IllegalArgumentException ("Path.createPath :: file/folder conflict: " ~ idup(segment)); 1165 } 1166 } 1167 1168 foreach (i, char c; path) 1169 if (c is '/') 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 void rename (cstring src, cstring dst) 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 void copy (cstring src, cstring dst) 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.Listing children (cstring path, bool all=false) 1226 { 1227 return FS.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 (cstring path, cstring pattern, bool recurse=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 return list; 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 mstring join (const(char[])[] paths...) 1266 { 1267 return FS.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 mstring standard (mstring path) 1284 { 1285 return replace (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 mstring native (mstring path) 1299 { 1300 return path; 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 cstring parent (cstring path) 1319 { 1320 return pop (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 cstring pop (cstring path) 1337 { 1338 size_t i = path.length; 1339 while (i && path[--i] != '/') {} 1340 return path [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 mstring split (mstring path, out mstring head, out mstring tail) 1353 { 1354 head = path; 1355 if (path.length > 1) 1356 foreach (i, char c; path[1..$]) 1357 if (c is '/') 1358 { 1359 head = path [0 .. i+1]; 1360 tail = path [i+2 .. $]; 1361 break; 1362 } 1363 return path; 1364 } 1365 1366 /******************************************************************************* 1367 1368 Replace all path 'from' instances with 'to', in place (overwrites 1369 the provided path). 1370 1371 *******************************************************************************/ 1372 1373 mstring replace (mstring path, char from, char to) 1374 { 1375 foreach (ref char c; path) 1376 if (c is from) 1377 c = to; 1378 return path; 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 PathParser parse (mstring path) 1390 { 1391 PathParser p; 1392 1393 p.parse (path); 1394 return p; 1395 } 1396 1397 /******************************************************************************* 1398 1399 *******************************************************************************/ 1400 1401 unittest 1402 { 1403 auto p = 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 bool patternMatch (cstring filename, cstring pattern) 1457 { 1458 // Verify that pattern[] is valid 1459 bool inbracket = false; 1460 for (auto i=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 int pi; 1478 int ni; 1479 char pc; 1480 char nc; 1481 int j; 1482 int not; 1483 int anymatch; 1484 1485 bool charMatch (char c1, char c2) 1486 { 1487 return c1 == 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 goto match; 1499 for (j = ni; j < filename.length; j++) 1500 { 1501 if (patternMatch(filename[j .. filename.length], 1502 pattern[pi + 1 .. pattern.length])) 1503 goto match; 1504 } 1505 goto nomatch; 1506 1507 case '?': 1508 if (ni == filename.length) 1509 goto nomatch; 1510 ni++; 1511 break; 1512 1513 case '[': 1514 if (ni == filename.length) 1515 goto nomatch; 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 goto nomatch; 1537 break; 1538 1539 default: 1540 if (ni == filename.length) 1541 goto nomatch; 1542 nc = filename[ni]; 1543 if (!charMatch(pc, nc)) 1544 goto nomatch; 1545 ni++; 1546 break; 1547 } 1548 } 1549 if (ni < filename.length) 1550 goto nomatch; 1551 1552 match: 1553 return true; 1554 1555 nomatch: 1556 return false; 1557 } 1558 1559 /******************************************************************************* 1560 1561 *******************************************************************************/ 1562 1563 unittest 1564 { 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 mstring normalize (cstring in_path, mstring buf = null) 1612 { 1613 size_t idx; // Current position 1614 size_t moveTo; // Position to move 1615 bool isAbsolute; // Whether the path is absolute 1616 enum {NodeStackLength = 64} 1617 mstring path; // resulting path to return 1618 1619 // Starting positions of regular path segments are pushed 1620 // on this stack to avoid backward scanning when .. segments 1621 // are encountered 1622 size_t[NodeStackLength] nodeStack; 1623 size_t nodeStackTop; 1624 1625 // Moves the path tail starting at the current position to 1626 // moveTo. Then sets the current position to moveTo. 1627 void move () 1628 { 1629 auto len = 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 a 1636 // separator. If true, normalizes the separator to '/' on 1637 // Windows and advances the current position to the next 1638 // character. 1639 bool isSep (ref size_t i) 1640 { 1641 char c = path[i]; 1642 if (c != '/') 1643 return false; 1644 i++; 1645 return true; 1646 } 1647 1648 if (buf is null) 1649 path = in_path.dup; 1650 else 1651 path = buf[0..in_path.length] = in_path[]; 1652 1653 if (idx == path.length) 1654 return path; 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 separators 1666 if (isSep(idx)) 1667 continue; 1668 1669 if (path[idx] == '.') 1670 { 1671 // leave the current position at the start of 1672 // the segment 1673 auto i = 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 not 1680 // empty, set moveTo and the current position 1681 // to the start position of the last found 1682 // regular segment 1683 if (nodeStackTop > 0) 1684 moveTo = nodeStack[--nodeStackTop]; 1685 1686 // If no regular segment start positions on the 1687 // stack, drop the .. segment if it is absolute 1688 // path or, otherwise, advance moveTo and the 1689 // current position to the character after the 1690 // '..' segment 1691 else 1692 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 the 1716 // segment 1717 if (moveTo != idx) 1718 move(); 1719 1720 // Push the start position of the regular segment on the 1721 // stack 1722 verify(nodeStackTop < NodeStackLength); 1723 nodeStack[nodeStackTop++] = idx; 1724 1725 // Skip the regular segment and set moveTo to the position 1726 // 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 return path; 1735 } 1736 1737 /******************************************************************************* 1738 1739 *******************************************************************************/ 1740 1741 unittest 1742 { 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 auto buf = new char[100]; 1777 auto ret = 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 import ocean.io.Stdout; 1790 1791 void main() 1792 { 1793 foreach (file; collate (".", "*.d", true)) 1794 Stdout (file).newline; 1795 } 1796 }