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 }