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 }