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