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 }