1 // Written in the D programming language. 2 3 /** 4 5 This module contains cross-platform file access utilities 6 7 8 9 Synopsis: 10 11 ---- 12 import dlangui.core.files; 13 ---- 14 15 Copyright: Vadim Lopatin, 2014 16 License: Boost License 1.0 17 Authors: Vadim Lopatin, coolreader.org@gmail.com 18 */ 19 module dlangui.core.files; 20 21 import std.algorithm; 22 23 private import dlangui.core.logger; 24 private import std.process; 25 private import std.path; 26 private import std.file; 27 private import std.utf; 28 29 30 /// path delimiter (\ for windows, / for others) 31 enum char PATH_DELIMITER = dirSeparator[0]; 32 33 /// Filesystem root entry / bookmark types 34 enum RootEntryType : uint { 35 /// filesystem root 36 ROOT, 37 /// current user home 38 HOME, 39 /// removable drive 40 REMOVABLE, 41 /// fixed drive 42 FIXED, 43 /// network 44 NETWORK, 45 /// cd rom 46 CDROM, 47 /// sd card 48 SDCARD, 49 /// custom bookmark 50 BOOKMARK, 51 } 52 53 /// Filesystem root entry item 54 struct RootEntry { 55 private RootEntryType _type; 56 private string _path; 57 private dstring _display; 58 this(RootEntryType type, string path, dstring display = null) { 59 _type = type; 60 _path = path; 61 _display = display; 62 if (display is null) { 63 _display = toUTF32(baseName(path)); 64 } 65 } 66 /// Returns type 67 @property RootEntryType type() { return _type; } 68 /// Returns path 69 @property string path() { return _path; } 70 /// Returns display label 71 @property dstring label() { return _display; } 72 /// Returns icon resource id 73 @property string icon() { 74 switch (type) with(RootEntryType) 75 { 76 case NETWORK: 77 return "folder-network"; 78 case BOOKMARK: 79 return "folder-bookmark"; 80 case CDROM: 81 return "drive-optical"; 82 case FIXED: 83 return "drive-harddisk"; 84 case HOME: 85 return "user-home"; 86 case ROOT: 87 return "computer"; 88 case SDCARD: 89 return "media-flash-sd-mmc"; 90 case REMOVABLE: 91 return "device-removable-media"; 92 default: 93 return "folder-blue"; 94 } 95 } 96 } 97 98 /// Returns user's home directory entry 99 @property RootEntry homeEntry() { 100 return RootEntry(RootEntryType.HOME, homePath); 101 } 102 103 /// Returns user's home directory 104 @property string homePath() { 105 string path; 106 version (Windows) { 107 path = environment.get("USERPROFILE"); 108 if (path is null) 109 path = environment.get("HOME"); 110 } else { 111 path = environment.get("HOME"); 112 } 113 if (path is null) 114 path = "."; // fallback to current directory 115 return path; 116 } 117 118 version(OSX) {} else version(Posix) 119 { 120 private bool isSpecialFileSystem(const(char)[] dir, const(char)[] type) 121 { 122 import std.string : startsWith; 123 if (dir.startsWith("/dev") || dir.startsWith("/proc") || dir.startsWith("/sys") || 124 dir.startsWith("/var/run") || dir.startsWith("/var/lock")) 125 { 126 return true; 127 } 128 129 if (type == "tmpfs" || type == "rootfs" || type == "rpc_pipefs") { 130 return true; 131 } 132 return false; 133 } 134 135 private string getDeviceLabelFallback(in char[] type, in char[] fsName, in char[] mountDir) 136 { 137 import std.format : format; 138 if (type == "vboxsf") { 139 return "VirtualBox shared folder"; 140 } 141 if (type == "fuse.gvfsd-fuse") { 142 return "GNOME Virtual file system"; 143 } 144 return format("%s (%s)", mountDir.baseName, type); 145 } 146 147 private RootEntryType getDeviceRootEntryType(in char[] type) 148 { 149 switch(type) 150 { 151 case "iso9660": 152 return RootEntryType.CDROM; 153 case "vfat": 154 return RootEntryType.REMOVABLE; 155 case "cifs": 156 case "davfs": 157 case "fuse.sshfs": 158 case "nfs": 159 case "nfs4": 160 return RootEntryType.NETWORK; 161 default: 162 return RootEntryType.FIXED; 163 } 164 } 165 } 166 167 version(FreeBSD) 168 { 169 private: 170 import core.sys.posix.sys.types; 171 172 enum MFSNAMELEN = 16; /* length of type name including null */ 173 enum MNAMELEN = 88; /* size of on/from name bufs */ 174 enum STATFS_VERSION = 0x20030518; /* current version number */ 175 176 struct fsid_t 177 { 178 int[2] val; 179 } 180 181 struct statfs { 182 uint f_version; /* structure version number */ 183 uint f_type; /* type of filesystem */ 184 ulong f_flags; /* copy of mount exported flags */ 185 ulong f_bsize; /* filesystem fragment size */ 186 ulong f_iosize; /* optimal transfer block size */ 187 ulong f_blocks; /* total data blocks in filesystem */ 188 ulong f_bfree; /* free blocks in filesystem */ 189 long f_bavail; /* free blocks avail to non-superuser */ 190 ulong f_files; /* total file nodes in filesystem */ 191 long f_ffree; /* free nodes avail to non-superuser */ 192 ulong f_syncwrites; /* count of sync writes since mount */ 193 ulong f_asyncwrites; /* count of async writes since mount */ 194 ulong f_syncreads; /* count of sync reads since mount */ 195 ulong f_asyncreads; /* count of async reads since mount */ 196 ulong[10] f_spare; /* unused spare */ 197 uint f_namemax; /* maximum filename length */ 198 uid_t f_owner; /* user that mounted the filesystem */ 199 fsid_t f_fsid; /* filesystem id */ 200 char[80] f_charspare; /* spare string space */ 201 char[MFSNAMELEN] f_fstypename; /* filesystem type name */ 202 char[MNAMELEN] f_mntfromname; /* mounted filesystem */ 203 char[MNAMELEN] f_mntonname; /* directory on which mounted */ 204 }; 205 206 extern(C) @nogc nothrow 207 { 208 int getmntinfo(statfs **mntbufp, int flags); 209 } 210 } 211 212 version(linux) 213 { 214 private: 215 import core.stdc.stdio : FILE; 216 struct mntent 217 { 218 char *mnt_fsname; /* Device or server for filesystem. */ 219 char *mnt_dir; /* Directory mounted on. */ 220 char *mnt_type; /* Type of filesystem: ufs, nfs, etc. */ 221 char *mnt_opts; /* Comma-separated options for fs. */ 222 int mnt_freq; /* Dump frequency (in days). */ 223 int mnt_passno; /* Pass number for `fsck'. */ 224 }; 225 226 extern(C) @nogc nothrow 227 { 228 FILE *setmntent(const char *file, const char *mode); 229 mntent *getmntent(FILE *stream); 230 mntent *getmntent_r(FILE * stream, mntent *result, char * buffer, int bufsize); 231 int addmntent(FILE* stream, const mntent *mnt); 232 int endmntent(FILE * stream); 233 char *hasmntopt(const mntent *mnt, const char *opt); 234 } 235 236 string unescapeLabel(string label) 237 { 238 import std.string : replace; 239 return label.replace("\\x20", " ") 240 .replace("\\x9", " ") //actually tab 241 .replace("\\x5c", "\\") 242 .replace("\\xA", " "); //actually newline 243 } 244 } 245 246 /// returns array of system root entries 247 @property RootEntry[] getRootPaths() { 248 RootEntry[] res; 249 res ~= RootEntry(RootEntryType.HOME, homePath); 250 version (Posix) { 251 res ~= RootEntry(RootEntryType.ROOT, "/", "File System"d); 252 } 253 version(Android) { 254 // do nothing 255 } else version(linux) { 256 import std.string : fromStringz; 257 import std.exception : collectException; 258 259 mntent ent; 260 char[1024] buf; 261 FILE* f = setmntent("/etc/mtab", "r"); 262 263 if (f) { 264 scope(exit) endmntent(f); 265 while(getmntent_r(f, &ent, buf.ptr, cast(int)buf.length) !is null) { 266 auto fsName = fromStringz(ent.mnt_fsname); 267 auto mountDir = fromStringz(ent.mnt_dir); 268 auto type = fromStringz(ent.mnt_type); 269 270 if (mountDir == "/" || //root is already added 271 isSpecialFileSystem(mountDir, type)) //don't list special file systems 272 { 273 continue; 274 } 275 276 string label; 277 enum byLabel = "/dev/disk/by-label"; 278 if (fsName.isAbsolute) { 279 try { 280 foreach(entry; dirEntries(byLabel, SpanMode.shallow)) 281 { 282 string resolvedLink; 283 if (entry.isSymlink && collectException(entry.readLink, resolvedLink) is null) { 284 auto normalized = buildNormalizedPath(byLabel, resolvedLink); 285 if (normalized == fsName) { 286 label = entry.name.baseName.unescapeLabel(); 287 } 288 } 289 } 290 } catch(Exception e) { 291 292 } 293 } 294 295 if (!label.length) { 296 label = getDeviceLabelFallback(type, fsName, mountDir); 297 } 298 auto entryType = getDeviceRootEntryType(type); 299 res ~= RootEntry(entryType, mountDir.idup, label.toUTF32); 300 } 301 } 302 } 303 304 version(FreeBSD) { 305 import std.string : fromStringz; 306 307 statfs* mntbufsPtr; 308 int mntbufsLen = getmntinfo(&mntbufsPtr, 0); 309 if (mntbufsLen) { 310 auto mntbufs = mntbufsPtr[0..mntbufsLen]; 311 312 foreach(buf; mntbufs) { 313 auto type = fromStringz(buf.f_fstypename.ptr); 314 auto fsName = fromStringz(buf.f_mntfromname.ptr); 315 auto mountDir = fromStringz(buf.f_mntonname.ptr); 316 317 if (mountDir == "/" || isSpecialFileSystem(mountDir, type)) { 318 continue; 319 } 320 321 string label = getDeviceLabelFallback(type, fsName, mountDir); 322 res ~= RootEntry(getDeviceRootEntryType(type), mountDir.idup, label.toUTF32); 323 } 324 } 325 } 326 327 version (Windows) { 328 import core.sys.windows.windows; 329 uint mask = GetLogicalDrives(); 330 foreach(int i; 0 .. 26) { 331 if (mask & (1 << i)) { 332 char letter = cast(char)('A' + i); 333 string path = "" ~ letter ~ ":\\"; 334 dstring display = ""d ~ letter ~ ":"d; 335 // detect drive type 336 RootEntryType type; 337 uint wtype = GetDriveTypeA(("" ~ path).ptr); 338 //Log.d("Drive ", path, " type ", wtype); 339 switch (wtype) { 340 case DRIVE_REMOVABLE: 341 type = RootEntryType.REMOVABLE; 342 break; 343 case DRIVE_REMOTE: 344 type = RootEntryType.NETWORK; 345 break; 346 case DRIVE_CDROM: 347 type = RootEntryType.CDROM; 348 break; 349 default: 350 type = RootEntryType.FIXED; 351 break; 352 } 353 res ~= RootEntry(type, path, display); 354 } 355 } 356 } 357 return res; 358 } 359 360 version(Windows) 361 { 362 private: 363 import core.sys.windows.windows; 364 import core.sys.windows.shlobj; 365 import core.sys.windows.wtypes; 366 import core.sys.windows.objbase; 367 import core.sys.windows.objidl; 368 369 pragma(lib, "Ole32"); 370 371 alias GUID KNOWNFOLDERID; 372 373 extern(Windows) @nogc @system HRESULT _dummy_SHGetKnownFolderPath(const(KNOWNFOLDERID)* rfid, DWORD dwFlags, HANDLE hToken, wchar** ppszPath) nothrow; 374 375 enum KNOWNFOLDERID FOLDERID_Links = {0xbfb9d5e0, 0xc6a9, 0x404c, [0xb2,0xb2,0xae,0x6d,0xb6,0xaf,0x49,0x68]}; 376 } 377 378 /// returns array of user bookmarked directories 379 RootEntry[] getBookmarkPaths() nothrow 380 { 381 RootEntry[] res; 382 version(OSX) { 383 384 } else version(Android) { 385 386 } else version(Posix) { 387 /* 388 * Probably we should follow https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/ but it requires XML library. 389 * So for now just try to read GTK3 bookmarks. Should be compatible with GTK file dialogs, Nautilus and other GTK file managers. 390 */ 391 392 import std.string : startsWith; 393 import std.stdio : File; 394 import std.exception : collectException; 395 import std.uri : decode; 396 try { 397 enum fileProtocol = "file://"; 398 auto configPath = environment.get("XDG_CONFIG_HOME"); 399 if (!configPath.length) { 400 configPath = buildPath(homePath(), ".config"); 401 } 402 auto bookmarksFile = buildPath(configPath, "gtk-3.0/bookmarks"); 403 foreach(line; File(bookmarksFile, "r").byLineCopy()) { 404 if (line.startsWith(fileProtocol)) { 405 auto splitted = line.findSplit(" "); 406 string path; 407 if (splitted[1].length) { 408 path = splitted[0][fileProtocol.length..$]; 409 } else { 410 path = line[fileProtocol.length..$]; 411 } 412 path = decode(path); 413 if (path.isAbsolute) { 414 // Note: GTK supports regular files in bookmarks too, but we allow directories only. 415 bool dirExists; 416 collectException(path.isDir, dirExists); 417 if (dirExists) { 418 dstring label; 419 if (splitted[1].length) { 420 label = splitted[2].toUTF32; 421 } else { 422 label = path.baseName.toUTF32; 423 } 424 res ~= RootEntry(RootEntryType.BOOKMARK, path, label); 425 } 426 } 427 } 428 } 429 } catch(Exception e) { 430 431 } 432 } else version(Windows) { 433 /* 434 * This will not include bookmarks of special items and virtual folders like Recent Files or Recycle bin. 435 */ 436 437 import core.stdc.wchar_ : wcslen; 438 import std.exception : enforce; 439 import std.utf : toUTF16z; 440 import std.file : dirEntries, SpanMode; 441 import std.string : endsWith; 442 443 try { 444 auto shell = enforce(LoadLibraryA("Shell32")); 445 scope(exit) FreeLibrary(shell); 446 447 auto ptrSHGetKnownFolderPath = cast(typeof(&_dummy_SHGetKnownFolderPath))enforce(GetProcAddress(shell, "SHGetKnownFolderPath")); 448 449 wchar* linksFolderZ; 450 const linksGuid = FOLDERID_Links; 451 enforce(ptrSHGetKnownFolderPath(&linksGuid, 0, null, &linksFolderZ) == S_OK); 452 scope(exit) CoTaskMemFree(linksFolderZ); 453 454 string linksFolder = linksFolderZ[0..wcslen(linksFolderZ)].toUTF8; 455 456 enforce(SUCCEEDED(CoInitialize(null))); 457 scope(exit) CoUninitialize(); 458 459 HRESULT hres; 460 IShellLink psl; 461 462 auto clsidShellLink = CLSID_ShellLink; 463 auto iidShellLink = IID_IShellLinkW; 464 hres = CoCreateInstance(&clsidShellLink, null, CLSCTX.CLSCTX_INPROC_SERVER, &iidShellLink, cast(LPVOID*)&psl); 465 enforce(SUCCEEDED(hres), "Failed to create IShellLink instance"); 466 scope(exit) psl.Release(); 467 468 IPersistFile ppf; 469 auto iidPersistFile = IID_IPersistFile; 470 hres = psl.QueryInterface(cast(GUID*)&iidPersistFile, cast(void**)&ppf); 471 enforce(SUCCEEDED(hres), "Failed to query IPersistFile interface"); 472 scope(exit) ppf.Release(); 473 474 foreach(linkFile; dirEntries(linksFolder, SpanMode.shallow)) { 475 if (!linkFile.name.endsWith(".lnk")) { 476 continue; 477 } 478 try { 479 wchar[MAX_PATH] szGotPath; 480 WIN32_FIND_DATA wfd; 481 482 hres = ppf.Load(linkFile.name.toUTF16z, STGM_READ); 483 enforce(SUCCEEDED(hres), "Failed to load link file"); 484 485 hres = psl.Resolve(null, SLR_FLAGS.SLR_NO_UI); 486 enforce(SUCCEEDED(hres), "Failed to resolve link"); 487 488 hres = psl.GetPath(szGotPath.ptr, szGotPath.length, &wfd, 0); 489 enforce(SUCCEEDED(hres), "Failed to get path of link target"); 490 491 auto path = szGotPath[0..wcslen(szGotPath.ptr)]; 492 493 if (path.length && path.toUTF8.isDir) { 494 res ~= RootEntry(RootEntryType.BOOKMARK, path.toUTF8, linkFile.name.baseName.stripExtension.toUTF32); 495 } 496 } catch(Exception e) { 497 498 } 499 } 500 } catch(Exception e) { 501 502 } 503 } 504 return res; 505 } 506 507 /// returns true if directory is root directory (e.g. / or C:\) 508 bool isRoot(in string path) pure nothrow { 509 string root = rootName(path); 510 if (path.equal(root)) 511 return true; 512 return false; 513 } 514 515 /** 516 * Check if path is hidden. 517 */ 518 bool isHidden(in string path) nothrow { 519 version(Windows) { 520 import core.sys.windows.winnt : FILE_ATTRIBUTE_HIDDEN; 521 import std.exception : collectException; 522 uint attrs; 523 if (collectException(path.getAttributes(), attrs) is null) { 524 return (attrs & FILE_ATTRIBUTE_HIDDEN) != 0; 525 } else { 526 return false; 527 } 528 } else version(Posix) { 529 //TODO: check for hidden attribute on macOS 530 return path.baseName.startsWith("."); 531 } else { 532 return false; 533 } 534 } 535 536 /// 537 unittest 538 { 539 version(Posix) { 540 assert(!"path/to/normal_file".isHidden()); 541 assert("path/to/.hidden_file".isHidden()); 542 } 543 } 544 545 private bool isReadable(in string filePath) nothrow 546 { 547 version(Posix) { 548 import core.sys.posix.unistd : access, R_OK; 549 import std.string : toStringz; 550 return access(toStringz(filePath), R_OK) == 0; 551 } else { 552 // TODO: Windows version 553 return true; 554 } 555 } 556 557 private bool isWritable(in string filePath) nothrow 558 { 559 version(Posix) { 560 import core.sys.posix.unistd : access, W_OK; 561 import std.string : toStringz; 562 return access(toStringz(filePath), W_OK) == 0; 563 } else { 564 // TODO: Windows version 565 return true; 566 } 567 } 568 569 private bool isExecutable(in string filePath) nothrow 570 { 571 version(Windows) { 572 //TODO: Use GetEffectiveRightsFromAclW? For now just check extension 573 string extension = filePath.extension; 574 foreach(ext; [".exe", ".com", ".bat", ".cmd"]) { 575 if (filenameCmp(extension, ext) == 0) 576 return true; 577 } 578 return false; 579 } else version(Posix) { 580 import core.sys.posix.unistd : access, X_OK; 581 import std.string : toStringz; 582 return access(toStringz(filePath), X_OK) == 0; 583 } else { 584 return false; 585 } 586 } 587 588 /// returns parent directory for specified path 589 string parentDir(in string path) pure nothrow { 590 return buildNormalizedPath(path, ".."); 591 } 592 593 /// check filename with pattern 594 bool filterFilename(in string filename, in string pattern) pure nothrow { 595 return globMatch(filename.baseName, pattern); 596 } 597 /// Filters file name by pattern list 598 bool filterFilename(in string filename, in string[] filters) pure nothrow { 599 if (filters.length == 0) 600 return true; // no filters - show all 601 foreach(pattern; filters) { 602 if (filterFilename(filename, pattern)) 603 return true; 604 } 605 return false; 606 } 607 608 enum AttrFilter 609 { 610 none = 0, 611 files = 1 << 0, /// Include regular files that match the filters. 612 dirs = 1 << 1, /// Include directories. 613 hidden = 1 << 2, /// Include hidden files and directoroies. 614 parent = 1 << 3, /// Include parent directory (..). Takes effect only with includeDirs. 615 thisDir = 1 << 4, /// Include this directory (.). Takes effect only with includeDirs. 616 special = 1 << 5, /// Include special files (On Unix: socket and device files, FIFO) that match the filters. 617 readable = 1 << 6, /// Listing only readable files and directories. 618 writable = 1 << 7, /// Listing only writable files and directories. 619 executable = 1 << 8, /// Include only executable files. This filter does not affect directories. 620 allVisible = AttrFilter.files | AttrFilter.dirs, /// Include all non-hidden files and directories without parent directory, this directory and special files. 621 all = AttrFilter.allVisible | AttrFilter.hidden /// Include all files and directories including hidden ones but without parent directory, this directory and special files. 622 } 623 624 /** List directory content 625 626 Optionally filters file names by filter (not applied to directories). 627 628 Returns true if directory exists and listed successfully, false otherwise. 629 Throws: Exception if $(D dir) is not directory or some error occured during directory listing. 630 */ 631 DirEntry[] listDirectory(in string dir, AttrFilter attrFilter = AttrFilter.all, in string[] filters = []) 632 { 633 DirEntry[] entries; 634 635 DirEntry[] dirs; 636 DirEntry[] files; 637 foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) { 638 if (!(attrFilter & AttrFilter.hidden) && e.name.isHidden()) 639 continue; 640 if ((attrFilter & AttrFilter.readable) && !e.name.isReadable()) 641 continue; 642 if ((attrFilter & AttrFilter.writable) && !e.name.isWritable()) 643 continue; 644 if (!e.isDir && (attrFilter & AttrFilter.executable) && !e.name.isExecutable()) 645 continue; 646 if (e.isDir && (attrFilter & AttrFilter.dirs)) { 647 dirs ~= e; 648 } else if ((attrFilter & AttrFilter.files) && filterFilename(e.name, filters)) { 649 if (e.isFile) { 650 files ~= e; 651 } else if (attrFilter & AttrFilter.special) { 652 files ~= e; 653 } 654 } 655 } 656 if ((attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.thisDir) ) { 657 entries ~= DirEntry(appendPath(dir, ".")) ~ entries; 658 } 659 if (!isRoot(dir) && (attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.parent)) { 660 entries ~= DirEntry(appendPath(dir, "..")); 661 } 662 dirs.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); 663 files.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); 664 entries ~= dirs; 665 entries ~= files; 666 return entries; 667 } 668 669 /** List directory content 670 671 Optionally filters file names by filter (not applied to directories). 672 673 Result will be placed into entries array. 674 675 Returns true if directory exists and listed successfully, false otherwise. 676 */ 677 deprecated bool listDirectory(in string dir, in bool includeDirs, in bool includeFiles, in bool showHiddenFiles, in string[] filters, ref DirEntry[] entries, in bool showExecutables = false) { 678 entries.length = 0; 679 680 AttrFilter attrFilter; 681 if (includeDirs) { 682 attrFilter |= AttrFilter.dirs; 683 attrFilter |= AttrFilter.parent; 684 } 685 if (includeFiles) 686 attrFilter |= AttrFilter.files; 687 if (showHiddenFiles) 688 attrFilter |= AttrFilter.hidden; 689 if (showExecutables) 690 attrFilter |= AttrFilter.executable; 691 692 import std.exception : collectException; 693 return collectException(listDirectory(dir, attrFilter, filters), entries) is null; 694 } 695 696 /// Returns true if char ch is / or \ slash 697 bool isPathDelimiter(in char ch) pure nothrow { 698 return ch == '/' || ch == '\\'; 699 } 700 701 /// Returns current directory 702 alias currentDir = std.file.getcwd; 703 704 /// Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath() 705 @property string exePath() { 706 string path = thisExePath(); 707 int lastSlash = 0; 708 for (int i = cast(int)path.length - 1; i >= 0; i--) 709 if (path[i] == PATH_DELIMITER) { 710 lastSlash = i; 711 break; 712 } 713 return path[0 .. lastSlash + 1]; 714 } 715 716 /// Returns current executable path and file name 717 @property string exeFilename() { 718 return thisExePath(); 719 } 720 721 /** 722 Returns application data directory 723 724 On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter. 725 726 On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir. 727 */ 728 string appDataPath(string subdir = null) { 729 string path; 730 version (Windows) { 731 path = environment.get("APPDATA"); 732 } 733 if (path is null) 734 path = homePath; 735 if (subdir !is null) { 736 path ~= PATH_DELIMITER; 737 path ~= subdir; 738 } 739 return path; 740 } 741 742 /// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf 743 char[] convertPathDelimiters(char[] buf) { 744 foreach(ref ch; buf) { 745 version (Windows) { 746 if (ch == '/') 747 ch = '\\'; 748 } else { 749 if (ch == '\\') 750 ch = '/'; 751 } 752 } 753 return buf; 754 } 755 756 /// Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix) 757 string convertPathDelimiters(in string src) { 758 char[] buf = src.dup; 759 return cast(string)convertPathDelimiters(buf); 760 } 761 762 /// Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" 763 string appendPath(string[] pathItems ...) { 764 char[] buf; 765 foreach (s; pathItems) { 766 if (buf.length && !isPathDelimiter(buf[$-1])) 767 buf ~= PATH_DELIMITER; 768 buf ~= s; 769 } 770 return convertPathDelimiters(buf).dup; 771 } 772 773 /// Appends file path parts with proper delimiters (as well converts delimiters inside path to system) to buffer e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" 774 char[] appendPath(char[] buf, string[] pathItems ...) { 775 foreach (s; pathItems) { 776 if (buf.length && !isPathDelimiter(buf[$-1])) 777 buf ~= PATH_DELIMITER; 778 buf ~= s; 779 } 780 return convertPathDelimiters(buf); 781 } 782 783 /** Deprecated: use std.path.pathSplitter instead. 784 Splits path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"] 785 */ 786 deprecated string[] splitPath(string path) { 787 string[] res; 788 int start = 0; 789 for (int i = 0; i <= path.length; i++) { 790 char ch = i < path.length ? path[i] : 0; 791 if (ch == '\\' || ch == '/' || ch == 0) { 792 if (start < i) 793 res ~= path[start .. i].dup; 794 start = i + 1; 795 } 796 } 797 return res; 798 } 799 800 /// if pathName is not absolute path, convert it to absolute (assuming it is relative to current directory) 801 string toAbsolutePath(string pathName) { 802 import std.path : isAbsolute, absolutePath, buildNormalizedPath; 803 if (pathName.isAbsolute) 804 return pathName; 805 return pathName.absolutePath.buildNormalizedPath; 806 } 807 808 /// for executable name w/o path, find absolute path to executable 809 string findExecutablePath(string executableName) { 810 import std.string : split; 811 version (Windows) { 812 if (!executableName.endsWith(".exe")) 813 executableName = executableName ~ ".exe"; 814 } 815 string currentExeDir = dirName(thisExePath()); 816 string inCurrentExeDir = absolutePath(buildNormalizedPath(currentExeDir, executableName)); 817 if (exists(inCurrentExeDir) && isFile(inCurrentExeDir)) 818 return inCurrentExeDir; // found in current directory 819 string pathVariable = environment.get("PATH"); 820 if (!pathVariable) 821 return null; 822 string[] paths = pathVariable.split(pathSeparator); 823 foreach(path; paths) { 824 string pathname = absolutePath(buildNormalizedPath(path, executableName)); 825 if (exists(pathname) && isFile(pathname)) 826 return pathname; 827 } 828 return null; 829 }