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