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 }