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