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 alias GUID KNOWNFOLDERID; 370 371 extern(Windows) @nogc @system HRESULT _dummy_SHGetKnownFolderPath(const(KNOWNFOLDERID)* rfid, DWORD dwFlags, HANDLE hToken, wchar** ppszPath) nothrow; 372 373 enum KNOWNFOLDERID FOLDERID_Links = {0xbfb9d5e0, 0xc6a9, 0x404c, [0xb2,0xb2,0xae,0x6d,0xb6,0xaf,0x49,0x68]}; 374 } 375 376 /// returns array of user bookmarked directories 377 RootEntry[] getBookmarkPaths() nothrow 378 { 379 RootEntry[] res; 380 version(OSX) { 381 382 } else version(Android) { 383 384 } else version(Posix) { 385 /* 386 * Probably we should follow https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/ but it requires XML library. 387 * So for now just try to read GTK3 bookmarks. Should be compatible with GTK file dialogs, Nautilus and other GTK file managers. 388 */ 389 390 import std.string : startsWith; 391 import std.stdio : File; 392 import std.exception : collectException; 393 import std.uri : decode; 394 try { 395 enum fileProtocol = "file://"; 396 auto configPath = environment.get("XDG_CONFIG_HOME"); 397 if (!configPath.length) { 398 configPath = buildPath(homePath(), ".config"); 399 } 400 auto bookmarksFile = buildPath(configPath, "gtk-3.0/bookmarks"); 401 foreach(line; File(bookmarksFile, "r").byLineCopy()) { 402 if (line.startsWith(fileProtocol)) { 403 auto splitted = line.findSplit(" "); 404 string path; 405 if (splitted[1].length) { 406 path = splitted[0][fileProtocol.length..$]; 407 } else { 408 path = line[fileProtocol.length..$]; 409 } 410 path = decode(path); 411 if (path.isAbsolute) { 412 // Note: GTK supports regular files in bookmarks too, but we allow directories only. 413 bool dirExists; 414 collectException(path.isDir, dirExists); 415 if (dirExists) { 416 dstring label; 417 if (splitted[1].length) { 418 label = splitted[2].toUTF32; 419 } else { 420 label = path.baseName.toUTF32; 421 } 422 res ~= RootEntry(RootEntryType.BOOKMARK, path, label); 423 } 424 } 425 } 426 } 427 } catch(Exception e) { 428 429 } 430 } else version(Windows) { 431 /* 432 * This will not include bookmarks of special items and virtual folders like Recent Files or Recycle bin. 433 */ 434 435 import core.stdc.wchar_ : wcslen; 436 import std.exception : enforce; 437 import std.utf : toUTF16z; 438 import std.file : dirEntries, SpanMode; 439 import std.string : endsWith; 440 441 try { 442 auto shell = enforce(LoadLibraryA("Shell32")); 443 scope(exit) FreeLibrary(shell); 444 445 auto ptrSHGetKnownFolderPath = cast(typeof(&_dummy_SHGetKnownFolderPath))enforce(GetProcAddress(shell, "SHGetKnownFolderPath")); 446 447 wchar* linksFolderZ; 448 const linksGuid = FOLDERID_Links; 449 enforce(ptrSHGetKnownFolderPath(&linksGuid, 0, null, &linksFolderZ) == S_OK); 450 scope(exit) CoTaskMemFree(linksFolderZ); 451 452 string linksFolder = linksFolderZ[0..wcslen(linksFolderZ)].toUTF8; 453 454 enforce(SUCCEEDED(CoInitialize(null))); 455 scope(exit) CoUninitialize(); 456 457 HRESULT hres; 458 IShellLink psl; 459 460 auto clsidShellLink = CLSID_ShellLink; 461 auto iidShellLink = IID_IShellLinkW; 462 hres = CoCreateInstance(&clsidShellLink, null, CLSCTX.CLSCTX_INPROC_SERVER, &iidShellLink, cast(LPVOID*)&psl); 463 enforce(SUCCEEDED(hres), "Failed to create IShellLink instance"); 464 scope(exit) psl.Release(); 465 466 IPersistFile ppf; 467 auto iidPersistFile = IID_IPersistFile; 468 hres = psl.QueryInterface(cast(GUID*)&iidPersistFile, cast(void**)&ppf); 469 enforce(SUCCEEDED(hres), "Failed to query IPersistFile interface"); 470 scope(exit) ppf.Release(); 471 472 foreach(linkFile; dirEntries(linksFolder, SpanMode.shallow)) { 473 if (!linkFile.name.endsWith(".lnk")) { 474 continue; 475 } 476 try { 477 wchar[MAX_PATH] szGotPath; 478 WIN32_FIND_DATA wfd; 479 480 hres = ppf.Load(linkFile.name.toUTF16z, STGM_READ); 481 enforce(SUCCEEDED(hres), "Failed to load link file"); 482 483 hres = psl.Resolve(null, SLR_FLAGS.SLR_NO_UI); 484 enforce(SUCCEEDED(hres), "Failed to resolve link"); 485 486 hres = psl.GetPath(szGotPath.ptr, szGotPath.length, &wfd, 0); 487 enforce(SUCCEEDED(hres), "Failed to get path of link target"); 488 489 auto path = szGotPath[0..wcslen(szGotPath.ptr)]; 490 491 if (path.length && path.toUTF8.isDir) { 492 res ~= RootEntry(RootEntryType.BOOKMARK, path.toUTF8, linkFile.name.baseName.stripExtension.toUTF32); 493 } 494 } catch(Exception e) { 495 496 } 497 } 498 } catch(Exception e) { 499 500 } 501 } 502 return res; 503 } 504 505 /// returns true if directory is root directory (e.g. / or C:\\) 506 bool isRoot(in string path) pure nothrow { 507 string root = rootName(path); 508 if (path.equal(root)) 509 return true; 510 return false; 511 } 512 513 /** 514 * Check if path is hidden. 515 */ 516 bool isHidden(in string path) nothrow { 517 version(Windows) { 518 import core.sys.windows.winnt : FILE_ATTRIBUTE_HIDDEN; 519 import std.exception : collectException; 520 uint attrs; 521 if (collectException(path.getAttributes(), attrs) is null) { 522 return (attrs & FILE_ATTRIBUTE_HIDDEN) != 0; 523 } else { 524 return false; 525 } 526 } else version(Posix) { 527 //TODO: check for hidden attribute on macOS 528 return path.baseName.startsWith("."); 529 } else { 530 return false; 531 } 532 } 533 534 /// 535 unittest 536 { 537 version(Posix) { 538 assert(!"path/to/normal_file".isHidden()); 539 assert("path/to/.hidden_file".isHidden()); 540 } 541 } 542 543 private bool isReadable(in string filePath) nothrow 544 { 545 version(Posix) { 546 import core.sys.posix.unistd : access, R_OK; 547 import std.string : toStringz; 548 return access(toStringz(filePath), R_OK) == 0; 549 } else { 550 // TODO: Windows version 551 return true; 552 } 553 } 554 555 private bool isWritable(in string filePath) nothrow 556 { 557 version(Posix) { 558 import core.sys.posix.unistd : access, W_OK; 559 import std.string : toStringz; 560 return access(toStringz(filePath), W_OK) == 0; 561 } else { 562 // TODO: Windows version 563 return true; 564 } 565 } 566 567 private bool isExecutable(in string filePath) nothrow 568 { 569 version(Windows) { 570 //TODO: Use GetEffectiveRightsFromAclW? For now just check extension 571 string extension = filePath.extension; 572 foreach(ext; [".exe", ".com", ".bat", ".cmd"]) { 573 if (filenameCmp(extension, ext) == 0) 574 return true; 575 } 576 return false; 577 } else version(Posix) { 578 import core.sys.posix.unistd : access, X_OK; 579 import std.string : toStringz; 580 return access(toStringz(filePath), X_OK) == 0; 581 } else { 582 return false; 583 } 584 } 585 586 /// returns parent directory for specified path 587 string parentDir(in string path) pure nothrow { 588 return buildNormalizedPath(path, ".."); 589 } 590 591 /// check filename with pattern 592 bool filterFilename(in string filename, in string pattern) pure nothrow { 593 return globMatch(filename.baseName, pattern); 594 } 595 /// Filters file name by pattern list 596 bool filterFilename(in string filename, in string[] filters) pure nothrow { 597 if (filters.length == 0) 598 return true; // no filters - show all 599 foreach(pattern; filters) { 600 if (filterFilename(filename, pattern)) 601 return true; 602 } 603 return false; 604 } 605 606 enum AttrFilter 607 { 608 none = 0, 609 files = 1 << 0, /// Include regular files that match the filters. 610 dirs = 1 << 1, /// Include directories. 611 hidden = 1 << 2, /// Include hidden files and directoroies. 612 parent = 1 << 3, /// Include parent directory (..). Takes effect only with includeDirs. 613 thisDir = 1 << 4, /// Include this directory (.). Takes effect only with includeDirs. 614 special = 1 << 5, /// Include special files (On Unix: socket and device files, FIFO) that match the filters. 615 readable = 1 << 6, /// Listing only readable files and directories. 616 writable = 1 << 7, /// Listing only writable files and directories. 617 executable = 1 << 8, /// Include only executable files. This filter does not affect directories. 618 allVisible = AttrFilter.files | AttrFilter.dirs, /// Include all non-hidden files and directories without parent directory, this directory and special files. 619 all = AttrFilter.allVisible | AttrFilter.hidden /// Include all files and directories including hidden ones but without parent directory, this directory and special files. 620 } 621 622 /** List directory content 623 624 Optionally filters file names by filter (not applied to directories). 625 626 Returns true if directory exists and listed successfully, false otherwise. 627 Throws: Exception if $(D dir) is not directory or some error occured during directory listing. 628 */ 629 DirEntry[] listDirectory(in string dir, AttrFilter attrFilter = AttrFilter.all, in string[] filters = []) 630 { 631 DirEntry[] entries; 632 633 DirEntry[] dirs; 634 DirEntry[] files; 635 foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) { 636 if (!(attrFilter & AttrFilter.hidden) && e.name.isHidden()) 637 continue; 638 if ((attrFilter & AttrFilter.readable) && !e.name.isReadable()) 639 continue; 640 if ((attrFilter & AttrFilter.writable) && !e.name.isWritable()) 641 continue; 642 if (!e.isDir && (attrFilter & AttrFilter.executable) && !e.name.isExecutable()) 643 continue; 644 if (e.isDir && (attrFilter & AttrFilter.dirs)) { 645 dirs ~= e; 646 } else if ((attrFilter & AttrFilter.files) && filterFilename(e.name, filters)) { 647 if (e.isFile) { 648 files ~= e; 649 } else if (attrFilter & AttrFilter.special) { 650 files ~= e; 651 } 652 } 653 } 654 if ((attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.thisDir) ) { 655 entries ~= DirEntry(appendPath(dir, ".")) ~ entries; 656 } 657 if (!isRoot(dir) && (attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.parent)) { 658 entries ~= DirEntry(appendPath(dir, "..")); 659 } 660 dirs.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); 661 files.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); 662 entries ~= dirs; 663 entries ~= files; 664 return entries; 665 } 666 667 /** List directory content 668 669 Optionally filters file names by filter (not applied to directories). 670 671 Result will be placed into entries array. 672 673 Returns true if directory exists and listed successfully, false otherwise. 674 */ 675 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) { 676 entries.length = 0; 677 678 AttrFilter attrFilter; 679 if (includeDirs) { 680 attrFilter |= AttrFilter.dirs; 681 attrFilter |= AttrFilter.parent; 682 } 683 if (includeFiles) 684 attrFilter |= AttrFilter.files; 685 if (showHiddenFiles) 686 attrFilter |= AttrFilter.hidden; 687 if (showExecutables) 688 attrFilter |= AttrFilter.executable; 689 690 import std.exception : collectException; 691 return collectException(listDirectory(dir, attrFilter, filters), entries) is null; 692 } 693 694 /// Returns true if char ch is / or \ slash 695 bool isPathDelimiter(in char ch) pure nothrow { 696 return ch == '/' || ch == '\\'; 697 } 698 699 /// Returns current directory 700 alias currentDir = std.file.getcwd; 701 702 /// Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath() 703 @property string exePath() { 704 string path = thisExePath(); 705 int lastSlash = 0; 706 for (int i = cast(int)path.length - 1; i >= 0; i--) 707 if (path[i] == PATH_DELIMITER) { 708 lastSlash = i; 709 break; 710 } 711 return path[0 .. lastSlash + 1]; 712 } 713 714 /// Returns current executable path and file name 715 @property string exeFilename() { 716 return thisExePath(); 717 } 718 719 /** 720 Returns application data directory 721 722 On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter. 723 724 On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir. 725 */ 726 string appDataPath(string subdir = null) { 727 string path; 728 version (Windows) { 729 path = environment.get("APPDATA"); 730 } 731 if (path is null) 732 path = homePath; 733 if (subdir !is null) { 734 path ~= PATH_DELIMITER; 735 path ~= subdir; 736 } 737 return path; 738 } 739 740 /// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf 741 char[] convertPathDelimiters(char[] buf) { 742 foreach(ref ch; buf) { 743 version (Windows) { 744 if (ch == '/') 745 ch = '\\'; 746 } else { 747 if (ch == '\\') 748 ch = '/'; 749 } 750 } 751 return buf; 752 } 753 754 /// Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix) 755 string convertPathDelimiters(in string src) { 756 char[] buf = src.dup; 757 return cast(string)convertPathDelimiters(buf); 758 } 759 760 /// Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" 761 string appendPath(string[] pathItems ...) { 762 char[] buf; 763 foreach (s; pathItems) { 764 if (buf.length && !isPathDelimiter(buf[$-1])) 765 buf ~= PATH_DELIMITER; 766 buf ~= s; 767 } 768 return convertPathDelimiters(buf).dup; 769 } 770 771 /// 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" 772 char[] appendPath(char[] buf, string[] pathItems ...) { 773 foreach (s; pathItems) { 774 if (buf.length && !isPathDelimiter(buf[$-1])) 775 buf ~= PATH_DELIMITER; 776 buf ~= s; 777 } 778 return convertPathDelimiters(buf); 779 } 780 781 /** Deprecated: use std.path.pathSplitter instead. 782 Splits path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"] 783 */ 784 deprecated string[] splitPath(string path) { 785 string[] res; 786 int start = 0; 787 for (int i = 0; i <= path.length; i++) { 788 char ch = i < path.length ? path[i] : 0; 789 if (ch == '\\' || ch == '/' || ch == 0) { 790 if (start < i) 791 res ~= path[start .. i].dup; 792 start = i + 1; 793 } 794 } 795 return res; 796 } 797 798 /// if pathName is not absolute path, convert it to absolute (assuming it is relative to current directory) 799 string toAbsolutePath(string pathName) { 800 import std.path : isAbsolute, absolutePath, buildNormalizedPath; 801 if (pathName.isAbsolute) 802 return pathName; 803 return pathName.absolutePath.buildNormalizedPath; 804 } 805 806 /// for executable name w/o path, find absolute path to executable 807 string findExecutablePath(string executableName) { 808 import std.string : split; 809 version (Windows) { 810 if (!executableName.endsWith(".exe")) 811 executableName = executableName ~ ".exe"; 812 } 813 string currentExeDir = dirName(thisExePath()); 814 string inCurrentExeDir = absolutePath(buildNormalizedPath(currentExeDir, executableName)); 815 if (exists(inCurrentExeDir) && isFile(inCurrentExeDir)) 816 return inCurrentExeDir; // found in current directory 817 string pathVariable = environment.get("PATH"); 818 if (!pathVariable) 819 return null; 820 string[] paths = pathVariable.split(pathSeparator); 821 foreach(path; paths) { 822 string pathname = absolutePath(buildNormalizedPath(path, executableName)); 823 if (exists(pathname) && isFile(pathname)) 824 return pathname; 825 } 826 return null; 827 }