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.stdio : File; 24 import std.string; 25 import std.typecons; 26 import std.utf; 27 28 import ae.sys.cmd : getCurrentThreadID; 29 30 public import std.typecons : No, Yes; 31 32 alias wcscmp = core.stdc.wchar_.wcscmp; 33 alias wcslen = core.stdc.wchar_.wcslen; 34 35 version(Windows) import ae.sys.windows.imports; 36 37 // ************************************************************************ 38 39 version (Windows) 40 { 41 // Work around std.file overload 42 mixin(importWin32!(q{winnt}, null, q{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT})); 43 } 44 45 // ************************************************************************ 46 47 version(Windows) 48 { 49 string[] fastListDir(bool recursive = false, bool symlinks=false)(string pathname, string pattern = null) 50 { 51 import core.sys.windows.windows; 52 53 static if (recursive) 54 enforce(!pattern, "TODO: recursive fastListDir with pattern"); 55 56 string[] result; 57 string c; 58 HANDLE h; 59 60 c = buildPath(pathname, pattern ? pattern : "*.*"); 61 WIN32_FIND_DATAW fileinfo; 62 63 h = FindFirstFileW(toUTF16z(c), &fileinfo); 64 if (h != INVALID_HANDLE_VALUE) 65 { 66 scope(exit) FindClose(h); 67 68 do 69 { 70 // Skip "." and ".." 71 if (wcscmp(fileinfo.cFileName.ptr, ".") == 0 || 72 wcscmp(fileinfo.cFileName.ptr, "..") == 0) 73 continue; 74 75 static if (!symlinks) 76 { 77 if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) 78 continue; 79 } 80 81 size_t clength = wcslen(fileinfo.cFileName.ptr); 82 string name = std.utf.toUTF8(fileinfo.cFileName[0 .. clength]); 83 string path = buildPath(pathname, name); 84 85 static if (recursive) 86 { 87 if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 88 { 89 result ~= fastListDir!recursive(path); 90 continue; 91 } 92 } 93 94 result ~= path; 95 } while (FindNextFileW(h,&fileinfo) != FALSE); 96 } 97 return result; 98 } 99 } 100 else 101 version (Posix) 102 { 103 private import core.stdc.errno; 104 private import core.sys.posix.dirent; 105 private import core.stdc.string; 106 107 string[] fastListDir(bool recursive=false, bool symlinks=false)(string pathname, string pattern = null) 108 { 109 string[] result; 110 DIR* h; 111 dirent* fdata; 112 113 h = opendir(toStringz(pathname)); 114 if (h) 115 { 116 try 117 { 118 while((fdata = readdir(h)) != null) 119 { 120 // Skip "." and ".." 121 if (!core.stdc..string.strcmp(fdata.d_name.ptr, ".") || 122 !core.stdc..string.strcmp(fdata.d_name.ptr, "..")) 123 continue; 124 125 static if (!symlinks) 126 { 127 if (fdata.d_type == DT_LNK) 128 continue; 129 } 130 131 size_t len = core.stdc..string.strlen(fdata.d_name.ptr); 132 string name = fdata.d_name[0 .. len].idup; 133 if (pattern && !globMatch(name, pattern)) 134 continue; 135 string path = buildPath(pathname, name); 136 137 static if (recursive) 138 { 139 if (fdata.d_type & DT_DIR) 140 { 141 result ~= fastListDir!(recursive, symlinks)(path); 142 continue; 143 } 144 } 145 146 result ~= path; 147 } 148 } 149 finally 150 { 151 closedir(h); 152 } 153 } 154 else 155 { 156 throw new std.file.FileException(pathname, errno); 157 } 158 return result; 159 } 160 } 161 else 162 static assert(0, "TODO"); 163 164 // ************************************************************************ 165 166 string buildPath2(string[] segments...) { return segments.length ? buildPath(segments) : null; } 167 168 /// Shell-like expansion of ?, * and ** in path components 169 DirEntry[] fileList(string pattern) 170 { 171 auto components = cast(string[])array(pathSplitter(pattern)); 172 foreach (i, component; components[0..$-1]) 173 if (component.contains("?") || component.contains("*")) // TODO: escape? 174 { 175 DirEntry[] expansions; // TODO: filter range instead? 176 auto dir = buildPath2(components[0..i]); 177 if (component == "**") 178 expansions = array(dirEntries(dir, SpanMode.depth)); 179 else 180 expansions = array(dirEntries(dir, component, SpanMode.shallow)); 181 182 DirEntry[] result; 183 foreach (expansion; expansions) 184 if (expansion.isDir()) 185 result ~= fileList(buildPath(expansion.name ~ components[i+1..$])); 186 return result; 187 } 188 189 auto dir = buildPath2(components[0..$-1]); 190 if (!dir || exists(dir)) 191 return array(dirEntries(dir, components[$-1], SpanMode.shallow)); 192 else 193 return null; 194 } 195 196 /// ditto 197 DirEntry[] fileList(string pattern0, string[] patterns...) 198 { 199 DirEntry[] result; 200 foreach (pattern; [pattern0] ~ patterns) 201 result ~= fileList(pattern); 202 return result; 203 } 204 205 /// ditto 206 string[] fastFileList(string pattern) 207 { 208 auto components = cast(string[])array(pathSplitter(pattern)); 209 foreach (i, component; components[0..$-1]) 210 if (component.contains("?") || component.contains("*")) // TODO: escape? 211 { 212 string[] expansions; // TODO: filter range instead? 213 auto dir = buildPath2(components[0..i]); 214 if (component == "**") 215 expansions = fastListDir!true(dir); 216 else 217 expansions = fastListDir(dir, component); 218 219 string[] result; 220 foreach (expansion; expansions) 221 if (expansion.isDir()) 222 result ~= fastFileList(buildPath(expansion ~ components[i+1..$])); 223 return result; 224 } 225 226 auto dir = buildPath2(components[0..$-1]); 227 if (!dir || exists(dir)) 228 return fastListDir(dir, components[$-1]); 229 else 230 return null; 231 } 232 233 /// ditto 234 string[] fastFileList(string pattern0, string[] patterns...) 235 { 236 string[] result; 237 foreach (pattern; [pattern0] ~ patterns) 238 result ~= fastFileList(pattern); 239 return result; 240 } 241 242 // ************************************************************************ 243 244 import std.datetime; 245 import std.exception; 246 247 deprecated SysTime getMTime(string name) 248 { 249 return timeLastModified(name); 250 } 251 252 /// If target exists, update its modification time; 253 /// otherwise create it as an empty file. 254 void touch(in char[] target) 255 { 256 if (exists(target)) 257 { 258 auto now = Clock.currTime(); 259 setTimes(target, now, now); 260 } 261 else 262 std.file.write(target, ""); 263 } 264 265 /// Returns true if the target file doesn't exist, 266 /// or source is newer than the target. 267 bool newerThan(string source, string target) 268 { 269 if (!target.exists) 270 return true; 271 return source.timeLastModified() > target.timeLastModified(); 272 } 273 274 /// Returns true if the target file doesn't exist, 275 /// or any of the sources are newer than the target. 276 bool anyNewerThan(string[] sources, string target) 277 { 278 if (!target.exists) 279 return true; 280 auto targetTime = target.timeLastModified(); 281 return sources.any!(source => source.timeLastModified() > targetTime)(); 282 } 283 284 version (Posix) 285 { 286 import core.sys.posix.sys.stat; 287 import core.sys.posix.unistd; 288 289 int getOwner(string fn) 290 { 291 stat_t s; 292 errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn); 293 return s.st_uid; 294 } 295 296 int getGroup(string fn) 297 { 298 stat_t s; 299 errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn); 300 return s.st_gid; 301 } 302 303 void setOwner(string fn, int uid, int gid) 304 { 305 errnoEnforce(chown(toStringz(fn), uid, gid) == 0, "chown: " ~ fn); 306 } 307 } 308 309 /// Try to rename; copy/delete if rename fails 310 void move(string src, string dst) 311 { 312 try 313 src.rename(dst); 314 catch (Exception e) 315 { 316 atomicCopy(src, dst); 317 src.remove(); 318 } 319 } 320 321 /// Make sure that the given directory exists 322 /// (and create parent directories as necessary). 323 void ensureDirExists(string path) 324 { 325 if (!path.exists) 326 path.mkdirRecurse(); 327 } 328 329 /// Make sure that the path to the given file name 330 /// exists (and create directories as necessary). 331 void ensurePathExists(string fn) 332 { 333 fn.dirName.ensureDirExists(); 334 } 335 336 import ae.utils.text; 337 338 /// Forcibly remove a file or directory. 339 /// If atomic is true, the entire directory is deleted "atomically" 340 /// (it is first moved/renamed to another location). 341 /// On Windows, this will move the file/directory out of the way, 342 /// if it is in use and cannot be deleted (but can be renamed). 343 void forceDelete(Flag!"atomic" atomic=Yes.atomic)(string fn, Flag!"recursive" recursive = No.recursive) 344 { 345 import std.process : environment; 346 version(Windows) 347 { 348 mixin(importWin32!q{winnt}); 349 mixin(importWin32!q{winbase}); 350 } 351 352 auto name = fn.baseName(); 353 fn = fn.absolutePath().longPath(); 354 355 version(Windows) 356 { 357 auto fnW = toUTF16z(fn); 358 auto attr = GetFileAttributesW(fnW); 359 wenforce(attr != INVALID_FILE_ATTRIBUTES, "GetFileAttributes"); 360 if (attr & FILE_ATTRIBUTE_READONLY) 361 SetFileAttributesW(fnW, attr & ~FILE_ATTRIBUTE_READONLY).wenforce("SetFileAttributes"); 362 } 363 364 static if (atomic) 365 { 366 // To avoid zombifying locked directories, try renaming it first. 367 // Attempting to delete a locked directory will make it inaccessible. 368 369 bool tryMoveTo(string target) 370 { 371 target = target.longPath(); 372 if (target.endsWith(dirSeparator)) 373 target = target[0..$-1]; 374 if (target.length && !target.exists) 375 return false; 376 377 string newfn; 378 do 379 newfn = format("%s%sdeleted-%s.%s.%s", target, dirSeparator, name, thisProcessID, randomString()); 380 while (newfn.exists); 381 382 version(Windows) 383 { 384 auto newfnW = toUTF16z(newfn); 385 if (!MoveFileW(fnW, newfnW)) 386 return false; 387 } 388 else 389 { 390 try 391 rename(fn, newfn); 392 catch (FileException e) 393 return false; 394 } 395 396 fn = newfn; 397 version(Windows) fnW = newfnW; 398 return true; 399 } 400 401 void tryMove() 402 { 403 auto tmp = environment.get("TEMP"); 404 if (tmp) 405 if (tryMoveTo(tmp)) 406 return; 407 408 version(Windows) 409 string tempDir = fn[0..7]~"Temp"; 410 else 411 enum tempDir = "/tmp"; 412 413 if (tryMoveTo(tempDir)) 414 return; 415 416 if (tryMoveTo(fn.dirName())) 417 return; 418 419 throw new Exception("Unable to delete " ~ fn ~ " atomically (all rename attempts failed)"); 420 } 421 422 tryMove(); 423 } 424 425 version(Windows) 426 { 427 if (attr & FILE_ATTRIBUTE_DIRECTORY) 428 { 429 if (recursive && (attr & FILE_ATTRIBUTE_REPARSE_POINT) == 0) 430 { 431 foreach (de; fn.dirEntries(SpanMode.shallow)) 432 forceDelete!(No.atomic)(de.name, Yes.recursive); 433 } 434 // Will fail if !recursive and directory is not empty 435 RemoveDirectoryW(fnW).wenforce("RemoveDirectory"); 436 } 437 else 438 DeleteFileW(fnW).wenforce("DeleteFile"); 439 } 440 else 441 { 442 if (recursive) 443 fn.removeRecurse(); 444 else 445 if (fn.isDir) 446 fn.rmdir(); 447 else 448 fn.remove(); 449 } 450 } 451 452 453 deprecated void forceDelete(bool atomic)(string fn, bool recursive = false) { forceDelete!(cast(Flag!"atomic")atomic)(fn, cast(Flag!"recursive")recursive); } 454 //deprecated void forceDelete()(string fn, bool recursive) { forceDelete!(Yes.atomic)(fn, cast(Flag!"recursive")recursive); } 455 456 deprecated unittest 457 { 458 mkdir("testdir"); touch("testdir/b"); forceDelete!(false )("testdir", true); 459 mkdir("testdir"); touch("testdir/b"); forceDelete!(true )("testdir", true); 460 } 461 462 unittest 463 { 464 mkdir("testdir"); touch("testdir/b"); forceDelete ("testdir", Yes.recursive); 465 mkdir("testdir"); touch("testdir/b"); forceDelete!(No .atomic)("testdir", Yes.recursive); 466 mkdir("testdir"); touch("testdir/b"); forceDelete!(Yes.atomic)("testdir", Yes.recursive); 467 } 468 469 /// If fn is a directory, delete it recursively. 470 /// Otherwise, delete the file or symlink fn. 471 void removeRecurse(string fn) 472 { 473 auto attr = fn.getAttributes(); 474 if (attr.attrIsSymlink) 475 { 476 version (Windows) 477 if (attr.attrIsDir) 478 fn.rmdir(); 479 else 480 fn.remove(); 481 else 482 fn.remove(); 483 } 484 else 485 if (attr.attrIsDir) 486 version (Windows) 487 fn.forceDelete!(No.atomic)(Yes.recursive); // For read-only files 488 else 489 fn.rmdirRecurse(); 490 else 491 fn.remove(); 492 } 493 494 /// Create an empty directory, deleting 495 /// all its contents if it already exists. 496 void recreateEmptyDirectory()(string dir) 497 { 498 if (dir.exists) 499 dir.forceDelete(Yes.recursive); 500 mkdir(dir); 501 } 502 503 bool isHidden()(string fn) 504 { 505 if (baseName(fn).startsWith(".")) 506 return true; 507 version (Windows) 508 { 509 mixin(importWin32!q{winnt}); 510 if (getAttributes(fn) & FILE_ATTRIBUTE_HIDDEN) 511 return true; 512 } 513 return false; 514 } 515 516 /// Return a file's unique ID. 517 ulong getFileID()(string fn) 518 { 519 version (Windows) 520 { 521 mixin(importWin32!q{winnt}); 522 mixin(importWin32!q{winbase}); 523 524 auto fnW = toUTF16z(fn); 525 auto h = CreateFileW(fnW, FILE_READ_ATTRIBUTES, 0, null, OPEN_EXISTING, 0, HANDLE.init); 526 wenforce(h!=INVALID_HANDLE_VALUE, fn); 527 scope(exit) CloseHandle(h); 528 BY_HANDLE_FILE_INFORMATION fi; 529 GetFileInformationByHandle(h, &fi).wenforce("GetFileInformationByHandle"); 530 531 ULARGE_INTEGER li; 532 li.LowPart = fi.nFileIndexLow; 533 li.HighPart = fi.nFileIndexHigh; 534 auto result = li.QuadPart; 535 enforce(result, "Null file ID"); 536 return result; 537 } 538 else 539 { 540 return DirEntry(fn).statBuf.st_ino; 541 } 542 } 543 544 unittest 545 { 546 touch("a"); 547 scope(exit) remove("a"); 548 hardLink("a", "b"); 549 scope(exit) remove("b"); 550 touch("c"); 551 scope(exit) remove("c"); 552 assert(getFileID("a") == getFileID("b")); 553 assert(getFileID("a") != getFileID("c")); 554 } 555 556 deprecated alias std.file.getSize getSize2; 557 558 /// Using UNC paths bypasses path length limitation when using Windows wide APIs. 559 string longPath(string s) 560 { 561 version (Windows) 562 { 563 if (!s.startsWith(`\\`)) 564 return `\\?\` ~ s.absolutePath().buildNormalizedPath().replace(`/`, `\`); 565 } 566 return s; 567 } 568 569 version (Windows) 570 { 571 static if (__traits(compiles, { mixin importWin32!q{winnt}; })) 572 static mixin(importWin32!q{winnt}); 573 574 void createReparsePoint(string reparseBufferName, string extraInitialization, string reparseTagName)(in char[] target, in char[] print, in char[] link) 575 { 576 mixin(importWin32!q{winbase}); 577 mixin(importWin32!q{windef}); 578 mixin(importWin32!q{winioctl}); 579 580 enum SYMLINK_FLAG_RELATIVE = 1; 581 582 HANDLE hLink = CreateFileW(link.toUTF16z(), GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, null); 583 wenforce(hLink && hLink != INVALID_HANDLE_VALUE, "CreateFileW"); 584 scope(exit) CloseHandle(hLink); 585 586 enum pathOffset = 587 mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName) .offsetof + 588 mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)._PathBuffer.offsetof; 589 590 auto targetW = target.toUTF16(); 591 auto printW = print .toUTF16(); 592 593 // Despite MSDN, two NUL-terminating characters are needed, one for each string. 594 595 auto pathBufferSize = targetW.length + 1 + printW.length + 1; // in chars 596 auto buf = new ubyte[pathOffset + pathBufferSize * WCHAR.sizeof]; 597 auto r = cast(REPARSE_DATA_BUFFER*)buf.ptr; 598 599 r.ReparseTag = mixin(reparseTagName); 600 r.ReparseDataLength = to!WORD(buf.length - mixin(q{r..} ~ reparseBufferName).offsetof); 601 602 auto pathBuffer = mixin(q{r..} ~ reparseBufferName).PathBuffer; 603 auto p = pathBuffer; 604 605 mixin(q{r..} ~ reparseBufferName).SubstituteNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof); 606 mixin(q{r..} ~ reparseBufferName).SubstituteNameLength = to!WORD(targetW.length * WCHAR.sizeof); 607 p[0..targetW.length] = targetW; 608 p += targetW.length; 609 *p++ = 0; 610 611 mixin(q{r..} ~ reparseBufferName).PrintNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof); 612 mixin(q{r..} ~ reparseBufferName).PrintNameLength = to!WORD(printW .length * WCHAR.sizeof); 613 p[0..printW.length] = printW; 614 p += printW.length; 615 *p++ = 0; 616 617 assert(p-pathBuffer == pathBufferSize); 618 619 mixin(extraInitialization); 620 621 DWORD dwRet; // Needed despite MSDN 622 DeviceIoControl(hLink, FSCTL_SET_REPARSE_POINT, buf.ptr, buf.length.to!DWORD(), null, 0, &dwRet, null).wenforce("DeviceIoControl"); 623 } 624 625 void acquirePrivilege(S)(S name) 626 { 627 mixin(importWin32!q{winbase}); 628 mixin(importWin32!q{windef}); 629 630 import ae.sys.windows; 631 632 HANDLE hToken = null; 633 wenforce(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)); 634 scope(exit) CloseHandle(hToken); 635 636 TOKEN_PRIVILEGES tp; 637 wenforce(LookupPrivilegeValue(null, name.toUTF16z(), &tp.Privileges[0].Luid), "LookupPrivilegeValue"); 638 639 tp.PrivilegeCount = 1; 640 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 641 wenforce(AdjustTokenPrivileges(hToken, FALSE, &tp, cast(DWORD)TOKEN_PRIVILEGES.sizeof, null, null), "AdjustTokenPrivileges"); 642 } 643 644 /// Link a directory. 645 /// Uses symlinks on POSIX, and directory junctions on Windows. 646 void dirLink()(in char[] original, in char[] link) 647 { 648 mkdir(link); 649 scope(failure) rmdir(link); 650 651 auto target = `\??\` ~ original.idup.absolutePath(); 652 if (target[$-1] != '\\') 653 target ~= '\\'; 654 655 createReparsePoint!(q{MountPointReparseBuffer}, q{}, q{IO_REPARSE_TAG_MOUNT_POINT})(target, null, link); 656 } 657 658 void symlink()(in char[] original, in char[] link) 659 { 660 mixin(importWin32!q{winnt}); 661 662 acquirePrivilege(SE_CREATE_SYMBOLIC_LINK_NAME); 663 664 touch(link); 665 scope(failure) remove(link); 666 667 createReparsePoint!(q{SymbolicLinkReparseBuffer}, q{r.SymbolicLinkReparseBuffer.Flags = link.isAbsolute() ? 0 : SYMLINK_FLAG_RELATIVE;}, q{IO_REPARSE_TAG_SYMLINK})(original, original, link); 668 } 669 } 670 else 671 alias std.file.symlink dirLink; 672 673 version(Windows) version(unittest) static mixin(importWin32!q{winnt}); 674 675 unittest 676 { 677 mkdir("a"); scope(exit) rmdir("a"[]); 678 touch("a/f"); scope(exit) remove("a/f"); 679 dirLink("a", "b"); scope(exit) version(Windows) rmdir("b"); else remove("b"); 680 //symlink("a/f", "c"); scope(exit) remove("c"); 681 assert("b".isSymlink()); 682 //assert("c".isSymlink()); 683 assert("b/f".exists()); 684 } 685 686 version (Windows) 687 { 688 void hardLink()(string src, string dst) 689 { 690 mixin(importWin32!q{w32api}); 691 692 static assert(_WIN32_WINNT >= 0x501, "CreateHardLinkW not available for target Windows platform. Specify -version=WindowsXP"); 693 694 mixin(importWin32!q{winnt}); 695 mixin(importWin32!q{winbase}); 696 697 wenforce(CreateHardLinkW(toUTF16z(dst), toUTF16z(src), null), "CreateHardLink failed: " ~ src ~ " -> " ~ dst); 698 } 699 } 700 version (Posix) 701 { 702 void hardLink()(string src, string dst) 703 { 704 import core.sys.posix.unistd; 705 enforce(link(toUTFz!(const char*)(src), toUTFz!(const char*)(dst)) == 0, "link() failed: " ~ dst); 706 } 707 } 708 709 version (Posix) 710 { 711 string realPath(string path) 712 { 713 // TODO: Windows version 714 import core.sys.posix.stdlib; 715 auto p = realpath(toUTFz!(const char*)(path), null); 716 errnoEnforce(p, "realpath"); 717 string result = fromStringz(p).idup; 718 free(p); 719 return result; 720 } 721 } 722 723 // /proc/self/mounts parsing 724 version (linux) 725 { 726 struct MountInfo 727 { 728 string spec; /// device path 729 string file; /// mount path 730 string vfstype; /// file system 731 string mntops; /// options 732 int freq; /// dump flag 733 int passno; /// fsck order 734 } 735 736 string unescapeMountString(in char[] s) 737 { 738 string result; 739 740 size_t p = 0; 741 for (size_t i=0; i+3<s.length;) 742 { 743 auto c = s[i]; 744 if (c == '\\') 745 { 746 result ~= s[p..i]; 747 result ~= to!int(s[i+1..i+4], 8); 748 i += 4; 749 p = i; 750 } 751 else 752 i++; 753 } 754 result ~= s[p..$]; 755 return result; 756 } 757 758 unittest 759 { 760 assert(unescapeMountString(`a\040b\040c`) == "a b c"); 761 assert(unescapeMountString(`\040`) == " "); 762 } 763 764 MountInfo parseMountInfo(in char[] line) 765 { 766 const(char)[][6] parts; 767 copy(line.splitter(" "), parts[]); 768 return MountInfo( 769 unescapeMountString(parts[0]), 770 unescapeMountString(parts[1]), 771 unescapeMountString(parts[2]), 772 unescapeMountString(parts[3]), 773 parts[4].to!int, 774 parts[5].to!int, 775 ); 776 } 777 778 /// Returns an iterator of MountInfo structs. 779 auto getMounts() 780 { 781 return File("/proc/self/mounts", "rb").byLine().map!parseMountInfo(); 782 } 783 784 /// Get the name of the filesystem that the given path is mounted under. 785 string getPathFilesystem(string path) 786 { 787 path = realPath(path); 788 size_t bestLength; string bestFS; 789 foreach (ref info; getMounts()) 790 { 791 if (path.startsWith(info.file) && (path.length == info.file.length || path[info.file.length] == '/')) 792 { 793 if (bestLength < info.file.length) 794 { 795 bestLength = info.file.length; 796 bestFS = info.vfstype; 797 } 798 } 799 } 800 return bestFS; 801 } 802 } 803 804 // **************************************************************************** 805 806 version (linux) 807 { 808 import core.sys.linux.sys.xattr; 809 import core.stdc.errno; 810 alias ENOATTR = ENODATA; 811 812 /// AA-like object for accessing a file's extended attributes. 813 struct XAttrs(Obj, string funPrefix) 814 { 815 Obj obj; 816 817 mixin("alias getFun = " ~ funPrefix ~ "getxattr;"); 818 mixin("alias setFun = " ~ funPrefix ~ "setxattr;"); 819 mixin("alias removeFun = " ~ funPrefix ~ "removexattr;"); 820 mixin("alias listFun = " ~ funPrefix ~ "listxattr;"); 821 822 void[] opIndex(string key) 823 { 824 auto cKey = key.toStringz(); 825 auto size = getFun(obj, cKey, null, 0); 826 errnoEnforce(size >= 0); 827 auto result = new void[size]; 828 // TODO: race condition, retry 829 size = getFun(obj, cKey, result.ptr, result.length); 830 errnoEnforce(size == result.length); 831 return result; 832 } 833 834 bool opIn_r(string key) 835 { 836 auto cKey = key.toStringz(); 837 auto size = getFun(obj, cKey, null, 0); 838 if (size >= 0) 839 return true; 840 else 841 if (errno == ENOATTR) 842 return false; 843 else 844 errnoEnforce(false, "Error reading file xattrs"); 845 assert(false); 846 } 847 848 void opIndexAssign(in void[] value, string key) 849 { 850 auto ret = setFun(obj, key.toStringz(), value.ptr, value.length, 0); 851 errnoEnforce(ret == 0); 852 } 853 854 void remove(string key) 855 { 856 auto ret = removeFun(obj, key.toStringz()); 857 errnoEnforce(ret == 0); 858 } 859 860 string[] keys() 861 { 862 auto size = listFun(obj, null, 0); 863 errnoEnforce(size >= 0); 864 auto buf = new char[size]; 865 // TODO: race condition, retry 866 size = listFun(obj, buf.ptr, buf.length); 867 errnoEnforce(size == buf.length); 868 869 char[][] result; 870 size_t start; 871 foreach (p, c; buf) 872 if (!c) 873 { 874 result ~= buf[start..p]; 875 start = p+1; 876 } 877 878 return cast(string[])result; 879 } 880 } 881 882 auto xAttrs(string path) 883 { 884 return XAttrs!(const(char)*, "")(path.toStringz()); 885 } 886 887 auto linkXAttrs(string path) 888 { 889 return XAttrs!(const(char)*, "l")(path.toStringz()); 890 } 891 892 auto xAttrs(in ref File f) 893 { 894 return XAttrs!(int, "f")(f.fileno); 895 } 896 897 unittest 898 { 899 enum fn = "test.txt"; 900 std.file.write(fn, "test"); 901 scope(exit) remove(fn); 902 903 auto attrs = xAttrs(fn); 904 enum key = "user.foo"; 905 assert(key !in attrs); 906 assert(attrs.keys == []); 907 908 attrs[key] = "bar"; 909 assert(key in attrs); 910 assert(attrs[key] == "bar"); 911 assert(attrs.keys == [key]); 912 913 attrs.remove(key); 914 assert(key !in attrs); 915 assert(attrs.keys == []); 916 } 917 } 918 919 // **************************************************************************** 920 921 version (Windows) 922 { 923 /// Enumerate all hard links to the specified file. 924 // TODO: Return a range 925 string[] enumerateHardLinks()(string fn) 926 { 927 mixin(importWin32!q{winnt}); 928 mixin(importWin32!q{winbase}); 929 930 alias extern(System) HANDLE function(LPCWSTR lpFileName, DWORD dwFlags, LPDWORD StringLength, PWCHAR LinkName) TFindFirstFileNameW; 931 alias extern(System) BOOL function(HANDLE hFindStream, LPDWORD StringLength, PWCHAR LinkName) TFindNextFileNameW; 932 933 auto kernel32 = GetModuleHandle("kernel32.dll"); 934 auto FindFirstFileNameW = cast(TFindFirstFileNameW)GetProcAddress(kernel32, "FindFirstFileNameW").wenforce("GetProcAddress(FindFirstFileNameW)"); 935 auto FindNextFileNameW = cast(TFindNextFileNameW)GetProcAddress(kernel32, "FindNextFileNameW").wenforce("GetProcAddress(FindNextFileNameW)"); 936 937 static WCHAR[0x8000] buf; 938 DWORD len = buf.length; 939 auto h = FindFirstFileNameW(toUTF16z(fn), 0, &len, buf.ptr); 940 wenforce(h != INVALID_HANDLE_VALUE, "FindFirstFileNameW"); 941 scope(exit) FindClose(h); 942 943 string[] result; 944 do 945 { 946 enforce(len > 0 && len < buf.length && buf[len-1] == 0, "Bad FindFirst/NextFileNameW result"); 947 result ~= buf[0..len-1].toUTF8(); 948 len = buf.length; 949 auto ok = FindNextFileNameW(h, &len, buf.ptr); 950 if (!ok && GetLastError() == ERROR_HANDLE_EOF) 951 break; 952 wenforce(ok, "FindNextFileNameW"); 953 } while(true); 954 return result; 955 } 956 } 957 958 uint hardLinkCount(string fn) 959 { 960 version (Windows) 961 { 962 // TODO: Optimize (don't transform strings) 963 return cast(uint)fn.enumerateHardLinks.length; 964 } 965 else 966 { 967 import core.sys.posix.sys.stat; 968 969 stat_t s; 970 errnoEnforce(stat(fn.toStringz(), &s) == 0, "stat"); 971 return s.st_nlink.to!uint; 972 } 973 } 974 975 unittest 976 { 977 touch("a.test"); 978 scope(exit) remove("a.test"); 979 assert("a.test".hardLinkCount() == 1); 980 981 hardLink("a.test", "b.test"); 982 scope(exit) remove("b.test"); 983 assert("a.test".hardLinkCount() == 2); 984 assert("b.test".hardLinkCount() == 2); 985 986 version(Windows) 987 { 988 auto paths = enumerateHardLinks("a.test"); 989 assert(paths.length == 2); 990 paths.sort(); 991 assert(paths[0].endsWith(`\a.test`), paths[0]); 992 assert(paths[1].endsWith(`\b.test`)); 993 } 994 } 995 996 void toFile(in void[] data, in char[] name) 997 { 998 std.file.write(name, data); 999 } 1000 1001 /// Uses UNC paths to open a file. 1002 /// Requires https://github.com/D-Programming-Language/phobos/pull/1888 1003 File openFile()(string fn, string mode = "rb") 1004 { 1005 File f; 1006 static if (is(typeof(&f.windowsHandleOpen))) 1007 { 1008 import core.sys.windows.windows; 1009 import ae.sys.windows.exception; 1010 1011 string winMode; 1012 foreach (c; mode) 1013 switch (c) 1014 { 1015 case 'r': 1016 case 'w': 1017 case 'a': 1018 case '+': 1019 winMode ~= c; 1020 break; 1021 case 'b': 1022 case 't': 1023 break; 1024 default: 1025 assert(false, "Unknown character in mode"); 1026 } 1027 DWORD access, creation; 1028 bool append; 1029 switch (winMode) 1030 { 1031 case "r" : access = GENERIC_READ ; creation = OPEN_EXISTING; break; 1032 case "r+": access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_EXISTING; break; 1033 case "w" : access = GENERIC_WRITE; creation = OPEN_ALWAYS ; break; 1034 case "w+": access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_ALWAYS ; break; 1035 case "a" : access = GENERIC_WRITE; creation = OPEN_ALWAYS ; append = true; break; 1036 case "a+": assert(false, "Not implemented"); // requires two file pointers 1037 default: assert(false, "Bad file mode: " ~ mode); 1038 } 1039 1040 auto pathW = toUTF16z(longPath(fn)); 1041 auto h = CreateFileW(pathW, access, FILE_SHARE_READ, null, creation, 0, HANDLE.init); 1042 wenforce(h != INVALID_HANDLE_VALUE); 1043 1044 if (append) 1045 h.SetFilePointer(0, null, FILE_END); 1046 1047 f.windowsHandleOpen(h, mode); 1048 } 1049 else 1050 f.open(fn, mode); 1051 return f; 1052 } 1053 1054 auto fileDigest(Digest)(string fn) 1055 { 1056 import std.range.primitives; 1057 Digest context; 1058 context.start(); 1059 put(context, openFile(fn, "rb").byChunk(64 * 1024)); 1060 auto digest = context.finish(); 1061 return digest; 1062 } 1063 1064 template mdFile() 1065 { 1066 import std.digest.md; 1067 alias mdFile = fileDigest!MD5; 1068 } 1069 1070 version (HAVE_WIN32) 1071 unittest 1072 { 1073 import std.digest.digest : toHexString; 1074 write("test.txt", "Hello, world!"); 1075 scope(exit) remove("test.txt"); 1076 assert(mdFile("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 1077 } 1078 1079 auto fileDigestCached(Digest)(string fn) 1080 { 1081 static typeof(Digest.init.finish())[ulong] cache; 1082 auto id = getFileID(fn); 1083 auto phash = id in cache; 1084 if (phash) 1085 return *phash; 1086 return cache[id] = fileDigest!Digest(fn); 1087 } 1088 1089 template mdFileCached() 1090 { 1091 import std.digest.md; 1092 alias mdFileCached = fileDigestCached!MD5; 1093 } 1094 1095 version (HAVE_WIN32) 1096 unittest 1097 { 1098 import std.digest.digest : toHexString; 1099 write("test.txt", "Hello, world!"); 1100 scope(exit) remove("test.txt"); 1101 assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 1102 write("test.txt", "Something else"); 1103 assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 1104 } 1105 1106 /// Read a File (which might be a stream) into an array 1107 void[] readFile(File f) 1108 { 1109 import std.range.primitives; 1110 auto result = appender!(ubyte[]); 1111 put(result, f.byChunk(64*1024)); 1112 return result.data; 1113 } 1114 1115 unittest 1116 { 1117 auto s = "0123456789".replicate(10_000); 1118 write("test.txt", s); 1119 scope(exit) remove("test.txt"); 1120 assert(readFile(File("test.txt")) == s); 1121 } 1122 1123 /// Like std.file.readText for non-UTF8 1124 ascii readAscii()(string fileName) 1125 { 1126 return cast(ascii)readFile(openFile(fileName, "rb")); 1127 } 1128 1129 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 1130 version(Posix) static import ae.sys.signals; 1131 1132 /// Start a thread which writes data to f asynchronously. 1133 Thread writeFileAsync(File f, in void[] data) 1134 { 1135 static class Writer : Thread 1136 { 1137 File target; 1138 const void[] data; 1139 1140 this(ref File f, in void[] data) 1141 { 1142 this.target = f; 1143 this.data = data; 1144 super(&run); 1145 } 1146 1147 void run() 1148 { 1149 version (Posix) 1150 { 1151 import ae.sys.signals; 1152 collectSignal(SIGPIPE, &write); 1153 } 1154 else 1155 write(); 1156 } 1157 1158 void write() 1159 { 1160 target.rawWrite(data); 1161 target.close(); 1162 } 1163 } 1164 1165 auto t = new Writer(f, data); 1166 t.start(); 1167 return t; 1168 } 1169 1170 /// Write data to a file, and ensure it gets written to disk 1171 /// before this function returns. 1172 /// Consider using as atomic!syncWrite. 1173 /// See also: syncUpdate 1174 void syncWrite()(string target, in void[] data) 1175 { 1176 auto f = File(target, "wb"); 1177 f.rawWrite(data); 1178 version (Windows) 1179 { 1180 mixin(importWin32!q{windows}); 1181 FlushFileBuffers(f.windowsHandle); 1182 } 1183 else 1184 { 1185 import core.sys.posix.unistd; 1186 fsync(f.fileno); 1187 } 1188 f.close(); 1189 } 1190 1191 /// Atomically save data to a file (if the file doesn't exist, 1192 /// or its contents differs). The update operation as a whole 1193 /// is not atomic, only the write is. 1194 void syncUpdate()(string fn, in void[] data) 1195 { 1196 if (!fn.exists || fn.read() != data) 1197 atomic!(syncWrite!())(fn, data); 1198 } 1199 1200 version(Windows) import ae.sys.windows.exception; 1201 1202 struct NamedPipeImpl 1203 { 1204 immutable string fileName; 1205 1206 /// Create a named pipe, and reserve a filename. 1207 this()(string name) 1208 { 1209 version(Windows) 1210 { 1211 mixin(importWin32!q{winbase}); 1212 1213 fileName = `\\.\pipe\` ~ name; 1214 auto h = CreateNamedPipeW(fileName.toUTF16z, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE, 10, 4096, 4096, 0, null).wenforce("CreateNamedPipeW"); 1215 f.windowsHandleOpen(h, "wb"); 1216 } 1217 else 1218 { 1219 import core.sys.posix.sys.stat; 1220 1221 fileName = `/tmp/` ~ name ~ `.fifo`; 1222 mkfifo(fileName.toStringz, S_IWUSR | S_IRUSR); 1223 } 1224 } 1225 1226 /// Wait for a peer to open the other end of the pipe. 1227 File connect()() 1228 { 1229 version(Windows) 1230 { 1231 mixin(importWin32!q{winbase}); 1232 mixin(importWin32!q{windef}); 1233 1234 BOOL bSuccess = ConnectNamedPipe(f.windowsHandle, null); 1235 1236 // "If a client connects before the function is called, the function returns zero 1237 // and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client 1238 // connects in the interval between the call to CreateNamedPipe and the call to 1239 // ConnectNamedPipe. In this situation, there is a good connection between client 1240 // and server, even though the function returns zero." 1241 if (!bSuccess) 1242 wenforce(GetLastError() == ERROR_PIPE_CONNECTED, "ConnectNamedPipe"); 1243 1244 return f; 1245 } 1246 else 1247 { 1248 return File(fileName, "w"); 1249 } 1250 } 1251 1252 ~this() 1253 { 1254 version(Windows) 1255 { 1256 // File.~this will take care of cleanup 1257 } 1258 else 1259 fileName.remove(); 1260 } 1261 1262 private: 1263 File f; 1264 } 1265 alias NamedPipe = RefCounted!NamedPipeImpl; 1266 1267 import ae.utils.textout : StringBuilder; 1268 1269 /// Avoid std.stdio.File.readln's memory corruption bug 1270 /// https://issues.dlang.org/show_bug.cgi?id=13856 1271 string safeReadln(File f) 1272 { 1273 StringBuilder buf; 1274 char[1] arr; 1275 while (true) 1276 { 1277 auto result = f.rawRead(arr[]); 1278 if (!result.length) 1279 break; 1280 buf.put(result); 1281 if (result[0] == '\x0A') 1282 break; 1283 } 1284 return buf.get(); 1285 } 1286 1287 // **************************************************************************** 1288 1289 /// Change the current directory to the given directory. Does nothing if dir is null. 1290 /// Return a scope guard which, upon destruction, restores the previous directory. 1291 /// Asserts that only one thread has changed the process's current directory at any time. 1292 auto pushd(string dir) 1293 { 1294 import core.atomic; 1295 1296 static int threadCount = 0; 1297 static shared int processCount = 0; 1298 1299 static struct Popd 1300 { 1301 string oldPath; 1302 this(string cwd) { oldPath = cwd; } 1303 ~this() { if (oldPath) pop(); } 1304 @disable this(); 1305 @disable this(this); 1306 1307 void pop() 1308 { 1309 assert(oldPath); 1310 scope(exit) oldPath = null; 1311 chdir(oldPath); 1312 1313 auto newThreadCount = --threadCount; 1314 auto newProcessCount = atomicOp!"-="(processCount, 1); 1315 assert(newThreadCount == newProcessCount); // Shouldn't happen 1316 } 1317 } 1318 1319 string cwd; 1320 if (dir) 1321 { 1322 auto newThreadCount = ++threadCount; 1323 auto newProcessCount = atomicOp!"+="(processCount, 1); 1324 assert(newThreadCount == newProcessCount, "Another thread already has an active pushd"); 1325 1326 cwd = getcwd(); 1327 chdir(dir); 1328 } 1329 return Popd(cwd); 1330 } 1331 1332 // **************************************************************************** 1333 1334 import std.algorithm; 1335 import std.process : thisProcessID; 1336 import std.traits; 1337 import std.typetuple; 1338 import ae.utils.meta; 1339 1340 enum targetParameterNames = "target/to/name/dst"; 1341 1342 /// Wrap an operation which creates a file or directory, 1343 /// so that it is created safely and, for files, atomically 1344 /// (by performing the underlying operation to a temporary 1345 /// location, then renaming the completed file/directory to 1346 /// the actual target location). targetName specifies the name 1347 /// of the parameter containing the target file/directory. 1348 auto atomic(alias impl, string targetName = targetParameterNames)(staticMap!(Unqual, ParameterTypeTuple!impl) args) 1349 { 1350 enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl)); 1351 return atomic!(impl, targetIndex)(args); 1352 } 1353 1354 /// ditto 1355 auto atomic(alias impl, size_t targetIndex)(staticMap!(Unqual, ParameterTypeTuple!impl) args) 1356 { 1357 // idup for https://d.puremagic.com/issues/show_bug.cgi?id=12503 1358 auto target = args[targetIndex].idup; 1359 auto temp = "%s.%s.%s.temp".format(target, thisProcessID, getCurrentThreadID); 1360 if (temp.exists) temp.removeRecurse(); 1361 scope(success) rename(temp, target); 1362 scope(failure) if (temp.exists) temp.removeRecurse(); 1363 args[targetIndex] = temp; 1364 return impl(args); 1365 } 1366 1367 /// ditto 1368 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12230 1369 // Can't be an overload because of https://issues.dlang.org/show_bug.cgi?id=13374 1370 //R atomicDg(string targetName = "target", R, Args...)(R delegate(Args) impl, staticMap!(Unqual, Args) args) 1371 auto atomicDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args) 1372 { 1373 enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA; 1374 return atomic!(impl, targetIndex)(args); 1375 } 1376 1377 deprecated alias safeUpdate = atomic; 1378 1379 unittest 1380 { 1381 enum fn = "atomic.tmp"; 1382 scope(exit) if (fn.exists) fn.remove(); 1383 1384 atomic!touch(fn); 1385 assert(fn.exists); 1386 fn.remove(); 1387 1388 atomicDg(&touch, fn); 1389 assert(fn.exists); 1390 } 1391 1392 /// Wrap an operation so that it is skipped entirely 1393 /// if the target already exists. Implies atomic. 1394 auto cached(alias impl, string targetName = targetParameterNames)(ParameterTypeTuple!impl args) 1395 { 1396 enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl)); 1397 auto target = args[targetIndex]; 1398 if (!target.exists) 1399 atomic!(impl, targetIndex)(args); 1400 return target; 1401 } 1402 1403 /// ditto 1404 // Exists due to the same reasons as atomicDg 1405 auto cachedDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args) 1406 { 1407 enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA; 1408 auto target = args[targetIndex]; 1409 if (!target.exists) 1410 atomic!(impl, targetIndex)(args); 1411 return target; 1412 } 1413 1414 deprecated alias obtainUsing = cached; 1415 1416 /// Create a file, or replace an existing file's contents 1417 /// atomically. 1418 /// Note: Consider using atomic!syncWrite or 1419 /// atomic!syncUpdate instead. 1420 alias atomic!writeProxy atomicWrite; 1421 deprecated alias safeWrite = atomicWrite; 1422 void writeProxy(string target, in void[] data) 1423 { 1424 std.file.write(target, data); 1425 } 1426 1427 // Work around for https://github.com/D-Programming-Language/phobos/pull/2784#issuecomment-68117241 1428 private void copy2(string source, string target) { std.file.copy(source, target); } 1429 1430 /// Copy a file, or replace an existing file's contents 1431 /// with another file's, atomically. 1432 alias atomic!copy2 atomicCopy; 1433 1434 unittest 1435 { 1436 enum fn = "cached.tmp"; 1437 scope(exit) if (fn.exists) fn.remove(); 1438 1439 cached!touch(fn); 1440 assert(fn.exists); 1441 1442 std.file.write(fn, "test"); 1443 1444 cachedDg!0(&writeProxy, fn, "test2"); 1445 assert(fn.readText() == "test"); 1446 } 1447 1448 // **************************************************************************** 1449 1450 template withTarget(alias targetGen, alias fun) 1451 { 1452 auto withTarget(Args...)(auto ref Args args) 1453 { 1454 auto target = targetGen(args); 1455 fun(args, target); 1456 return target; 1457 } 1458 } 1459 1460 /// Two-argument buildPath with reversed arguments. 1461 /// Useful for UFCS chaining. 1462 string prependPath(string target, string path) 1463 { 1464 return buildPath(path, target); 1465 }