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