1 /** 2 * File stuff 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.file; 15 16 import core.stdc.wchar_; 17 import core.thread; 18 19 import std.array; 20 import std.conv; 21 import std.file; 22 import std.meta : allSatisfy; 23 import std.path; 24 import std.range.primitives; 25 import std.stdio : File; 26 import std.string; 27 import std.traits : Unqual; 28 import std.typecons; 29 import std.utf; 30 31 import ae.sys.cmd : getCurrentThreadID; 32 import ae.utils.math : TypeForBits, bitsFor; 33 import ae.utils.path; 34 35 public import std.typecons : No, Yes; 36 37 deprecated alias wcscmp = core.stdc.wchar_.wcscmp; 38 deprecated alias wcslen = core.stdc.wchar_.wcslen; 39 40 version(Windows) import ae.sys.windows.imports; 41 42 // ************************************************************************ 43 44 version (Windows) 45 { 46 // Work around std.file overload 47 mixin(importWin32!(q{winnt}, null, q{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT})); 48 } 49 version (Posix) 50 { 51 private import core.stdc.errno; 52 private import core.sys.posix.dirent; 53 private import core.stdc.string; 54 } 55 56 // ************************************************************************ 57 58 deprecated string[] fastListDir(bool recursive = false, bool symlinks=false)(string pathname, string pattern = null) 59 { 60 string[] result; 61 62 listDir!((e) { 63 static if (!symlinks) 64 { 65 // Note: shouldn't this just skip recursion? 66 if (e.isSymlink) 67 return; 68 } 69 70 if (pattern && !globMatch(e.baseName, pattern)) 71 return; 72 73 static if (recursive) 74 { 75 if (e.entryIsDir) 76 { 77 // Note: why exclude directories from results? 78 e.recurse(); 79 return; 80 } 81 } 82 83 result ~= e.fullName; 84 })(pathname); 85 return result; 86 } 87 88 // ************************************************************************ 89 90 version (Windows) 91 { 92 mixin(importWin32!(q{winnt}, null, q{WCHAR})); 93 mixin(importWin32!(q{winbase}, null, q{WIN32_FIND_DATAW})); 94 } 95 96 /// The OS's "native" filesystem character type (private in Phobos). 97 version (Windows) 98 alias FSChar = WCHAR; 99 else version (Posix) 100 alias FSChar = char; 101 else 102 static assert(0); 103 104 /// Reads a time field from a stat_t with full precision (private in Phobos). 105 SysTime statTimeToStdTime(string which)(ref const stat_t statbuf) 106 { 107 auto unixTime = mixin(`statbuf.st_` ~ which ~ `time`); 108 auto stdTime = unixTimeToStdTime(unixTime); 109 110 static if (is(typeof(mixin(`statbuf.st_` ~ which ~ `tim`)))) 111 stdTime += mixin(`statbuf.st_` ~ which ~ `tim.tv_nsec`) / 100; 112 else 113 static if (is(typeof(mixin(`statbuf.st_` ~ which ~ `timensec`)))) 114 stdTime += mixin(`statbuf.st_` ~ which ~ `timensec`) / 100; 115 else 116 static if (is(typeof(mixin(`statbuf.st_` ~ which ~ `time_nsec`)))) 117 stdTime += mixin(`statbuf.st_` ~ which ~ `time_nsec`) / 100; 118 else 119 static if (is(typeof(mixin(`statbuf.__st_` ~ which ~ `timensec`)))) 120 stdTime += mixin(`statbuf.__st_` ~ which ~ `timensec`) / 100; 121 122 return SysTime(stdTime); 123 } 124 125 version (OSX) 126 version = Darwin; 127 else version (iOS) 128 version = Darwin; 129 else version (TVOS) 130 version = Darwin; 131 else version (WatchOS) 132 version = Darwin; 133 134 private 135 version (Posix) 136 { 137 // TODO: upstream into Druntime 138 extern (C) 139 { 140 int dirfd(DIR *dirp) pure nothrow @nogc; 141 int openat(int fd, const char *path, int oflag, ...) nothrow @nogc; 142 143 version (OSX) 144 { 145 version (AArch64) 146 enum INODE64Suffix = ""; 147 else 148 enum INODE64Suffix = "$INODE64"; 149 } 150 else 151 enum INODE64Suffix = ""; 152 153 pragma(mangle, "fstatat" ~ INODE64Suffix) 154 int fstatat(int fd, const char *path, stat_t *buf, int flag) nothrow @nogc; 155 156 pragma(mangle, "fdopendir" ~ INODE64Suffix) 157 DIR *fdopendir(int fd) nothrow @nogc; 158 } 159 version (linux) 160 { 161 enum AT_SYMLINK_NOFOLLOW = 0x100; 162 enum O_DIRECTORY = 0x10000; 163 } 164 version (Darwin) 165 { 166 enum AT_SYMLINK_NOFOLLOW = 0x20; 167 enum O_DIRECTORY = 0x100000; 168 } 169 version (FreeBSD) 170 { 171 enum AT_SYMLINK_NOFOLLOW = 0x200; 172 enum O_DIRECTORY = 0x20000; 173 } 174 } 175 176 import ae.utils.range : nullTerminatedPtrRange; 177 178 // https://issues.dlang.org/show_bug.cgi?id=7016 179 version (Windows) static import ae.sys.windows.misc; 180 181 /** 182 Fast templated directory iterator 183 184 Example: 185 --- 186 string[] entries; 187 listDir!((e) { 188 entries ~= e.fullName.relPath(tmpDir); 189 if (e.entryIsDir) 190 e.recurse(); 191 })(tmpDir); 192 --- 193 194 If called with `Yes.includeRoot`, then the callback is 195 first called with the argument, even if it's not a directory. 196 */ 197 template listDir(alias handler, Flag!q{includeRoot} includeRoot = No.includeRoot) 198 { 199 private: // (This is an eponymous template, so this is to aid documentation generators.) 200 201 // Data shared for the entire iteration (all Entry instances). 202 /*non-static*/ struct Context 203 { 204 // Tether to handler alias context 205 void callHandler(Args...)(Entry* e, auto ref Args args) { handler(e, args); } 206 207 // Set when .stop() is called on an entry. 208 bool timeToStop = false; 209 210 // Reusable buffer used for the full path. 211 FSChar[] pathBuf; 212 } 213 214 /// A pointer to this type will be passed to the `listDir` predicate. 215 public static struct Entry 216 { 217 version (Posix) 218 { 219 private const(char)* name; /// Name relative to `dirFD`. 220 dirent* ent; /// POSIX `dirent`. May be null with `Yes.includeRoot`. 221 222 private stat_t[enumLength!StatTarget] statBuf; 223 224 /// Result of `stat` call. 225 /// Other values are the same as `errno`. 226 enum StatResult : int 227 { 228 noInfo = 0, /// Not called yet. 229 statOK = int.max, /// All OK 230 unknownError = int.min, /// `errno` returned 0 or `int.max` 231 } 232 233 int dirFD; /// POSIX directory file descriptor. 234 } 235 version (Windows) 236 { 237 WIN32_FIND_DATAW findData; /// Windows `WIN32_FIND_DATAW`. 238 } 239 240 // Cached values. 241 // Cleared (memset to 0) for every directory entry. 242 struct Data 243 { 244 const(FSChar)[] baseNameFS; // Cached base name in FSChars. 245 string baseName; // Cached base name in D chars. 246 string fullName; // Cached full name in D chars. 247 248 // Position within context.pathBuf of where the full name for this Entry ends. 249 Nullable!size_t pathTailPos; 250 251 version (Posix) 252 { 253 StatResult[enumLength!StatTarget] statResult; // Cached `stat` results. 254 } 255 } 256 Data data; 257 258 // Recursion 259 260 Entry* parent; /// 261 private Context* context; 262 263 /// Request recursion on the current `entry`. 264 version (Posix) 265 { 266 void recurse(Args...)(auto ref Args args) 267 { 268 import core.sys.posix.fcntl; 269 int flags = O_RDONLY; 270 static if (is(typeof(O_DIRECTORY))) 271 flags |= O_DIRECTORY; 272 auto fd = openat(dirFD, *name ? name : ".", flags); 273 errnoEnforce(fd >= 0, 274 "Failed to open %s as subdirectory of directory %s" 275 .format(this.baseNameFS, this.parent.fullName)); 276 auto subdir = fdopendir(fd); 277 errnoEnforce(subdir, 278 "Failed to open subdirectory %s of directory %s as directory" 279 .format(this.baseNameFS, this.parent.fullName)); 280 scan(subdir, fd, &this, args); 281 } 282 } 283 else 284 version (Windows) 285 { 286 void recurse(Args...)(auto ref Args args) 287 { 288 needFullPath(); 289 appendString(context.pathBuf, 290 data.pathTailPos.get(), "\\*.*\0"w); 291 scan(&this, false, args); 292 } 293 } 294 295 /// Stop iteration. 296 void stop() { context.timeToStop = true; } 297 298 // Name 299 300 /// Returns a pointer to the base file name, as a 301 /// null-terminated string, in the operating system's 302 /// character type. Fastest. 303 const(FSChar)* baseNameFSPtr() nothrow @nogc 304 { 305 version (Posix) return name; 306 version (Windows) 307 { 308 // We don't use findData.cFileName because of the special case for "." and "..". 309 needFullPath(); 310 auto startPos = parent ? parent.data.pathTailPos.get() : 0; 311 if (startPos) 312 startPos++; 313 return context.pathBuf.ptr + startPos; 314 } 315 } 316 317 // Bounded variant of std.string.fromStringz for static arrays. 318 private static T[] fromStringz(T, size_t n)(ref T[n] buf) 319 { 320 foreach (i, c; buf) 321 if (!c) 322 return buf[0 .. i]; 323 // This should only happen in case of an OS / libc bug. 324 assert(false, "File name buffer is not null-terminated"); 325 } 326 327 /// Returns the base file name, as a D character array, in the 328 /// operating system's character type. Fast. 329 const(FSChar)[] baseNameFS() nothrow @nogc 330 { 331 if (!data.baseNameFS) 332 { 333 version (Posix) data.baseNameFS = .fromStringz(name); 334 version (Windows) 335 { 336 // We don't use findData.cFileName because of the special case for "." and "..". 337 needFullPath(); 338 auto startPos = parent ? parent.data.pathTailPos.get() : 0; 339 if (startPos) 340 startPos++; 341 data.baseNameFS = context.pathBuf[startPos .. this.data.pathTailPos.get()]; 342 } 343 } 344 return data.baseNameFS; 345 } 346 347 /// Returns the base file name as a D string. Allocates. 348 string baseName() // allocates 349 { 350 if (!data.baseName) 351 data.baseName = baseNameFS.to!string; 352 return data.baseName; 353 } 354 355 private void needFullPath() nothrow @nogc 356 { 357 if (data.pathTailPos.isNull) 358 { 359 version (Posix) 360 parent.needFullPath(); 361 version (Windows) 362 { 363 // directory separator was added during recursion 364 auto startPos = parent.data.pathTailPos.get(); 365 if (startPos) 366 startPos++; 367 auto baseNamePtr = findData.cFileName.ptr; 368 } 369 version (Posix) 370 { 371 immutable FSChar[] separator = "/"; 372 auto startPos = parent.data.pathTailPos.get() 373 ? appendString(context.pathBuf, 374 parent.data.pathTailPos.get(), separator) 375 : 0; 376 auto baseNamePtr = this.baseNameFSPtr; 377 } 378 data.pathTailPos = appendString(context.pathBuf, 379 startPos, 380 baseNamePtr.nullTerminatedPtrRange 381 ); 382 } 383 } 384 385 /// Returns the full file name, as a D character array, in the 386 /// operating system's character type. Fast. 387 const(FSChar)[] fullNameFS() nothrow @nogc // fast 388 { 389 needFullPath(); 390 return context.pathBuf[0 .. data.pathTailPos.get()]; 391 } 392 393 /// Returns the full file name as a D string. Allocates. 394 string fullName() // allocates 395 { 396 if (!data.fullName) 397 data.fullName = fullNameFS.to!string; 398 return data.fullName; 399 } 400 401 // Attributes 402 403 version (Posix) 404 { 405 /// We can stat two different things on POSIX: the directory entry itself, 406 /// or the link target (if the directory entry is a symbolic link). 407 enum StatTarget 408 { 409 dirEntry, /// do not dereference (lstat) 410 linkTarget, /// dereference 411 } 412 413 private bool tryStat(StatTarget target)() nothrow @nogc 414 { 415 if (data.statResult[target] == StatResult.noInfo) 416 { 417 // If we already did the other kind of stat, can we reuse its result? 418 if (data.statResult[1 - target] != StatResult.noInfo) 419 { 420 // Yes, if we know this isn't a link from the directory entry. 421 static if (__traits(compiles, ent.d_type)) 422 if (ent && ent.d_type != DT_UNKNOWN && ent.d_type != DT_LNK) 423 goto reuse; 424 // Yes, if we already found out this isn't a link from an lstat call. 425 static if (target == StatTarget.linkTarget) 426 if (data.statResult[StatTarget.dirEntry] == StatResult.statOK 427 && (statBuf[StatTarget.dirEntry].st_mode & S_IFMT) != S_IFLNK) 428 goto reuse; 429 } 430 431 if (false) 432 { 433 reuse: 434 statBuf[target] = statBuf[1 - target]; 435 data.statResult[target] = data.statResult[1 - target]; 436 } 437 else 438 { 439 int flags = target == StatTarget.dirEntry ? AT_SYMLINK_NOFOLLOW : 0; 440 auto res = fstatat(dirFD, *name ? name : ".", &statBuf[target], flags); 441 if (res) 442 { 443 auto error = errno; 444 data.statResult[target] = cast(StatResult)error; 445 if (error == StatResult.noInfo || error == StatResult.statOK) 446 data.statResult[target] = StatResult.unknownError; // unknown error? 447 } 448 else 449 data.statResult[target] = StatResult.statOK; // no error 450 } 451 } 452 return data.statResult[target] == StatResult.statOK; 453 } 454 455 private ErrnoException statError(StatTarget target)() 456 { 457 errno = data.statResult[target]; 458 return new ErrnoException("Failed to stat " ~ 459 (target == StatTarget.linkTarget ? "link target" : "directory entry") ~ 460 ": " ~ fullName); 461 } 462 463 /// Return the result of `stat` / `lstat` (depending on `target`) 464 /// for this `Entry`, performing it first if necessary. 465 stat_t* needStat(StatTarget target)() 466 { 467 if (!tryStat!target) 468 throw statError!target(); 469 return &statBuf[target]; 470 } 471 472 // Check if this is an object of the given type. 473 private bool deIsType(typeof(DT_REG) dType, typeof(S_IFREG) statType) 474 { 475 static if (__traits(compiles, ent.d_type)) 476 if (ent && ent.d_type != DT_UNKNOWN) 477 return ent.d_type == dType; 478 479 return (needStat!(StatTarget.dirEntry)().st_mode & S_IFMT) == statType; 480 } 481 482 /// Returns true if this is a symlink. 483 @property bool isSymlink() 484 { 485 return deIsType(DT_LNK, S_IFLNK); 486 } 487 488 /// Returns true if this is a file (but not if it's a symlink pointing to one). 489 @property bool entryIsFile() 490 { 491 return deIsType(DT_REG, S_IFREG); 492 } 493 494 /// Returns true if this is a directory. 495 /// You probably want to use this one to decide whether to recurse. 496 @property bool entryIsDir() 497 { 498 return deIsType(DT_DIR, S_IFDIR); 499 } 500 501 // Check if this is an object of the given type, or a link pointing to one. 502 private bool ltIsType(typeof(DT_REG) dType, typeof(S_IFREG) statType) 503 { 504 static if (__traits(compiles, ent.d_type)) 505 if (ent && ent.d_type != DT_UNKNOWN && ent.d_type != DT_LNK) 506 return ent.d_type == dType; 507 508 if (tryStat!(StatTarget.linkTarget)()) 509 return (statBuf[StatTarget.linkTarget].st_mode & S_IFMT) == statType; 510 511 if (isSymlink()) // broken symlink? 512 return false; // a broken symlink does not point at anything. 513 514 throw statError!(StatTarget.linkTarget)(); 515 } 516 517 /// Returns true if this is a file, or a link pointing to one. 518 @property bool isFile() 519 { 520 return ltIsType(DT_REG, S_IFREG); 521 } 522 523 /// Returns true if this is a directory, or a link pointing to one. 524 @property bool isDir() 525 { 526 return ltIsType(DT_DIR, S_IFDIR); 527 } 528 529 /// Returns the raw POSIX attributes of this directory entry, 530 /// or the link target if this directory entry is a symlink. 531 @property uint attributes() 532 { 533 return needStat!(StatTarget.linkTarget)().st_mode; 534 } 535 536 /// Returns the raw POSIX attributes of this directory entry. 537 @property uint linkAttributes() 538 { 539 return needStat!(StatTarget.dirEntry)().st_mode; 540 } 541 542 // Other attributes 543 544 /// Returns the "c" time of this directory entry, 545 /// or the link target if this directory entry is a symlink. 546 @property SysTime timeStatusChanged() 547 { 548 return statTimeToStdTime!"c"(*needStat!(StatTarget.linkTarget)()); 549 } 550 551 /// Returns the "a" time of this directory entry, 552 /// or the link target if this directory entry is a symlink. 553 @property SysTime timeLastAccessed() 554 { 555 return statTimeToStdTime!"a"(*needStat!(StatTarget.linkTarget)()); 556 } 557 558 /// Returns the "m" time of this directory entry, 559 /// or the link target if this directory entry is a symlink. 560 @property SysTime timeLastModified() 561 { 562 return statTimeToStdTime!"m"(*needStat!(StatTarget.linkTarget)()); 563 } 564 565 /// Returns the "birth" time of this directory entry, 566 /// or the link target if this directory entry is a symlink. 567 static if (is(typeof(&statTimeToStdTime!"birth"))) 568 @property SysTime timeCreated() 569 { 570 return statTimeToStdTime!"birth"(*needStat!(StatTarget.linkTarget)()); 571 } 572 573 /// Returns the size in bytes of this directory entry, 574 /// or the link target if this directory entry is a symlink. 575 @property ulong size() 576 { 577 return needStat!(StatTarget.linkTarget)().st_size; 578 } 579 580 /// Returns the inode number of this directory entry, 581 /// or the link target if this directory entry is a symlink. 582 @property ulong fileID() 583 { 584 static if (__traits(compiles, ent.d_ino)) 585 if (ent) 586 return ent.d_ino; 587 return needStat!(StatTarget.linkTarget)().st_ino; 588 } 589 } 590 591 version (Windows) 592 { 593 /// Returns true if this is a directory, or a reparse point pointing to one. 594 @property bool isDir() const pure nothrow 595 { 596 return (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; 597 } 598 599 /// Returns true if this is a file, or a reparse point pointing to one. 600 @property bool isFile() const pure nothrow 601 { 602 return !isDir; 603 } 604 605 /// Returns true if this is a reparse point. 606 @property bool isSymlink() const pure nothrow 607 { 608 return (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; 609 } 610 611 /// Returns true if this is a file (but not if it's a reparse point pointing to one). 612 @property bool entryIsFile() const pure nothrow 613 { 614 return isFile && !isSymlink; 615 } 616 617 /// Returns true if this is a directory. 618 /// You probably want to use this one to decide whether to recurse. 619 @property bool entryIsDir() const pure nothrow 620 { 621 return isDir && !isSymlink; 622 } 623 624 /// Returns the raw Windows attributes of this directory entry. 625 @property uint attributes() const pure nothrow 626 { 627 return findData.dwFileAttributes; 628 } 629 630 /// Returns the size in bytes of this directory entry. 631 @property ulong size() const pure nothrow 632 { 633 return makeUlong(findData.nFileSizeLow, findData.nFileSizeHigh); 634 } 635 636 /// Returns the creation time of this directory entry. 637 @property SysTime timeCreated() const 638 { 639 return FILETIMEToSysTime(&findData.ftCreationTime); 640 } 641 642 /// Returns the last access time of this directory entry. 643 @property SysTime timeLastAccessed() const 644 { 645 return FILETIMEToSysTime(&findData.ftLastAccessTime); 646 } 647 648 /// Returns the last modification time of this directory entry. 649 @property SysTime timeLastModified() const 650 { 651 return FILETIMEToSysTime(&findData.ftLastWriteTime); 652 } 653 654 /// Returns the 64-bit unique file index of this file. 655 @property ulong fileID() 656 { 657 return getFileID(fullName); 658 } 659 } 660 } 661 662 version (Posix) 663 { 664 // The length of the buffer on the stack. 665 enum initialPathBufLength = 256; 666 667 private static void scan(Args...)(DIR* dir, int dirFD, Entry* parentEntry, auto ref Args args) 668 { 669 Entry entry = void; 670 entry.parent = parentEntry; 671 entry.context = entry.parent.context; 672 entry.dirFD = dirFD; 673 674 scope(exit) closedir(dir); 675 676 dirent* ent; 677 while ((ent = readdir(dir)) != null) 678 { 679 // Apparently happens on some OS X versions. 680 enforce(ent.d_name[0], 681 "Empty dir entry name (OS bug?)"); 682 683 // Skip "." and ".." 684 if (ent.d_name[0] == '.' && ( 685 ent.d_name[1] == 0 || 686 (ent.d_name[1] == '.' && ent.d_name[2] == 0))) 687 continue; 688 689 entry.ent = ent; 690 entry.name = ent.d_name.ptr; 691 entry.data = Entry.Data.init; 692 entry.context.callHandler(&entry, args); 693 if (entry.context.timeToStop) 694 break; 695 } 696 } 697 } 698 699 enum isPath(Path) = (isForwardRange!Path || isSomeString!Path) && 700 isSomeChar!(ElementEncodingType!Path); 701 702 import core.stdc.stdlib : malloc, realloc, free; 703 704 static FSChar[] reallocPathBuf(FSChar[] buf, size_t newLength) nothrow @nogc 705 { 706 if (buf.length == initialPathBufLength) // current buffer is on stack 707 { 708 auto ptr = cast(FSChar*) malloc(newLength * FSChar.sizeof); 709 ptr[0 .. buf.length] = buf[]; 710 return ptr[0 .. newLength]; 711 } 712 else // current buffer on C heap (malloc'd above) 713 { 714 auto ptr = cast(FSChar*) realloc(buf.ptr, newLength * FSChar.sizeof); 715 return ptr[0 .. newLength]; 716 } 717 } 718 719 // Append a string to the buffer, reallocating as necessary. 720 // Returns the new length of the string in the buffer. 721 static size_t appendString(Str)(ref FSChar[] buf, size_t pos, Str str) nothrow @nogc 722 if (isPath!Str) 723 { 724 static if (ElementEncodingType!Str.sizeof == FSChar.sizeof 725 && is(typeof(str.length))) 726 { 727 // No transcoding needed and length known 728 auto remainingSpace = buf.length - pos; 729 if (str.length > remainingSpace) 730 buf = reallocPathBuf(buf, (pos + str.length) * 3 / 2); 731 buf[pos .. pos + str.length] = str[]; 732 pos += str.length; 733 } 734 else 735 { 736 // Need to transcode 737 auto p = buf.ptr + pos; 738 auto bufEnd = buf.ptr + buf.length; 739 foreach (c; byUTF!FSChar(str)) 740 { 741 if (p == bufEnd) // out of room 742 { 743 auto newBuf = reallocPathBuf(buf, buf.length * 3 / 2); 744 745 // Update pointers to point into the new buffer. 746 p = newBuf.ptr + (p - buf.ptr); 747 buf = newBuf; 748 bufEnd = buf.ptr + buf.length; 749 } 750 *p++ = c; 751 } 752 pos = p - buf.ptr; 753 } 754 return pos; 755 } 756 757 version (Windows) 758 { 759 mixin(importWin32!(q{winbase})); 760 import ae.sys.windows.misc : makeUlong; 761 762 // The length of the buffer on the stack. 763 enum initialPathBufLength = MAX_PATH; 764 765 enum FIND_FIRST_EX_LARGE_FETCH = 2; 766 enum FindExInfoBasic = cast(FINDEX_INFO_LEVELS)1; 767 768 static void scan(Args...)(Entry* parentEntry, bool isRoot, auto ref Args args) 769 { 770 Entry entry = void; 771 entry.parent = parentEntry; 772 entry.context = parentEntry.context; 773 774 auto name = entry.context.pathBuf.ptr; 775 HANDLE hFind = FindFirstFileExW( 776 *name ? name : ".", 777 FindExInfoBasic, 778 &entry.findData, 779 FINDEX_SEARCH_OPS.FindExSearchNameMatch, 780 null, 781 FIND_FIRST_EX_LARGE_FETCH, // https://devblogs.microsoft.com/oldnewthing/20131024-00/?p=2843 782 ); 783 if (hFind == INVALID_HANDLE_VALUE) 784 throw new WindowsException(GetLastError(), 785 text("FindFirstFileW: ", name.fromStringz)); 786 scope(exit) FindClose(hFind); 787 do 788 { 789 if (!isRoot) 790 { 791 // Skip "." and ".." 792 auto fn = entry.findData.cFileName.ptr; 793 if (fn[0] == '.' && ( 794 fn[1] == 0 || 795 (fn[1] == '.' && fn[2] == 0))) 796 continue; 797 } 798 799 entry.data = Entry.Data.init; 800 if (isRoot) 801 { 802 // Windows puts the base name of the entry in findData.cFileName, 803 // even when we are addressing it by a different way, such as "." or "..". 804 // However, this breaks the invariant that `iterationPath.buildPath(entryName)` is the iterated item; 805 // therefore, it is not useful to us because we will want to do path construction, 806 // and we want to be consistent across all platforms. 807 808 // The call from listDir below will ensure that the parentEntry's 809 // pathTailPos will point to the base name of the full path; 810 // we then just need to set our end to the end of the string passed to FindFirstFileExW, 811 // thus allowing us to return fake but correct baseName / fullName values. 812 entry.data.pathTailPos = name.fromStringz.length; 813 } 814 815 entry.context.callHandler(&entry, args); 816 if (entry.context.timeToStop) 817 break; 818 } 819 while (FindNextFileW(hFind, &entry.findData)); 820 if (GetLastError() != ERROR_NO_MORE_FILES) 821 throw new WindowsException(GetLastError(), 822 text("FindNextFileW: ", parentEntry.fullNameFS)); 823 } 824 } 825 826 public void listDir(Path, Args...)(Path dirPath, auto ref Args args) 827 if (isPath!Path) 828 { 829 Context context; 830 831 FSChar[initialPathBufLength] pathBufStore = void; 832 context.pathBuf = pathBufStore[]; 833 834 scope (exit) 835 { 836 if (context.pathBuf.length != initialPathBufLength) 837 free(context.pathBuf.ptr); 838 } 839 840 Entry rootEntry; 841 rootEntry.context = &context; 842 843 auto endPos = appendString(context.pathBuf, 0, dirPath); 844 appendString(context.pathBuf, endPos, "\0"); 845 rootEntry.data.pathTailPos = endPos - (endPos > 0 && context.pathBuf[endPos - 1].isDirSeparator() ? 1 : 0); 846 assert(!rootEntry.data.pathTailPos.isNull); 847 848 static if (includeRoot) 849 { 850 version (Posix) 851 { 852 version (OSX) 853 enum AT_FDCWD = -2; 854 else 855 import core.sys.posix.fcntl : AT_FDCWD; 856 857 rootEntry.dirFD = AT_FDCWD; 858 rootEntry.ent = null; 859 auto name = InlineStr(dirPath, '\0'); 860 rootEntry.name = name[].ptr; 861 862 context.callHandler(&rootEntry, args); 863 } 864 else 865 version (Windows) 866 { 867 while (rootEntry.data.pathTailPos.get() && !context.pathBuf[rootEntry.data.pathTailPos.get()].isDirSeparator()) 868 rootEntry.data.pathTailPos.get()--; 869 scan(&rootEntry, true, args); 870 } 871 } 872 else 873 { 874 version (Posix) 875 { 876 auto dir = dirPath.length ? opendir(InlineStr(dirPath, '\0')[].ptr) : opendir("."); 877 checkDir(dir, dirPath); 878 879 scan(dir, dirfd(dir), &rootEntry, args); 880 } 881 else 882 version (Windows) 883 { 884 const WCHAR[] tailString = endPos == 0 || context.pathBuf[endPos - 1].isDirSeparator() ? "*.*\0"w : "\\*.*\0"w; 885 appendString(context.pathBuf, endPos, tailString); 886 887 scan(&rootEntry, false, args); 888 } 889 } 890 } 891 892 // Workaround for https://github.com/ldc-developers/ldc/issues/2960 893 version (Posix) 894 private void checkDir(Path)(DIR* dir, auto ref Path dirPath) 895 { 896 errnoEnforce(dir, "Failed to open directory " ~ dirPath); 897 } 898 } 899 900 // Helper for listDir 901 private union InlineArr(T, size_t inlineSize) 902 { 903 private: 904 static assert(inlineSize * T.sizeof > T[].sizeof); 905 alias InlineSize = TypeForBits!(bitsFor(inlineSize)); 906 static assert(inlineSize <= InlineSize.max); 907 908 T[] str; 909 struct 910 { 911 T[inlineSize] inlineBuf; 912 InlineSize inlineLength; 913 } 914 915 public: 916 private enum bool isConstituent(C) = is(typeof(str[0] = C.init)) || is(typeof(str[] = C.init[])); 917 918 /// Construct from any combination of values or slices of values. 919 this(Args...)(auto ref scope Args args) 920 if (allSatisfy!(isConstituent, Args)) 921 { 922 size_t length; 923 foreach (ref arg; args) 924 static if (is(typeof(arg) : T)) 925 length++; 926 else 927 length += arg.length; 928 929 T[] data; 930 if (length > inlineSize) 931 data = this.str = new T[inlineSize]; 932 else 933 { 934 inlineLength = cast(InlineSize)length; 935 data = inlineBuf[0 .. length]; 936 } 937 938 size_t p = 0; 939 foreach (ref arg; args) 940 static if (is(typeof(arg) : T)) 941 data[p++] = arg; 942 else 943 { 944 data[p .. p + arg.length] = arg[]; 945 p += arg.length; 946 } 947 } 948 949 inout(T)[] opSlice() inout 950 { 951 if (inlineLength) 952 return inlineBuf[0 .. inlineLength]; 953 else 954 return str; 955 } 956 957 bool opCast(T : bool)() const { return this !is typeof(this).init; } 958 959 bool opEquals(ref const InlineArr other) const 960 { 961 return this[] == other[]; 962 } 963 } 964 965 private alias InlineStr = InlineArr!(char, 255); // ditto 966 967 unittest 968 { 969 auto tmpDir = deleteme ~ "-dir"; 970 if (tmpDir.exists) tmpDir.removeRecurse(); 971 mkdirRecurse(tmpDir); 972 scope(exit) rmdirRecurse(tmpDir); 973 974 touch(tmpDir ~ "/a"); 975 touch(tmpDir ~ "/b"); 976 mkdir(tmpDir ~ "/c"); 977 touch(tmpDir ~ "/c/1"); 978 touch(tmpDir ~ "/c/2"); 979 980 string[] entries; 981 listDir!((e) { 982 assert(equal(e.fullNameFS, e.fullName)); 983 entries ~= e.fullName.relPath(tmpDir); 984 if (e.entryIsDir) 985 e.recurse(); 986 })(tmpDir); 987 988 assert(equal( 989 entries.sort, 990 ["a", "b", "c", "c/1", "c/2"].map!(name => name.replace("/", dirSeparator)), 991 ), text(entries)); 992 993 entries = null; 994 import std.ascii : isDigit; 995 listDir!((e) { 996 entries ~= e.fullName.relPath(tmpDir); 997 if (e.baseNameFS[0].isDigit) 998 e.stop(); 999 else 1000 if (e.entryIsDir) 1001 e.recurse(); 1002 })(tmpDir); 1003 1004 assert(entries.length < 5 && entries[$-1][$-1].isDigit, text(entries)); 1005 1006 // Test Yes.includeRoot 1007 tmpDir.listDir!((e) { 1008 assert(e.fullName == tmpDir, e.fullName ~ " != " ~ tmpDir); 1009 assert(e.entryIsDir); 1010 }, Yes.includeRoot); 1011 1012 // Test Yes.includeRoot with the empty string 1013 "".listDir!((e) { 1014 assert(e.baseName == "", e.baseName ~ ` != ""`); 1015 assert(e.fullName == "", e.fullName ~ ` != ""`); 1016 assert(e.entryIsDir); 1017 }, Yes.includeRoot); 1018 1019 entries = null; 1020 tmpDir.listDir!((e) { 1021 assert(equal(e.fullNameFS, e.fullName)); 1022 entries ~= e.fullName.relPath(tmpDir); 1023 if (e.entryIsDir) 1024 e.recurse(); 1025 }, Yes.includeRoot); 1026 1027 assert(equal( 1028 entries.sort, 1029 [".", "a", "b", "c", "c/1", "c/2"].map!(name => name.replace("/", dirSeparator)), 1030 ), text(entries)); 1031 1032 // Additional arguments 1033 size_t maxDepth; 1034 listDir!((e, depth) { 1035 if (depth > maxDepth) 1036 maxDepth = depth; 1037 if (e.entryIsDir) 1038 e.recurse(depth + 1); 1039 }, Yes.includeRoot)(tmpDir, 0); 1040 assert(maxDepth == 2); 1041 1042 // Symlink test 1043 (){ 1044 // Wine's implementation of symlinks/junctions is incomplete 1045 version (Windows) 1046 if (getWineVersion()) 1047 return; 1048 1049 dirLink("c", tmpDir ~ "/d"); 1050 dirLink("x", tmpDir ~ "/e"); 1051 1052 string[] entries; 1053 listDir!((e) { 1054 entries ~= e.fullName.relPath(tmpDir); 1055 if (e.entryIsDir) 1056 e.recurse(); 1057 })(tmpDir); 1058 1059 assert(equal( 1060 entries.sort, 1061 ["a", "b", "c", "c/1", "c/2", "d", "e"].map!(name => name.replace("/", dirSeparator)), 1062 )); 1063 1064 // Recurse into symlinks 1065 1066 entries = null; 1067 listDir!((e) { 1068 entries ~= e.fullName.relPath(tmpDir); 1069 if (e.isDir) 1070 try 1071 e.recurse(); 1072 catch (Exception e) // broken junctions on Windows throw 1073 {} 1074 })(tmpDir); 1075 1076 assert(equal( 1077 entries.sort, 1078 ["a", "b", "c", "c/1", "c/2", "d", "d/1", "d/2", "e"].map!(name => name.replace("/", dirSeparator)), 1079 )); 1080 }(); 1081 } 1082 1083 // ************************************************************************ 1084 1085 private string buildPath2(string[] segments...) { return segments.length ? buildPath(segments) : null; } 1086 1087 /// Shell-like expansion of ?, * and ** in path components 1088 DirEntry[] fileList(string pattern) 1089 { 1090 auto components = cast(string[])array(pathSplitter(pattern)); 1091 foreach (i, component; components[0..$-1]) 1092 if (component.contains("?") || component.contains("*")) // TODO: escape? 1093 { 1094 DirEntry[] expansions; // TODO: filter range instead? 1095 auto dir = buildPath2(components[0..i]); 1096 if (component == "**") 1097 expansions = array(dirEntries(dir, SpanMode.depth)); 1098 else 1099 expansions = array(dirEntries(dir, component, SpanMode.shallow)); 1100 1101 DirEntry[] result; 1102 foreach (expansion; expansions) 1103 if (expansion.isDir()) 1104 result ~= fileList(buildPath(expansion.name ~ components[i+1..$])); 1105 return result; 1106 } 1107 1108 auto dir = buildPath2(components[0..$-1]); 1109 if (!dir || exists(dir)) 1110 return array(dirEntries(dir, components[$-1], SpanMode.shallow)); 1111 else 1112 return null; 1113 } 1114 1115 /// ditto 1116 DirEntry[] fileList(string pattern0, string[] patterns...) 1117 { 1118 DirEntry[] result; 1119 foreach (pattern; [pattern0] ~ patterns) 1120 result ~= fileList(pattern); 1121 return result; 1122 } 1123 1124 /// ditto 1125 deprecated string[] fastFileList(string pattern) 1126 { 1127 auto components = cast(string[])array(pathSplitter(pattern)); 1128 foreach (i, component; components[0..$-1]) 1129 if (component.contains("?") || component.contains("*")) // TODO: escape? 1130 { 1131 string[] expansions; // TODO: filter range instead? 1132 auto dir = buildPath2(components[0..i]); 1133 if (component == "**") 1134 expansions = fastListDir!true(dir); 1135 else 1136 expansions = fastListDir(dir, component); 1137 1138 string[] result; 1139 foreach (expansion; expansions) 1140 if (expansion.isDir()) 1141 result ~= fastFileList(buildPath(expansion ~ components[i+1..$])); 1142 return result; 1143 } 1144 1145 auto dir = buildPath2(components[0..$-1]); 1146 if (!dir || exists(dir)) 1147 return fastListDir(dir, components[$-1]); 1148 else 1149 return null; 1150 } 1151 1152 /// ditto 1153 deprecated string[] fastFileList(string pattern0, string[] patterns...) 1154 { 1155 string[] result; 1156 foreach (pattern; [pattern0] ~ patterns) 1157 result ~= fastFileList(pattern); 1158 return result; 1159 } 1160 1161 // ************************************************************************ 1162 1163 import std.datetime; 1164 import std.exception; 1165 1166 deprecated SysTime getMTime(string name) 1167 { 1168 return timeLastModified(name); 1169 } 1170 1171 /// Return true if we can open this path for reading as a file. 1172 bool isReadableFile(string path) 1173 { 1174 try 1175 File(path, "rb"); 1176 catch (Exception e) 1177 return false; 1178 return true; 1179 } 1180 1181 /// If target exists, update its modification time; 1182 /// otherwise create it as an empty file. 1183 void touch(in char[] target) 1184 { 1185 File(target, "ab"); 1186 } 1187 1188 /// Returns true if the target file doesn't exist, 1189 /// or source is newer than the target. 1190 bool newerThan(string source, string target) 1191 { 1192 if (!target.exists) 1193 return true; 1194 return source.timeLastModified() > target.timeLastModified(); 1195 } 1196 1197 /// Returns true if the target file doesn't exist, 1198 /// or any of the sources are newer than the target. 1199 bool anyNewerThan(string[] sources, string target) 1200 { 1201 if (!target.exists) 1202 return true; 1203 auto targetTime = target.timeLastModified(); 1204 return sources.any!(source => source.timeLastModified() > targetTime)(); 1205 } 1206 1207 version (Posix) 1208 { 1209 import core.sys.posix.sys.stat; 1210 import core.sys.posix.unistd; 1211 1212 /// Get the ID of the user owning this file. 1213 int getOwner(string fn) 1214 { 1215 stat_t s; 1216 errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn); 1217 return s.st_uid; 1218 } 1219 1220 /// Get the ID of the group owning this file. 1221 int getGroup(string fn) 1222 { 1223 stat_t s; 1224 errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn); 1225 return s.st_gid; 1226 } 1227 1228 /// Set the owner user and group of this file. 1229 void setOwner(string fn, int uid, int gid) 1230 { 1231 errnoEnforce(chown(toStringz(fn), uid, gid) == 0, "chown: " ~ fn); 1232 } 1233 } 1234 1235 /// Try to rename; copy/delete if rename fails 1236 void move(string src, string dst) 1237 { 1238 try 1239 src.rename(dst); 1240 catch (Exception e) 1241 { 1242 atomicCopy(src, dst); 1243 src.remove(); 1244 } 1245 } 1246 1247 /// Make sure that the given directory exists 1248 /// (and create parent directories as necessary). 1249 void ensureDirExists(string path) 1250 { 1251 if (!path.exists) 1252 path.mkdirRecurse(); 1253 } 1254 1255 /// Make sure that the path to the given file name 1256 /// exists (and create directories as necessary). 1257 void ensurePathExists(string fn) 1258 { 1259 fn.dirName.ensureDirExists(); 1260 } 1261 1262 /// Combines `ensurePathExists` and `touch`. 1263 void ensureFileExists(string fn) 1264 { 1265 fn.ensurePathExists(); 1266 if (!fn.exists) 1267 fn.touch(); 1268 } 1269 1270 static import core.stdc.errno; 1271 version (Windows) 1272 { 1273 static import core.sys.windows.winerror; 1274 static import std.windows.syserror; 1275 static import ae.sys.windows.exception; 1276 } 1277 1278 /// Catch common Phobos exception types corresponding to file operations. 1279 bool collectOSError(alias checkCError, alias checkWinError)(scope void delegate() operation) 1280 { 1281 mixin(() { 1282 string code = q{ 1283 try 1284 { 1285 operation(); 1286 return true; 1287 } 1288 catch (FileException e) 1289 { 1290 version (Windows) 1291 bool collect = checkWinError(e.errno); 1292 else 1293 bool collect = checkCError(e.errno); 1294 if (collect) 1295 return false; 1296 else 1297 throw e; 1298 } 1299 catch (ErrnoException e) 1300 { 1301 if (checkCError(e.errno)) 1302 return false; 1303 else 1304 throw e; 1305 } 1306 }; 1307 version(Windows) code ~= q{ 1308 catch (std.windows.syserror.WindowsException e) 1309 { 1310 if (checkWinError(e.code)) 1311 return false; 1312 else 1313 throw e; 1314 } 1315 catch (ae.sys.windows.exception.WindowsException e) 1316 { 1317 if (checkWinError(e.code)) 1318 return false; 1319 else 1320 throw e; 1321 } 1322 }; 1323 return code; 1324 }()); 1325 } 1326 1327 /// Collect a "file not found" error. 1328 alias collectNotFoundError = collectOSError!( 1329 errno => errno == core.stdc.errno.ENOENT, 1330 (code) { version(Windows) return 1331 code == core.sys.windows.winerror.ERROR_FILE_NOT_FOUND || 1332 code == core.sys.windows.winerror.ERROR_PATH_NOT_FOUND; }, 1333 ); 1334 1335 /// 1336 unittest 1337 { 1338 auto fn = deleteme; 1339 if (fn.exists) fn.removeRecurse(); 1340 foreach (dg; [ 1341 { openFile(fn, "rb"); }, 1342 { mkdir(fn.buildPath("b")); }, 1343 { hardLink(fn, fn ~ "2"); }, 1344 ]) 1345 assert(!dg.collectNotFoundError); 1346 } 1347 1348 /// Collect a "file already exists" error. 1349 alias collectFileExistsError = collectOSError!( 1350 errno => errno == core.stdc.errno.EEXIST, 1351 (code) { version(Windows) return 1352 code == core.sys.windows.winerror.ERROR_FILE_EXISTS || 1353 code == core.sys.windows.winerror.ERROR_ALREADY_EXISTS; }, 1354 ); 1355 1356 /// 1357 unittest 1358 { 1359 auto fn = deleteme; 1360 foreach (dg; [ 1361 { mkdir(fn); }, 1362 { openFile(fn, "wxb"); }, 1363 { touch(fn ~ "2"); hardLink(fn ~ "2", fn); }, 1364 ]) 1365 { 1366 if (fn.exists) fn.removeRecurse(); 1367 assert( dg.collectFileExistsError); 1368 assert(!dg.collectFileExistsError); 1369 } 1370 } 1371 1372 import ae.utils.text; 1373 1374 /// Forcibly remove a file or directory. 1375 /// If atomic is true, the entire directory is deleted "atomically" 1376 /// (it is first moved/renamed to another location). 1377 /// On Windows, this will move the file/directory out of the way, 1378 /// if it is in use and cannot be deleted (but can be renamed). 1379 void forceDelete(Flag!"atomic" atomic=Yes.atomic)(string fn, Flag!"recursive" recursive = No.recursive) 1380 { 1381 import std.process : environment; 1382 version(Windows) 1383 { 1384 mixin(importWin32!q{winnt}); 1385 mixin(importWin32!q{winbase}); 1386 } 1387 1388 auto name = fn.baseName(); 1389 fn = fn.absolutePath().longPath(); 1390 1391 version(Windows) 1392 { 1393 auto fnW = toUTF16z(fn); 1394 auto attr = GetFileAttributesW(fnW); 1395 wenforce(attr != INVALID_FILE_ATTRIBUTES, "GetFileAttributes"); 1396 if (attr & FILE_ATTRIBUTE_READONLY) 1397 SetFileAttributesW(fnW, attr & ~FILE_ATTRIBUTE_READONLY).wenforce("SetFileAttributes"); 1398 } 1399 1400 static if (atomic) 1401 { 1402 // To avoid zombifying locked directories, try renaming it first. 1403 // Attempting to delete a locked directory will make it inaccessible. 1404 1405 bool tryMoveTo(string target) 1406 { 1407 target = target.longPath(); 1408 if (target.endsWith(dirSeparator)) 1409 target = target[0..$-1]; 1410 if (target.length && !target.exists) 1411 return false; 1412 1413 string newfn; 1414 do 1415 newfn = format("%s%sdeleted-%s.%s.%s", target, dirSeparator, name, thisProcessID, randomString()); 1416 while (newfn.exists); 1417 1418 version(Windows) 1419 { 1420 auto newfnW = toUTF16z(newfn); 1421 if (!MoveFileW(fnW, newfnW)) 1422 return false; 1423 } 1424 else 1425 { 1426 try 1427 rename(fn, newfn); 1428 catch (FileException e) 1429 return false; 1430 } 1431 1432 fn = newfn; 1433 version(Windows) fnW = newfnW; 1434 return true; 1435 } 1436 1437 void tryMove() 1438 { 1439 auto tmp = environment.get("TEMP"); 1440 if (tmp) 1441 if (tryMoveTo(tmp)) 1442 return; 1443 1444 version(Windows) 1445 string tempDir = fn[0..7]~"Temp"; 1446 else 1447 enum tempDir = "/tmp"; 1448 1449 if (tryMoveTo(tempDir)) 1450 return; 1451 1452 if (tryMoveTo(fn.dirName())) 1453 return; 1454 1455 throw new Exception("Unable to delete " ~ fn ~ " atomically (all rename attempts failed)"); 1456 } 1457 1458 tryMove(); 1459 } 1460 1461 version(Windows) 1462 { 1463 if (attr & FILE_ATTRIBUTE_DIRECTORY) 1464 { 1465 if (recursive && (attr & FILE_ATTRIBUTE_REPARSE_POINT) == 0) 1466 { 1467 foreach (de; fn.dirEntries(SpanMode.shallow)) 1468 forceDelete!(No.atomic)(de.name, Yes.recursive); 1469 } 1470 // Will fail if !recursive and directory is not empty 1471 RemoveDirectoryW(fnW).wenforce("RemoveDirectory"); 1472 } 1473 else 1474 DeleteFileW(fnW).wenforce("DeleteFile"); 1475 } 1476 else 1477 { 1478 if (recursive) 1479 fn.removeRecurse(); 1480 else 1481 if (fn.isDir) 1482 fn.rmdir(); 1483 else 1484 fn.remove(); 1485 } 1486 } 1487 1488 1489 deprecated void forceDelete(bool atomic)(string fn, bool recursive = false) { forceDelete!(cast(Flag!"atomic")atomic)(fn, cast(Flag!"recursive")recursive); } 1490 //deprecated void forceDelete()(string fn, bool recursive) { forceDelete!(Yes.atomic)(fn, cast(Flag!"recursive")recursive); } 1491 1492 deprecated unittest 1493 { 1494 mkdir("testdir"); touch("testdir/b"); forceDelete!(false )("testdir", true); 1495 mkdir("testdir"); touch("testdir/b"); forceDelete!(true )("testdir", true); 1496 } 1497 1498 unittest 1499 { 1500 mkdir("testdir"); touch("testdir/b"); forceDelete ("testdir", Yes.recursive); 1501 mkdir("testdir"); touch("testdir/b"); forceDelete!(No .atomic)("testdir", Yes.recursive); 1502 mkdir("testdir"); touch("testdir/b"); forceDelete!(Yes.atomic)("testdir", Yes.recursive); 1503 } 1504 1505 /// If fn is a directory, delete it recursively. 1506 /// Otherwise, delete the file or symlink fn. 1507 void removeRecurse(string fn) 1508 { 1509 auto attr = fn.getAttributes(); 1510 if (attr.attrIsSymlink) 1511 { 1512 version (Windows) 1513 if (attr.attrIsDir) 1514 fn.rmdir(); 1515 else 1516 fn.remove(); 1517 else 1518 fn.remove(); 1519 } 1520 else 1521 if (attr.attrIsDir) 1522 version (Windows) 1523 fn.forceDelete!(No.atomic)(Yes.recursive); // For read-only files 1524 else 1525 fn.rmdirRecurse(); 1526 else 1527 fn.remove(); 1528 } 1529 1530 /// Create an empty directory, deleting 1531 /// all its contents if it already exists. 1532 void recreateEmptyDirectory()(string dir) 1533 { 1534 if (dir.exists) 1535 dir.forceDelete(Yes.recursive); 1536 mkdir(dir); 1537 } 1538 1539 /// Copy a directory recursively. 1540 void copyRecurse(DirEntry src, string dst) 1541 { 1542 version (Posix) 1543 if (src.isSymlink) 1544 return symlink(dst, readLink(src)); 1545 if (src.isFile) 1546 return copy(src, dst, PreserveAttributes.yes); 1547 dst.mkdir(); 1548 foreach (de; src.dirEntries(SpanMode.shallow)) 1549 copyRecurse(de, dst.buildPath(de.baseName)); 1550 } 1551 void copyRecurse(string src, string dst) { copyRecurse(DirEntry(src), dst); } /// ditto 1552 1553 version (linux) static if (is(typeof(mixin(q{{import core.sys.linux.fs : RENAME_EXCHANGE, RENAME_NOREPLACE, RENAME_WHITEOUT;}})))) 1554 { 1555 private extern(C) int renameat2(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, uint flags); 1556 1557 /// Atomically exchange the given files. 1558 void atomicExchange(string a, string b) 1559 { 1560 import core.sys.posix.fcntl : AT_FDCWD; 1561 import core.sys.linux.fs : RENAME_EXCHANGE, RENAME_NOREPLACE, RENAME_WHITEOUT; 1562 import std.exception : errnoEnforce; 1563 import std.string : toStringz; 1564 import ae.utils.math : eq; 1565 1566 renameat2( 1567 AT_FDCWD, a.toStringz, 1568 AT_FDCWD, b.toStringz, 1569 RENAME_EXCHANGE 1570 ).eq(0).errnoEnforce("renameat2"); 1571 } 1572 } 1573 1574 /// Return true if the given file would be hidden from directory listings. 1575 /// Returns true for files starting with `'.'`, and, on Windows, hidden files. 1576 bool isHidden()(string fn) 1577 { 1578 if (baseName(fn).startsWith(".")) 1579 return true; 1580 version (Windows) 1581 { 1582 mixin(importWin32!q{winnt}); 1583 if (getAttributes(fn) & FILE_ATTRIBUTE_HIDDEN) 1584 return true; 1585 } 1586 return false; 1587 } 1588 1589 /// Return a file's unique ID. 1590 ulong getFileID()(string fn) 1591 { 1592 version (Windows) 1593 { 1594 mixin(importWin32!q{winnt}); 1595 mixin(importWin32!q{winbase}); 1596 1597 auto fnW = toUTF16z(fn); 1598 auto h = CreateFileW(fnW, FILE_READ_ATTRIBUTES, 0, null, OPEN_EXISTING, 0, HANDLE.init); 1599 wenforce(h!=INVALID_HANDLE_VALUE, fn); 1600 scope(exit) CloseHandle(h); 1601 BY_HANDLE_FILE_INFORMATION fi; 1602 GetFileInformationByHandle(h, &fi).wenforce("GetFileInformationByHandle"); 1603 1604 ULARGE_INTEGER li; 1605 li.LowPart = fi.nFileIndexLow; 1606 li.HighPart = fi.nFileIndexHigh; 1607 auto result = li.QuadPart; 1608 enforce(result, "Null file ID"); 1609 return result; 1610 } 1611 else 1612 { 1613 return DirEntry(fn).statBuf.st_ino; 1614 } 1615 } 1616 1617 unittest 1618 { 1619 auto base = deleteme; 1620 touch(base ~ "a"); 1621 scope(exit) remove(base ~ "a"); 1622 hardLink(base ~ "a", base ~ "b"); 1623 scope(exit) remove(base ~ "b"); 1624 touch(base ~ "c"); 1625 scope(exit) remove(base ~ "c"); 1626 assert(getFileID(base ~ "a") == getFileID(base ~ "b")); 1627 assert(getFileID(base ~ "a") != getFileID(base ~ "c")); 1628 } 1629 1630 deprecated alias std.file.getSize getSize2; 1631 1632 /// Using UNC paths bypasses path length limitation when using Windows wide APIs. 1633 string longPath(string s) 1634 { 1635 version (Windows) 1636 { 1637 if (!s.startsWith(`\\`)) 1638 return `\\?\` ~ s.absolutePath().buildNormalizedPath().replace(`/`, `\`); 1639 } 1640 return s; 1641 } 1642 1643 version (Windows) 1644 { 1645 static if (__traits(compiles, { mixin importWin32!q{winnt}; })) 1646 static mixin(importWin32!q{winnt}); 1647 1648 /// Common code for creating Windows reparse points. 1649 private void createReparsePoint(string reparseBufferName, string extraInitialization, string reparseTagName)(in char[] target, in char[] print, in char[] link) 1650 { 1651 mixin(importWin32!q{winbase}); 1652 mixin(importWin32!q{windef}); 1653 mixin(importWin32!q{winioctl}); 1654 1655 enum SYMLINK_FLAG_RELATIVE = 1; 1656 1657 HANDLE hLink = CreateFileW(link.toUTF16z(), GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, null); 1658 wenforce(hLink && hLink != INVALID_HANDLE_VALUE, "CreateFileW"); 1659 scope(exit) CloseHandle(hLink); 1660 1661 enum pathOffset = 1662 mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName) .offsetof + 1663 mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)._PathBuffer.offsetof; 1664 1665 auto targetW = target.toUTF16(); 1666 auto printW = print .toUTF16(); 1667 1668 // Despite MSDN, two NUL-terminating characters are needed, one for each string. 1669 1670 auto pathBufferSize = targetW.length + 1 + printW.length + 1; // in chars 1671 auto buf = new ubyte[pathOffset + pathBufferSize * WCHAR.sizeof]; 1672 auto r = cast(REPARSE_DATA_BUFFER*)buf.ptr; 1673 1674 r.ReparseTag = mixin(reparseTagName); 1675 r.ReparseDataLength = to!WORD(buf.length - mixin(q{r..} ~ reparseBufferName).offsetof); 1676 1677 auto pathBuffer = mixin(q{r..} ~ reparseBufferName).PathBuffer; 1678 auto p = pathBuffer; 1679 1680 mixin(q{r..} ~ reparseBufferName).SubstituteNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof); 1681 mixin(q{r..} ~ reparseBufferName).SubstituteNameLength = to!WORD(targetW.length * WCHAR.sizeof); 1682 p[0..targetW.length] = targetW; 1683 p += targetW.length; 1684 *p++ = 0; 1685 1686 mixin(q{r..} ~ reparseBufferName).PrintNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof); 1687 mixin(q{r..} ~ reparseBufferName).PrintNameLength = to!WORD(printW .length * WCHAR.sizeof); 1688 p[0..printW.length] = printW; 1689 p += printW.length; 1690 *p++ = 0; 1691 1692 assert(p-pathBuffer == pathBufferSize); 1693 1694 mixin(extraInitialization); 1695 1696 DWORD dwRet; // Needed despite MSDN 1697 DeviceIoControl(hLink, FSCTL_SET_REPARSE_POINT, buf.ptr, buf.length.to!DWORD(), null, 0, &dwRet, null).wenforce("DeviceIoControl"); 1698 } 1699 1700 /// Attempt to acquire the specified privilege. 1701 void acquirePrivilege(S)(S name) 1702 { 1703 mixin(importWin32!q{winbase}); 1704 mixin(importWin32!q{windef}); 1705 1706 import ae.sys.windows; 1707 1708 HANDLE hToken = null; 1709 wenforce(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)); 1710 scope(exit) CloseHandle(hToken); 1711 1712 TOKEN_PRIVILEGES tp; 1713 wenforce(LookupPrivilegeValue(null, name.toUTF16z(), &tp.Privileges[0].Luid), "LookupPrivilegeValue"); 1714 1715 tp.PrivilegeCount = 1; 1716 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 1717 wenforce(AdjustTokenPrivileges(hToken, FALSE, &tp, cast(DWORD)TOKEN_PRIVILEGES.sizeof, null, null), "AdjustTokenPrivileges"); 1718 } 1719 1720 /// Link a directory. 1721 /// Uses symlinks on POSIX, and directory junctions on Windows. 1722 void dirLink()(in char[] original, in char[] link) 1723 { 1724 mkdir(link); 1725 scope(failure) rmdir(link); 1726 1727 auto target = `\??\` ~ (cast(string)original).absolutePath((cast(string)link.dirName).absolutePath).buildNormalizedPath; 1728 if (target[$-1] != '\\') 1729 target ~= '\\'; 1730 1731 createReparsePoint!(q{MountPointReparseBuffer}, q{}, q{IO_REPARSE_TAG_MOUNT_POINT})(target, null, link); 1732 } 1733 1734 /// Windows implementation of `std.file.symlink`. 1735 void symlink()(in char[] original, in char[] link) 1736 { 1737 mixin(importWin32!q{winnt}); 1738 1739 acquirePrivilege(SE_CREATE_SYMBOLIC_LINK_NAME); 1740 1741 touch(link); 1742 scope(failure) remove(link); 1743 1744 createReparsePoint!(q{SymbolicLinkReparseBuffer}, q{r.SymbolicLinkReparseBuffer.Flags = link.isAbsolute() ? 0 : SYMLINK_FLAG_RELATIVE;}, q{IO_REPARSE_TAG_SYMLINK})(original, original, link); 1745 } 1746 } 1747 else 1748 alias std.file.symlink dirLink; /// `std.file.symlink` is used to implement `dirLink` on POSIX. 1749 1750 version(Windows) version(unittest) static mixin(importWin32!q{winnt}); 1751 1752 unittest 1753 { 1754 // Wine's implementation of symlinks/junctions is incomplete 1755 version (Windows) 1756 if (getWineVersion()) 1757 return; 1758 1759 mkdir("a"); scope(exit) rmdir("a"[]); 1760 touch("a/f"); scope(exit) remove("a/f"); 1761 dirLink("a", "b"); scope(exit) version(Windows) rmdir("b"); else remove("b"); 1762 //symlink("a/f", "c"); scope(exit) remove("c"); 1763 assert("b".isSymlink()); 1764 //assert("c".isSymlink()); 1765 assert("b/f".exists()); 1766 } 1767 1768 version (Windows) 1769 { 1770 /// Create a hard link. 1771 void hardLink()(string src, string dst) 1772 { 1773 mixin(importWin32!q{w32api}); 1774 1775 static assert(_WIN32_WINNT >= 0x501, "CreateHardLinkW not available for target Windows platform. Specify -version=WindowsXP"); 1776 1777 mixin(importWin32!q{winnt}); 1778 mixin(importWin32!q{winbase}); 1779 1780 wenforce(CreateHardLinkW(toUTF16z(dst), toUTF16z(src), null), "CreateHardLink failed: " ~ src ~ " -> " ~ dst); 1781 } 1782 1783 /// Deletes a file, which might be a read-only hard link 1784 /// (thus, deletes the read-only file/link without affecting other links to it). 1785 void deleteHardLink()(string fn) 1786 { 1787 mixin(importWin32!q{winbase}); 1788 1789 auto fnW = toUTF16z(fn); 1790 1791 DWORD attrs = GetFileAttributesW(fnW); 1792 wenforce(attrs != INVALID_FILE_ATTRIBUTES, "GetFileAttributesW failed: " ~ fn); 1793 1794 if (attrs & FILE_ATTRIBUTE_READONLY) 1795 SetFileAttributesW(fnW, attrs & ~FILE_ATTRIBUTE_READONLY) 1796 .wenforce("SetFileAttributesW failed: " ~ fn); 1797 HANDLE h = CreateFileW(fnW, GENERIC_READ|GENERIC_WRITE, 7, null, OPEN_EXISTING, 1798 FILE_FLAG_DELETE_ON_CLOSE, null); 1799 wenforce(h != INVALID_HANDLE_VALUE, "CreateFileW failed: " ~ fn); 1800 if (attrs & FILE_ATTRIBUTE_READONLY) 1801 SetFileAttributesW(fnW, attrs) 1802 .wenforce("SetFileAttributesW failed: " ~ fn); 1803 CloseHandle(h).wenforce("CloseHandle failed: " ~ fn); 1804 } 1805 } 1806 version (Posix) 1807 { 1808 /// Create a hard link. 1809 void hardLink()(string src, string dst) 1810 { 1811 import core.sys.posix.unistd; 1812 errnoEnforce(link(toUTFz!(const char*)(src), toUTFz!(const char*)(dst)) == 0, "link() failed: " ~ dst); 1813 } 1814 1815 alias deleteHardLink = remove; /// `std.file.remove` is used to implement `deleteHardLink` on POSIX. 1816 } 1817 1818 unittest 1819 { 1820 write("a", "foo"); scope(exit) remove("a"); 1821 hardLink("a", "b"); 1822 assert("b".readText == "foo"); 1823 deleteHardLink("b"); 1824 assert(!"b".exists); 1825 } 1826 1827 version (Posix) 1828 { 1829 /// Resolve the target of the symlink, returning a valid path 1830 /// (i.e. relative to `path`'s base, not the symlink's directory). 1831 string linkTarget(string path) 1832 { 1833 auto target = readLink(path); 1834 // Note: we don't use buildNormalizedPath because "a/b/.." 1835 // may not be the same as "a" on POSIX. 1836 return path.dirName.buildPath(target); 1837 } 1838 1839 /// Ensure that `realPath(path)` exists, creating a file or 1840 /// directory (according to `isFile`) as necessary, as well as any 1841 /// missing directory components or symlink targets (recursively 1842 /// if necessary). 1843 void createLinkTargets(string path, bool isFile) 1844 { 1845 if (path == "/" || path == ".") 1846 return; 1847 createLinkTargets(path.dirName, false); 1848 if (path.exists && path.isSymlink) 1849 createLinkTargets(path.linkTarget, isFile); 1850 else 1851 if (isFile) 1852 path.ensureFileExists(); 1853 else 1854 path.ensureDirExists(); 1855 } 1856 1857 /// Wrapper around the C `realpath` function. 1858 string realPath(string path) 1859 { 1860 // TODO: Windows version 1861 import core.sys.posix.stdlib; 1862 auto p = realpath(toUTFz!(const char*)(path), null); 1863 errnoEnforce(p, "realpath"); 1864 string result = fromStringz(p).idup; 1865 free(p); 1866 return result; 1867 } 1868 } 1869 1870 // /proc/self/mounts parsing 1871 version (linux) 1872 { 1873 /// A parsed line from /proc/self/mounts. 1874 struct MountInfo 1875 { 1876 string spec; /// device path 1877 string file; /// mount path 1878 string vfstype; /// file system 1879 string mntops; /// options 1880 int freq; /// dump flag - always 0 1881 int passno; /// fsck order - always 0 1882 } 1883 1884 private string unescapeMountString(in char[] s) 1885 { 1886 string result; 1887 1888 size_t p = 0; 1889 for (size_t i=0; i+3<s.length;) 1890 { 1891 auto c = s[i]; 1892 if (c == '\\') 1893 { 1894 result ~= s[p..i]; 1895 result ~= to!int(s[i+1..i+4], 8); 1896 i += 4; 1897 p = i; 1898 } 1899 else 1900 i++; 1901 } 1902 result ~= s[p..$]; 1903 return result; 1904 } 1905 1906 unittest 1907 { 1908 assert(unescapeMountString(`a\040b\040c`) == "a b c"); 1909 assert(unescapeMountString(`\040`) == " "); 1910 } 1911 1912 /// Parse a line from /proc/self/mounts. 1913 MountInfo parseMountInfo(const(char)[] line) 1914 { 1915 import std.ascii : isDigit; 1916 import ae.utils.array : skipUntil; 1917 1918 // Trim off " 0 0" from the end 1919 // https://github.com/torvalds/linux/blob/v6.3/fs/proc_namespace.c#L130 1920 foreach (_; 0 .. 2) 1921 { 1922 while (line.length && isDigit(line[$-1])) 1923 line = line[0 .. $-1]; 1924 if (line.length && line[$-1] == ' ') 1925 line = line[0 .. $-1]; 1926 } 1927 1928 return MountInfo( 1929 unescapeMountString(line.skipUntil(" ")), 1930 unescapeMountString(line.skipUntil(" ")), 1931 unescapeMountString(line.skipUntil(" ")), 1932 line.idup, 1933 0, 1934 0, 1935 ); 1936 } 1937 1938 unittest 1939 { 1940 auto mi = parseMountInfo(`drvfs /mnt/c 9p rw,dirsync,noatime,aname=drvfs;path=C:\;uid=1000;gid=1000;symlinkroot=/mnt/,mmap,access=client,msize=262144,trans=virtio 0 0`); 1941 assert(mi == MountInfo("drvfs", "/mnt/c", "9p", `rw,dirsync,noatime,aname=drvfs;path=C:\;uid=1000;gid=1000;symlinkroot=/mnt/,mmap,access=client,msize=262144,trans=virtio`, 0, 0)); 1942 } 1943 1944 /// Returns an iterator of MountInfo structs. 1945 auto getMounts() 1946 { 1947 return File("/proc/self/mounts", "rb").byLine().map!parseMountInfo(); 1948 } 1949 1950 /// Get MountInfo with longest mount point matching path. 1951 /// Returns MountInfo.init if none match. 1952 MountInfo getPathMountInfo(R)(R mounts, string path) 1953 if (isInputRange!R && is(ElementType!R : MountInfo)) 1954 { 1955 path = realPath(path); 1956 size_t bestLength; MountInfo bestInfo; 1957 foreach (ref info; mounts) 1958 { 1959 if (path.pathStartsWith(info.file)) 1960 { 1961 if (bestLength < info.file.length) 1962 { 1963 bestLength = info.file.length; 1964 bestInfo = info; 1965 } 1966 } 1967 } 1968 return bestInfo; 1969 } 1970 1971 MountInfo getPathMountInfo(string path) 1972 { 1973 return getMounts().getPathMountInfo(path); 1974 } 1975 1976 /// Get the name of the filesystem that the given path is mounted under. 1977 /// Returns null if none match. 1978 string getPathFilesystem(string path) 1979 { 1980 return getPathMountInfo(path).vfstype; 1981 } 1982 } 1983 1984 // **************************************************************************** 1985 1986 version (linux) 1987 { 1988 import core.sys.linux.sys.xattr; 1989 import core.stdc.errno; 1990 private alias ENOATTR = ENODATA; 1991 1992 /// AA-like object for accessing a file's extended attributes. 1993 struct XAttrs(Obj, string funPrefix) 1994 { 1995 private Obj obj; 1996 1997 mixin("alias getFun = " ~ funPrefix ~ "getxattr;"); 1998 mixin("alias setFun = " ~ funPrefix ~ "setxattr;"); 1999 mixin("alias removeFun = " ~ funPrefix ~ "removexattr;"); 2000 mixin("alias listFun = " ~ funPrefix ~ "listxattr;"); 2001 2002 /// True if extended attributes are supported on this filesystem. 2003 bool supported() 2004 { 2005 auto size = getFun(obj, "user.\x01", null, 0); 2006 return size >= 0 || errno != EOPNOTSUPP; 2007 } 2008 2009 /// Read an extended attribute. 2010 void[] opIndex(string key) 2011 { 2012 auto cKey = key.toStringz(); 2013 sizediff_t size = 0; 2014 void[] buf; 2015 do 2016 { 2017 buf.length = size; 2018 size = getFun(obj, cKey, buf.ptr, buf.length); 2019 errnoEnforce(size >= 0, __traits(identifier, getFun)); 2020 } while (size != buf.length); 2021 return buf; 2022 } 2023 2024 /// Check if an extended attribute is present. 2025 bool opBinaryRight(string op)(string key) 2026 if (op == "in") 2027 { 2028 auto cKey = key.toStringz(); 2029 auto size = getFun(obj, cKey, null, 0); 2030 if (size >= 0) 2031 return true; 2032 else 2033 if (errno == ENOATTR) 2034 return false; 2035 else 2036 errnoEnforce(false, __traits(identifier, getFun)); 2037 assert(false); 2038 } 2039 2040 /// Write an extended attribute. 2041 void opIndexAssign(const(void)[] value, string key) 2042 { 2043 auto ret = setFun(obj, key.toStringz(), value.ptr, value.length, 0); 2044 errnoEnforce(ret == 0, __traits(identifier, setFun)); 2045 } 2046 2047 /// Delete an extended attribute. 2048 void remove(string key) 2049 { 2050 auto ret = removeFun(obj, key.toStringz()); 2051 errnoEnforce(ret == 0, __traits(identifier, removeFun)); 2052 } 2053 2054 /// Return a list of all extended attribute names. 2055 string[] keys() 2056 { 2057 sizediff_t size = 0; 2058 char[] buf; 2059 do 2060 { 2061 buf.length = size; 2062 size = listFun(obj, buf.ptr, buf.length); 2063 errnoEnforce(size >= 0, __traits(identifier, listFun)); 2064 } while (size != buf.length); 2065 2066 char[][] result; 2067 size_t start; 2068 foreach (p, c; buf) 2069 if (!c) 2070 { 2071 result ~= buf[start..p]; 2072 start = p+1; 2073 } 2074 2075 return cast(string[])result; 2076 } 2077 } 2078 2079 /// Return `XAttrs` for the given path, 2080 /// or the link destination if the path leads to as symbolic link. 2081 auto xAttrs(string path) 2082 { 2083 return XAttrs!(const(char)*, "")(path.toStringz()); 2084 } 2085 2086 /// Return `XAttrs` for the given path. 2087 auto linkXAttrs(string path) 2088 { 2089 return XAttrs!(const(char)*, "l")(path.toStringz()); 2090 } 2091 2092 /// Return `XAttrs` for the given open file. 2093 auto xAttrs(ref const File f) 2094 { 2095 return XAttrs!(int, "f")(f.fileno); 2096 } 2097 2098 /// 2099 unittest 2100 { 2101 if (!xAttrs(".").supported) 2102 { 2103 import std.stdio : stderr; 2104 stderr.writeln("ae.sys.file: xattrs not supported on current filesystem, skipping test."); 2105 return; 2106 } 2107 2108 enum fn = "test.txt"; 2109 std.file.write(fn, "test"); 2110 scope(exit) remove(fn); 2111 2112 auto attrs = xAttrs(fn); 2113 enum key = "user.foo"; 2114 assert(key !in attrs); 2115 assertThrown!ErrnoException(attrs[key]); 2116 assert(attrs.keys == []); 2117 2118 attrs[key] = "bar"; 2119 assert(key in attrs); 2120 assert(attrs[key] == "bar"); 2121 assert(attrs.keys == [key]); 2122 2123 attrs.remove(key); 2124 assert(key !in attrs); 2125 assert(attrs.keys == []); 2126 } 2127 } 2128 2129 // **************************************************************************** 2130 2131 version (Windows) 2132 { 2133 /// Enumerate all hard links to the specified file. 2134 // TODO: Return a range 2135 string[] enumerateHardLinks()(string fn) 2136 { 2137 mixin(importWin32!q{winnt}); 2138 mixin(importWin32!q{winbase}); 2139 2140 alias extern(System) HANDLE function(LPCWSTR lpFileName, DWORD dwFlags, LPDWORD StringLength, PWCHAR LinkName) TFindFirstFileNameW; 2141 alias extern(System) BOOL function(HANDLE hFindStream, LPDWORD StringLength, PWCHAR LinkName) TFindNextFileNameW; 2142 2143 auto kernel32 = GetModuleHandle("kernel32.dll"); 2144 auto FindFirstFileNameW = cast(TFindFirstFileNameW)GetProcAddress(kernel32, "FindFirstFileNameW").wenforce("GetProcAddress(FindFirstFileNameW)"); 2145 auto FindNextFileNameW = cast(TFindNextFileNameW)GetProcAddress(kernel32, "FindNextFileNameW").wenforce("GetProcAddress(FindNextFileNameW)"); 2146 2147 static WCHAR[0x8000] buf; 2148 DWORD len = buf.length; 2149 auto h = FindFirstFileNameW(toUTF16z(fn), 0, &len, buf.ptr); 2150 wenforce(h != INVALID_HANDLE_VALUE, "FindFirstFileNameW"); 2151 scope(exit) FindClose(h); 2152 2153 string[] result; 2154 do 2155 { 2156 enforce(len > 0 && len < buf.length && buf[len-1] == 0, "Bad FindFirst/NextFileNameW result"); 2157 result ~= buf[0..len-1].toUTF8(); 2158 len = buf.length; 2159 auto ok = FindNextFileNameW(h, &len, buf.ptr); 2160 if (!ok && GetLastError() == ERROR_HANDLE_EOF) 2161 break; 2162 wenforce(ok, "FindNextFileNameW"); 2163 } while(true); 2164 return result; 2165 } 2166 } 2167 2168 /// Obtain the hard link count for the given file. 2169 uint hardLinkCount(string fn) 2170 { 2171 version (Windows) 2172 { 2173 // TODO: Optimize (don't transform strings) 2174 return cast(uint)fn.enumerateHardLinks.length; 2175 } 2176 else 2177 { 2178 import core.sys.posix.sys.stat; 2179 2180 stat_t s; 2181 errnoEnforce(stat(fn.toStringz(), &s) == 0, "stat"); 2182 return s.st_nlink.to!uint; 2183 } 2184 } 2185 2186 // https://issues.dlang.org/show_bug.cgi?id=7016 2187 version (unittest) 2188 version (Windows) 2189 import ae.sys.windows.misc : getWineVersion; 2190 2191 unittest 2192 { 2193 // FindFirstFileNameW not implemented in Wine 2194 version (Windows) 2195 if (getWineVersion()) 2196 return; 2197 2198 touch("a.test"); 2199 scope(exit) remove("a.test"); 2200 assert("a.test".hardLinkCount() == 1); 2201 2202 hardLink("a.test", "b.test"); 2203 scope(exit) remove("b.test"); 2204 assert("a.test".hardLinkCount() == 2); 2205 assert("b.test".hardLinkCount() == 2); 2206 2207 version(Windows) 2208 { 2209 auto paths = enumerateHardLinks("a.test"); 2210 assert(paths.length == 2); 2211 paths.sort(); 2212 assert(paths[0].endsWith(`\a.test`), paths[0]); 2213 assert(paths[1].endsWith(`\b.test`)); 2214 } 2215 } 2216 2217 /// Argument-reversed version of `std.file.write`, 2218 /// usable at the end of an UFCS chain. 2219 static if (is(typeof({ import std.stdio : toFile; }))) 2220 { 2221 static import std.stdio; 2222 alias toFile = std.stdio.toFile; 2223 } 2224 else 2225 { 2226 void toFile(const(void)[] data, in char[] name) 2227 { 2228 std.file.write(name, data); 2229 } 2230 } 2231 2232 /// Same as toFile, but accepts void[] and does not conflict with the 2233 /// std.stdio function. 2234 void writeTo(const(void)[] data, in char[] target) 2235 { 2236 std.file.write(target, data); 2237 } 2238 2239 /// Polyfill for Windows fopen implementations with support for UNC 2240 /// paths and the 'x' subspecifier. 2241 File openFile()(string fn, string mode = "rb") 2242 { 2243 File f; 2244 static if (is(typeof(&f.windowsHandleOpen))) 2245 { 2246 import core.sys.windows.windows; 2247 import ae.sys.windows.exception; 2248 2249 string winMode, cMode; 2250 foreach (c; mode) 2251 { 2252 switch (c) 2253 { 2254 case 'r': 2255 case 'w': 2256 case 'a': 2257 case '+': 2258 case 'x': 2259 winMode ~= c; 2260 break; 2261 case 'b': 2262 case 't': 2263 break; 2264 default: 2265 assert(false, "Unknown character in mode"); 2266 } 2267 if (c != 'x') 2268 cMode ~= c; 2269 } 2270 DWORD access, creation; 2271 bool append; 2272 switch (winMode) 2273 { 2274 case "r" : access = GENERIC_READ ; creation = OPEN_EXISTING; break; 2275 case "r+" : access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_EXISTING; break; 2276 case "w" : access = GENERIC_WRITE; creation = CREATE_ALWAYS; break; 2277 case "w+" : access = GENERIC_READ | GENERIC_WRITE; creation = CREATE_ALWAYS; break; 2278 case "a" : access = GENERIC_WRITE; creation = OPEN_ALWAYS ; version (CRuntime_Microsoft) append = true; break; 2279 case "a+" : access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_ALWAYS ; version (CRuntime_Microsoft) assert(false, "MSVCRT can't fdopen with a+"); else break; 2280 case "wx" : access = GENERIC_WRITE; creation = CREATE_NEW ; break; 2281 case "w+x": access = GENERIC_READ | GENERIC_WRITE; creation = CREATE_NEW ; break; 2282 case "ax" : access = GENERIC_WRITE; creation = CREATE_NEW ; version (CRuntime_Microsoft) append = true; break; 2283 case "a+x": access = GENERIC_READ | GENERIC_WRITE; creation = CREATE_NEW ; version (CRuntime_Microsoft) assert(false, "MSVCRT can't fdopen with a+"); else break; 2284 default: assert(false, "Bad file mode: " ~ mode); 2285 } 2286 2287 auto pathW = toUTF16z(longPath(fn)); 2288 auto h = CreateFileW(pathW, access, FILE_SHARE_READ, null, creation, 0, HANDLE.init); 2289 wenforce(h != INVALID_HANDLE_VALUE); 2290 2291 if (append) 2292 h.SetFilePointer(0, null, FILE_END); 2293 2294 f.windowsHandleOpen(h, cMode); 2295 } 2296 else 2297 f.open(fn, mode); 2298 return f; 2299 } 2300 2301 unittest 2302 { 2303 enum Existence { any, mustExist, mustNotExist } 2304 enum Pos { none /* not readable/writable */, start, end, empty } 2305 static struct Behavior 2306 { 2307 Existence existence; 2308 bool truncating; 2309 Pos read, write; 2310 } 2311 2312 void test(string mode, in Behavior expected) 2313 { 2314 static if (isVersion!q{CRuntime_Microsoft} || isVersion!q{OSX}) 2315 if (mode == "a+" || mode == "a+x") 2316 return; 2317 2318 Behavior behavior; 2319 2320 static int counter; 2321 auto fn = text(deleteme, counter++); 2322 2323 collectException(fn.remove()); 2324 bool mustExist = !!collectException(openFile(fn, mode)); 2325 touch(fn); 2326 bool mustNotExist = !!collectException(openFile(fn, mode)); 2327 2328 if (!mustExist) 2329 if (!mustNotExist) 2330 behavior.existence = Existence.any; 2331 else 2332 behavior.existence = Existence.mustNotExist; 2333 else 2334 if (!mustNotExist) 2335 behavior.existence = Existence.mustExist; 2336 else 2337 assert(false, "Can't open file whether it exists or not"); 2338 2339 void create() 2340 { 2341 if (mustNotExist) 2342 collectException(fn.remove()); 2343 else 2344 write(fn, "foo"); 2345 } 2346 2347 create(); 2348 openFile(fn, mode); 2349 behavior.truncating = getSize(fn) == 0; 2350 2351 create(); 2352 { 2353 auto f = openFile(fn, mode); 2354 ubyte[] buf; 2355 if (collectException(f.rawRead(new ubyte[1]), buf)) 2356 { 2357 behavior.read = Pos.none; 2358 // Work around https://issues.dlang.org/show_bug.cgi?id=19751 2359 f.reopen(fn, "w"); 2360 } 2361 else 2362 if (buf.length) 2363 behavior.read = Pos.start; 2364 else 2365 if (f.size) 2366 behavior.read = Pos.end; 2367 else 2368 behavior.read = Pos.empty; 2369 } 2370 2371 create(); 2372 { 2373 string s; 2374 { 2375 auto f = openFile(fn, mode); 2376 if (collectException(f.rawWrite("b"))) 2377 { 2378 s = null; 2379 // Work around https://issues.dlang.org/show_bug.cgi?id=19751 2380 f.reopen(fn, "w"); 2381 } 2382 else 2383 { 2384 f.close(); 2385 s = fn.readText; 2386 } 2387 } 2388 2389 if (s is null) 2390 behavior.write = Pos.none; 2391 else 2392 if (s == "b") 2393 behavior.write = Pos.empty; 2394 else 2395 if (s.endsWith("b")) 2396 behavior.write = Pos.end; 2397 else 2398 if (s.startsWith("b")) 2399 behavior.write = Pos.start; 2400 else 2401 assert(false, "Can't detect write position"); 2402 } 2403 2404 2405 if (behavior != expected) 2406 { 2407 import ae.utils.array : isOneOf; 2408 version (Windows) 2409 if (getWineVersion() && mode.isOneOf("w", "a", "wx", "ax")) 2410 { 2411 // Ignore bug in Wine msvcrt implementation 2412 return; 2413 } 2414 2415 assert(false, text(mode, ": expected ", expected, ", got ", behavior)); 2416 } 2417 } 2418 2419 test("r" , Behavior(Existence.mustExist , false, Pos.start, Pos.none )); 2420 test("r+" , Behavior(Existence.mustExist , false, Pos.start, Pos.start)); 2421 test("w" , Behavior(Existence.any , true , Pos.none , Pos.empty)); 2422 test("w+" , Behavior(Existence.any , true , Pos.empty, Pos.empty)); 2423 test("a" , Behavior(Existence.any , false, Pos.none , Pos.end )); 2424 test("a+" , Behavior(Existence.any , false, Pos.start, Pos.end )); 2425 test("wx" , Behavior(Existence.mustNotExist, true , Pos.none , Pos.empty)); 2426 test("w+x", Behavior(Existence.mustNotExist, true , Pos.empty, Pos.empty)); 2427 test("ax" , Behavior(Existence.mustNotExist, true , Pos.none , Pos.empty)); 2428 test("a+x", Behavior(Existence.mustNotExist, true , Pos.empty, Pos.empty)); 2429 } 2430 2431 private version(Windows) 2432 { 2433 version (CRuntime_Microsoft) 2434 { 2435 alias chsize_size_t = long; 2436 extern(C) int _chsize_s(int fd, chsize_size_t size); 2437 alias chsize = _chsize_s; 2438 } 2439 else 2440 { 2441 import core.stdc.config : c_long; 2442 alias chsize_size_t = c_long; 2443 extern(C) int chsize(int fd, c_long size); 2444 } 2445 } 2446 2447 /// Truncate the given file to the given size. 2448 void truncate(File f, ulong length) 2449 { 2450 f.flush(); 2451 version (Windows) 2452 chsize(f.fileno, length.to!chsize_size_t); 2453 else 2454 ftruncate(f.fileno, length.to!off_t); 2455 } 2456 2457 unittest 2458 { 2459 write("test.txt", "abcde"); 2460 auto f = File("test.txt", "r+b"); 2461 f.write("xyz"); 2462 f.truncate(f.tell); 2463 f.close(); 2464 assert("test.txt".readText == "xyz"); 2465 } 2466 2467 /// Calculate the digest of a file. 2468 auto fileDigest(Digest)(string fn) 2469 { 2470 import std.range.primitives; 2471 Digest context; 2472 context.start(); 2473 put(context, openFile(fn, "rb").byChunk(64 * 1024)); 2474 auto digest = context.finish(); 2475 return digest; 2476 } 2477 2478 /// Calculate the MD5 hash of a file. 2479 template mdFile() 2480 { 2481 import std.digest.md; 2482 alias mdFile = fileDigest!MD5; 2483 } 2484 2485 version (HAVE_WIN32) 2486 unittest 2487 { 2488 import std.digest : toHexString; 2489 write("test.txt", "Hello, world!"); 2490 scope(exit) remove("test.txt"); 2491 assert(mdFile("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 2492 } 2493 2494 /// Calculate the digest of a file, and memoize it. 2495 auto fileDigestCached(Digest)(string fn) 2496 { 2497 static typeof(Digest.init.finish())[ulong] cache; 2498 auto id = getFileID(fn); 2499 auto phash = id in cache; 2500 if (phash) 2501 return *phash; 2502 return cache[id] = fileDigest!Digest(fn); 2503 } 2504 2505 /// Calculate the MD5 hash of a file, and memoize it. 2506 template mdFileCached() 2507 { 2508 import std.digest.md; 2509 alias mdFileCached = fileDigestCached!MD5; 2510 } 2511 2512 version (HAVE_WIN32) 2513 unittest 2514 { 2515 import std.digest : toHexString; 2516 write("test.txt", "Hello, world!"); 2517 scope(exit) remove("test.txt"); 2518 assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 2519 write("test.txt", "Something else"); 2520 assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 2521 } 2522 2523 /// Read a File (which might be a stream) into an array 2524 ubyte[] readFile(File f) 2525 { 2526 import std.range.primitives; 2527 auto result = appender!(ubyte[]); 2528 put(result, f.byChunk(64*1024)); 2529 return result.data; 2530 } 2531 2532 unittest 2533 { 2534 auto s = "0123456789".replicate(10_000); 2535 write("test.txt", s); 2536 scope(exit) remove("test.txt"); 2537 assert(readFile(File("test.txt")) == s); 2538 } 2539 2540 /// Read exactly `buf.length` bytes and return true. 2541 /// On EOF, return false. 2542 bool readExactly(File f, ubyte[] buf) 2543 { 2544 if (!buf.length) 2545 return true; 2546 auto read = f.rawRead(buf); 2547 if (read.length==0) return false; 2548 enforce(read.length == buf.length, "Unexpected end of file"); 2549 return true; 2550 } 2551 2552 private 2553 version (Windows) 2554 { 2555 version (CRuntime_DigitalMars) 2556 extern(C) sizediff_t read(int, void*, size_t); 2557 else 2558 { 2559 extern(C) sizediff_t _read(int, void*, size_t); 2560 alias read = _read; 2561 } 2562 } 2563 else 2564 import core.sys.posix.unistd : read; 2565 2566 /// Like `File.rawRead`, but returns as soon as any data is available. 2567 void[] readPartial(File f, void[] buf) 2568 { 2569 assert(buf.length); 2570 auto numRead = read(f.fileno, buf.ptr, buf.length); 2571 errnoEnforce(numRead >= 0); 2572 return buf[0 .. numRead]; 2573 } 2574 2575 /// Like std.file.readText for non-UTF8 2576 ascii readAscii()(string fileName) 2577 { 2578 return cast(ascii)readFile(openFile(fileName, "rb")); 2579 } 2580 2581 // https://issues.dlang.org/show_bug.cgi?id=7016 2582 version(Posix) static import ae.sys.signals; 2583 2584 /// Start a thread which writes data to f asynchronously. 2585 Thread writeFileAsync(File f, const(void)[] data) 2586 { 2587 static class Writer : Thread 2588 { 2589 File target; 2590 const void[] data; 2591 2592 this(ref File f, const(void)[] data) 2593 { 2594 this.target = f; 2595 this.data = data; 2596 super(&run); 2597 } 2598 2599 void run() 2600 { 2601 version (Posix) 2602 { 2603 import ae.sys.signals; 2604 collectSignal(SIGPIPE, &write); 2605 } 2606 else 2607 write(); 2608 } 2609 2610 void write() 2611 { 2612 target.rawWrite(data); 2613 target.close(); 2614 } 2615 } 2616 2617 auto t = new Writer(f, data); 2618 t.start(); 2619 return t; 2620 } 2621 2622 /// Start a thread which reads data from f asynchronously. 2623 /// Call the returning delegate to obtain the read data, 2624 /// blocking until the read completes. 2625 ubyte[] delegate() readFileAsync(File f) 2626 { 2627 static class Reader : Thread 2628 { 2629 File target; 2630 ubyte[] data; 2631 2632 this(ref File f) 2633 { 2634 this.target = f; 2635 this.data = data; 2636 super(&run); 2637 } 2638 2639 void run() 2640 { 2641 data = readFile(target); 2642 } 2643 } 2644 2645 auto t = new Reader(f); 2646 t.start(); 2647 return { 2648 t.join(); 2649 return t.data; 2650 }; 2651 } 2652 2653 /// Write data to a file, and ensure it gets written to disk 2654 /// before this function returns. 2655 /// Consider using as atomic!syncWrite. 2656 /// See also: syncUpdate 2657 void syncWrite()(string target, const(void)[] data) 2658 { 2659 auto f = File(target, "wb"); 2660 f.rawWrite(data); 2661 version (Windows) 2662 { 2663 mixin(importWin32!q{windows}); 2664 FlushFileBuffers(f.windowsHandle); 2665 } 2666 else 2667 { 2668 import core.sys.posix.unistd; 2669 fsync(f.fileno); 2670 } 2671 f.close(); 2672 } 2673 2674 /// Atomically save data to a file (if the file doesn't exist, 2675 /// or its contents differs). The update operation as a whole 2676 /// is not atomic, only the write is. 2677 void syncUpdate()(string fn, const(void)[] data) 2678 { 2679 if (!fn.exists || fn.read() != data) 2680 atomic!(syncWrite!())(fn, data); 2681 } 2682 2683 version(Windows) import ae.sys.windows.exception; 2684 2685 /// Create a named pipe, and allow interacting with it using a `std.stdio.File`. 2686 struct NamedPipeImpl 2687 { 2688 immutable string fileName; /// 2689 2690 /// Create a named pipe, and reserve a filename. 2691 this()(string name) 2692 { 2693 version(Windows) 2694 { 2695 mixin(importWin32!q{winbase}); 2696 2697 fileName = `\\.\pipe\` ~ name; 2698 auto h = CreateNamedPipeW(fileName.toUTF16z, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE, 10, 4096, 4096, 0, null).wenforce("CreateNamedPipeW"); 2699 f.windowsHandleOpen(h, "wb"); 2700 } 2701 else 2702 { 2703 import core.sys.posix.sys.stat; 2704 2705 fileName = `/tmp/` ~ name ~ `.fifo`; 2706 mkfifo(fileName.toStringz, S_IWUSR | S_IRUSR); 2707 } 2708 } 2709 2710 /// Wait for a peer to open the other end of the pipe. 2711 File connect()() 2712 { 2713 version(Windows) 2714 { 2715 mixin(importWin32!q{winbase}); 2716 mixin(importWin32!q{windef}); 2717 2718 BOOL bSuccess = ConnectNamedPipe(f.windowsHandle, null); 2719 2720 // "If a client connects before the function is called, the function returns zero 2721 // and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client 2722 // connects in the interval between the call to CreateNamedPipe and the call to 2723 // ConnectNamedPipe. In this situation, there is a good connection between client 2724 // and server, even though the function returns zero." 2725 if (!bSuccess) 2726 wenforce(GetLastError() == ERROR_PIPE_CONNECTED, "ConnectNamedPipe"); 2727 2728 return f; 2729 } 2730 else 2731 { 2732 return File(fileName, "w"); 2733 } 2734 } 2735 2736 ~this() 2737 { 2738 version(Windows) 2739 { 2740 // File.~this will take care of cleanup 2741 } 2742 else 2743 fileName.remove(); 2744 } 2745 2746 private: 2747 File f; 2748 } 2749 alias NamedPipe = RefCounted!NamedPipeImpl; /// ditto 2750 2751 import ae.utils.textout : StringBuilder; 2752 2753 /// Avoid std.stdio.File.readln's memory corruption bug 2754 /// https://issues.dlang.org/show_bug.cgi?id=13856 2755 string safeReadln(File f) 2756 { 2757 StringBuilder buf; 2758 char[1] arr; 2759 while (true) 2760 { 2761 auto result = f.rawRead(arr[]); 2762 if (!result.length) 2763 break; 2764 buf.put(result); 2765 if (result[0] == '\x0A') 2766 break; 2767 } 2768 return buf.get(); 2769 } 2770 2771 // **************************************************************************** 2772 2773 /// Change the current directory to the given directory. Does nothing if dir is null. 2774 /// Return a scope guard which, upon destruction, restores the previous directory. 2775 /// Asserts that only one thread has changed the process's current directory at any time. 2776 auto pushd(string dir) 2777 { 2778 import core.atomic; 2779 2780 static int threadCount = 0; 2781 static shared int processCount = 0; 2782 2783 static struct Popd 2784 { 2785 string oldPath; 2786 this(string cwd) { oldPath = cwd; } 2787 ~this() { if (oldPath) pop(); } 2788 @disable this(); 2789 @disable this(this); 2790 2791 void pop() 2792 { 2793 assert(oldPath); 2794 scope(exit) oldPath = null; 2795 chdir(oldPath); 2796 2797 auto newThreadCount = --threadCount; 2798 auto newProcessCount = atomicOp!"-="(processCount, 1); 2799 assert(newThreadCount == newProcessCount); // Shouldn't happen 2800 } 2801 } 2802 2803 string cwd; 2804 if (dir) 2805 { 2806 auto newThreadCount = ++threadCount; 2807 auto newProcessCount = atomicOp!"+="(processCount, 1); 2808 assert(newThreadCount == newProcessCount, "Another thread already has an active pushd"); 2809 2810 cwd = getcwd(); 2811 chdir(dir); 2812 } 2813 return Popd(cwd); 2814 } 2815 2816 // **************************************************************************** 2817 2818 import std.algorithm; 2819 import std.process : thisProcessID; 2820 import std.traits; 2821 import std.typetuple; 2822 import ae.utils.meta; 2823 2824 /// Parameter names that `atomic` assumes 2825 /// indicate a destination file by default. 2826 enum targetParameterNames = "target/to/name/dst"; 2827 2828 /// Wrap an operation which creates a file or directory, 2829 /// so that it is created safely and, for files, atomically 2830 /// (by performing the underlying operation to a temporary 2831 /// location, then renaming the completed file/directory to 2832 /// the actual target location). targetName specifies the name 2833 /// of the parameter containing the target file/directory. 2834 auto atomic(alias impl, string targetName = targetParameterNames)(staticMap!(Unqual, ParameterTypeTuple!impl) args) 2835 { 2836 enum targetIndex = findParameter([ParameterNames!impl], targetName, __traits(identifier, impl)); 2837 return atomic!(impl, targetIndex)(args); 2838 } 2839 2840 /// ditto 2841 auto atomic(alias impl, size_t targetIndex)(staticMap!(Unqual, ParameterTypeTuple!impl) args) 2842 { 2843 // idup for https://issues.dlang.org/show_bug.cgi?id=12503 2844 auto target = args[targetIndex].idup; 2845 auto temp = "%s.%s.%s.temp".format(target, thisProcessID, getCurrentThreadID); 2846 if (temp.exists) temp.removeRecurse(); 2847 scope(success) rename(temp, target); 2848 scope(failure) if (temp.exists) temp.removeRecurse(); 2849 args[targetIndex] = temp; 2850 return impl(args); 2851 } 2852 2853 /// ditto 2854 // Workaround for https://issues.dlang.org/show_bug.cgi?id=12230 2855 // Can't be an overload because of https://issues.dlang.org/show_bug.cgi?id=13374 2856 //R atomicDg(string targetName = "target", R, Args...)(R delegate(Args) impl, staticMap!(Unqual, Args) args) 2857 auto atomicDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args) 2858 { 2859 enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA; 2860 return atomic!(impl, targetIndex)(args); 2861 } 2862 2863 deprecated alias safeUpdate = atomic; 2864 2865 unittest 2866 { 2867 enum fn = "atomic.tmp"; 2868 scope(exit) if (fn.exists) fn.remove(); 2869 2870 atomic!touch(fn); 2871 assert(fn.exists); 2872 fn.remove(); 2873 2874 atomicDg(&touch, fn); 2875 assert(fn.exists); 2876 } 2877 2878 /// Wrap an operation so that it is skipped entirely 2879 /// if the target already exists. Implies atomic. 2880 auto cached(alias impl, string targetName = targetParameterNames)(ParameterTypeTuple!impl args) 2881 { 2882 enum targetIndex = findParameter([ParameterNames!impl], targetName, __traits(identifier, impl)); 2883 auto target = args[targetIndex]; 2884 if (!target.exists) 2885 atomic!(impl, targetIndex)(args); 2886 return target; 2887 } 2888 2889 /// ditto 2890 // Exists due to the same reasons as atomicDg 2891 auto cachedDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args) 2892 { 2893 enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA; 2894 auto target = args[targetIndex]; 2895 if (!target.exists) 2896 atomic!(impl, targetIndex)(args); 2897 return target; 2898 } 2899 2900 deprecated alias obtainUsing = cached; 2901 2902 /// Create a file, or replace an existing file's contents 2903 /// atomically. 2904 /// Note: Consider using atomic!syncWrite or 2905 /// atomic!syncUpdate instead. 2906 alias atomicWrite = atomic!_writeProxy; 2907 deprecated alias safeWrite = atomicWrite; 2908 /*private*/ void _writeProxy(string target, const(void)[] data) 2909 { 2910 std.file.write(target, data); 2911 } 2912 2913 // Work around for https://github.com/D-Programming-Language/phobos/pull/2784#issuecomment-68117241 2914 private void copy2(string source, string target) { std.file.copy(source, target); } 2915 2916 /// Copy a file, or replace an existing file's contents 2917 /// with another file's, atomically. 2918 alias atomic!copy2 atomicCopy; 2919 2920 unittest 2921 { 2922 enum fn = "cached.tmp"; 2923 scope(exit) if (fn.exists) fn.remove(); 2924 2925 cached!touch(fn); 2926 assert(fn.exists); 2927 2928 std.file.write(fn, "test"); 2929 2930 cachedDg!0(&_writeProxy, fn, "test2"); 2931 assert(fn.readText() == "test"); 2932 } 2933 2934 /// Like `symlink`, but replaces it atomically if it already exists. 2935 version (Windows) 2936 void atomicSymlink()(in char[] original, in char[] link) { atomic!(symlink!(), "link"); } 2937 else 2938 alias atomicSymlink = atomic!(symlink!(string, string), "link"); 2939 2940 // **************************************************************************** 2941 2942 /// Composes a function which generates a file name 2943 /// with a function which creates the file. 2944 /// Returns the file name. 2945 template withTarget(alias targetGen, alias fun) 2946 { 2947 auto withTarget(Args...)(auto ref Args args) 2948 { 2949 auto target = targetGen(args); 2950 fun(args, target); 2951 return target; 2952 } 2953 } 2954 2955 /// Two-argument buildPath with reversed arguments. 2956 /// Useful for UFCS chaining. 2957 string prependPath(string target, string path) 2958 { 2959 return buildPath(path, target); 2960 }