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 /** 717 Returns application data directory 718 719 On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter. 720 721 On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir. 722 */ 723 string appDataPath(string subdir = null) { 724 string path; 725 version (Windows) { 726 path = environment.get("APPDATA"); 727 } 728 if (path is null) 729 path = homePath; 730 if (subdir !is null) { 731 path ~= PATH_DELIMITER; 732 path ~= subdir; 733 } 734 return path; 735 } 736 737 /// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf 738 char[] convertPathDelimiters(char[] buf) { 739 foreach(ref ch; buf) { 740 version (Windows) { 741 if (ch == '/') 742 ch = '\\'; 743 } else { 744 if (ch == '\\') 745 ch = '/'; 746 } 747 } 748 return buf; 749 } 750 751 /// Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix) 752 string convertPathDelimiters(in string src) { 753 char[] buf = src.dup; 754 return cast(string)convertPathDelimiters(buf); 755 } 756 757 /// Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" 758 string appendPath(string[] pathItems ...) { 759 char[] buf; 760 foreach (s; pathItems) { 761 if (buf.length && !isPathDelimiter(buf[$-1])) 762 buf ~= PATH_DELIMITER; 763 buf ~= s; 764 } 765 return convertPathDelimiters(buf).dup; 766 } 767 768 /// 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" 769 char[] appendPath(char[] buf, string[] pathItems ...) { 770 foreach (s; pathItems) { 771 if (buf.length && !isPathDelimiter(buf[$-1])) 772 buf ~= PATH_DELIMITER; 773 buf ~= s; 774 } 775 return convertPathDelimiters(buf); 776 } 777 778 /** Deprecated: use std.path.pathSplitter instead. 779 Splits path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"] 780 */ 781 deprecated string[] splitPath(string path) { 782 string[] res; 783 int start = 0; 784 for (int i = 0; i <= path.length; i++) { 785 char ch = i < path.length ? path[i] : 0; 786 if (ch == '\\' || ch == '/' || ch == 0) { 787 if (start < i) 788 res ~= path[start .. i].dup; 789 start = i + 1; 790 } 791 } 792 return res; 793 } 794 795 /// for executable name w/o path, find absolute path to executable 796 string findExecutablePath(string executableName) { 797 import std.string : split; 798 version (Windows) { 799 if (!executableName.endsWith(".exe")) 800 executableName = executableName ~ ".exe"; 801 } 802 string currentExeDir = dirName(thisExePath()); 803 string inCurrentExeDir = absolutePath(buildNormalizedPath(currentExeDir, executableName)); 804 if (exists(inCurrentExeDir) && isFile(inCurrentExeDir)) 805 return inCurrentExeDir; // found in current directory 806 string pathVariable = environment.get("PATH"); 807 if (!pathVariable) 808 return null; 809 string[] paths = pathVariable.split(pathSeparator); 810 foreach(path; paths) { 811 string pathname = absolutePath(buildNormalizedPath(path, executableName)); 812 if (exists(pathname) && isFile(pathname)) 813 return pathname; 814 } 815 return null; 816 }