1 // Written in the D programming language.
2 
3 /**
4 
5 This module contains cross-platform file access utilities
6 
7 
8 
9 Synopsis:
10 
11 ----
12 import dlangui.core.files;
13 ----
14 
15 Copyright: Vadim Lopatin, 2014
16 License:   Boost License 1.0
17 Authors:   Vadim Lopatin, coolreader.org@gmail.com
18 */
19 module dlangui.core.files;
20 
21 import std.algorithm;
22 
23 private import dlangui.core.logger;
24 private import std.process;
25 private import std.path;
26 private import std.file;
27 private import std.utf;
28 
29 
30 /// path delimiter (\ for windows, / for others)
31 enum char PATH_DELIMITER = dirSeparator[0];
32 
33 /// Filesystem root entry / bookmark types
34 enum RootEntryType : uint {
35     /// filesystem root
36     ROOT,
37     /// current user home
38     HOME,
39     /// removable drive
40     REMOVABLE,
41     /// fixed drive
42     FIXED,
43     /// network
44     NETWORK,
45     /// cd rom
46     CDROM,
47     /// sd card
48     SDCARD,
49     /// custom bookmark
50     BOOKMARK,
51 }
52 
53 /// Filesystem root entry item
54 struct RootEntry {
55     private RootEntryType _type;
56     private string _path;
57     private dstring _display;
58     this(RootEntryType type, string path, dstring display = null) {
59         _type = type;
60         _path = path;
61         _display = display;
62         if (display is null) {
63             _display = toUTF32(baseName(path));
64         }
65     }
66     /// Returns type
67     @property RootEntryType type() { return _type; }
68     /// Returns path
69     @property string path() { return _path; }
70     /// Returns display label
71     @property dstring label() { return _display; }
72     /// Returns icon resource id
73     @property string icon() {
74         switch (type) with(RootEntryType)
75         {
76             case NETWORK:
77                 return "folder-network";
78             case BOOKMARK:
79                 return "folder-bookmark";
80             case CDROM:
81                 return "drive-optical";
82             case FIXED:
83                 return "drive-harddisk";
84             case HOME:
85                 return "user-home";
86             case ROOT:
87                 return "computer";
88             case SDCARD:
89                 return "media-flash-sd-mmc";
90             case REMOVABLE:
91                 return "device-removable-media";
92             default:
93                 return "folder-blue";
94         }
95     }
96 }
97 
98 /// Returns user's home directory entry
99 @property RootEntry homeEntry() {
100     return RootEntry(RootEntryType.HOME, homePath);
101 }
102 
103 /// Returns user's home directory
104 @property string homePath() {
105     string path;
106     version (Windows) {
107         path = environment.get("USERPROFILE");
108         if (path is null)
109             path = environment.get("HOME");
110     } else {
111         path = environment.get("HOME");
112     }
113     if (path is null)
114         path = "."; // fallback to current directory
115     return path;
116 }
117 
118 version(OSX) {} else version(Posix)
119 {
120     private bool isSpecialFileSystem(const(char)[] dir, const(char)[] type)
121     {
122         import std.string : startsWith;
123         if (dir.startsWith("/dev") || dir.startsWith("/proc") || dir.startsWith("/sys") || 
124             dir.startsWith("/var/run") || dir.startsWith("/var/lock"))
125         {
126             return true;
127         }
128         
129         if (type == "tmpfs" || type == "rootfs" || type == "rpc_pipefs") {
130             return true;
131         }
132         return false;
133     }
134 
135     private string getDeviceLabelFallback(in char[] type, in char[] fsName, in char[] mountDir)
136     {
137         import std.format : format;
138         if (type == "vboxsf") {
139             return "VirtualBox shared folder";
140         }
141         if (type == "fuse.gvfsd-fuse") {
142             return "GNOME Virtual file system";
143         }
144         return format("%s (%s)", mountDir.baseName, type);
145     }
146  
147     private RootEntryType getDeviceRootEntryType(in char[] type)
148     {
149         switch(type)
150         {
151             case "iso9660":
152                 return RootEntryType.CDROM;
153             case "vfat":
154                 return RootEntryType.REMOVABLE;
155             case "cifs":
156             case "davfs":
157             case "fuse.sshfs":
158             case "nfs":
159             case "nfs4":
160                 return RootEntryType.NETWORK;
161             default:
162                 return RootEntryType.FIXED;
163          }
164     }
165 }
166 
167 version(FreeBSD)
168 {
169 private:
170     import core.sys.posix.sys.types;
171     
172     enum MFSNAMELEN = 16;          /* length of type name including null */
173     enum MNAMELEN  = 88;          /* size of on/from name bufs */
174     enum STATFS_VERSION = 0x20030518;      /* current version number */
175     
176     struct fsid_t
177     {
178         int[2] val;
179     }
180     
181     struct statfs {
182         uint f_version;         /* structure version number */
183         uint f_type;            /* type of filesystem */
184         ulong f_flags;           /* copy of mount exported flags */
185         ulong f_bsize;           /* filesystem fragment size */
186         ulong f_iosize;          /* optimal transfer block size */
187         ulong f_blocks;          /* total data blocks in filesystem */
188         ulong f_bfree;           /* free blocks in filesystem */
189         long  f_bavail;          /* free blocks avail to non-superuser */
190         ulong f_files;           /* total file nodes in filesystem */
191         long  f_ffree;           /* free nodes avail to non-superuser */
192         ulong f_syncwrites;      /* count of sync writes since mount */
193         ulong f_asyncwrites;         /* count of async writes since mount */
194         ulong f_syncreads;       /* count of sync reads since mount */
195         ulong f_asyncreads;      /* count of async reads since mount */
196         ulong[10] f_spare;       /* unused spare */
197         uint f_namemax;         /* maximum filename length */
198         uid_t     f_owner;          /* user that mounted the filesystem */
199         fsid_t    f_fsid;           /* filesystem id */
200         char[80]      f_charspare;      /* spare string space */
201         char[MFSNAMELEN] f_fstypename; /* filesystem type name */
202         char[MNAMELEN] f_mntfromname;  /* mounted filesystem */
203         char[MNAMELEN] f_mntonname;    /* directory on which mounted */
204     };
205     
206     extern(C) @nogc nothrow 
207     {
208         int getmntinfo(statfs **mntbufp, int flags);
209     }
210 }
211 
212 version(linux)
213 {
214 private:
215     import core.stdc.stdio : FILE;
216     struct mntent
217     {
218         char *mnt_fsname;   /* Device or server for filesystem.  */
219         char *mnt_dir;      /* Directory mounted on.  */
220         char *mnt_type;     /* Type of filesystem: ufs, nfs, etc.  */
221         char *mnt_opts;     /* Comma-separated options for fs.  */
222         int mnt_freq;       /* Dump frequency (in days).  */
223         int mnt_passno;     /* Pass number for `fsck'.  */
224     };
225     
226     extern(C) @nogc nothrow 
227     {
228         FILE *setmntent(const char *file, const char *mode);
229         mntent *getmntent(FILE *stream);
230         mntent *getmntent_r(FILE * stream, mntent *result, char * buffer, int bufsize);
231         int addmntent(FILE* stream, const mntent *mnt);
232         int endmntent(FILE * stream);
233         char *hasmntopt(const mntent *mnt, const char *opt);
234     }
235     
236     string unescapeLabel(string label)
237     {
238         import std.string : replace;
239         return label.replace("\\x20", " ")
240                     .replace("\\x9", " ") //actually tab
241                     .replace("\\x5c", "\\")
242                     .replace("\\xA", " "); //actually newline
243     }
244 }
245 
246 /// returns array of system root entries
247 @property RootEntry[] getRootPaths() {
248     RootEntry[] res;
249     res ~= RootEntry(RootEntryType.HOME, homePath);
250     version (Posix) {
251         res ~= RootEntry(RootEntryType.ROOT, "/", "File System"d);
252     }
253     version(Android) {
254         // do nothing
255     } else version(linux) {
256         import std.string : fromStringz;
257         import std.exception : collectException;
258         
259         mntent ent;
260         char[1024] buf;
261         FILE* f = setmntent("/etc/mtab", "r");
262         
263         if (f) {
264             scope(exit) endmntent(f);
265             while(getmntent_r(f, &ent, buf.ptr, cast(int)buf.length) !is null) {
266                 auto fsName = fromStringz(ent.mnt_fsname);
267                 auto mountDir = fromStringz(ent.mnt_dir);
268                 auto type = fromStringz(ent.mnt_type);
269                 
270                 if (mountDir == "/" || //root is already added
271                     isSpecialFileSystem(mountDir, type)) //don't list special file systems
272                 {
273                     continue;
274                 }
275                 
276                 string label;
277                 enum byLabel = "/dev/disk/by-label";
278                 if (fsName.isAbsolute) {
279                     try {
280                         foreach(entry; dirEntries(byLabel, SpanMode.shallow))
281                         {
282                                 string resolvedLink;
283                                 if (entry.isSymlink && collectException(entry.readLink, resolvedLink) is null) {
284                                     auto normalized = buildNormalizedPath(byLabel, resolvedLink);
285                                 if (normalized == fsName) {
286                                     label = entry.name.baseName.unescapeLabel();
287                                 }
288                             }
289                         }
290                     } catch(Exception e) {
291 
292                     }
293                 }
294                 
295                 if (!label.length) {
296                     label = getDeviceLabelFallback(type, fsName, mountDir);
297                 }
298                 auto entryType = getDeviceRootEntryType(type);
299                 res ~= RootEntry(entryType, mountDir.idup, label.toUTF32);
300             }   
301         }
302     }
303     
304     version(FreeBSD) {
305         import std.string : fromStringz;
306         
307         statfs* mntbufsPtr;
308         int mntbufsLen = getmntinfo(&mntbufsPtr, 0);
309         if (mntbufsLen) {
310             auto mntbufs = mntbufsPtr[0..mntbufsLen];
311             
312             foreach(buf; mntbufs) {
313                 auto type = fromStringz(buf.f_fstypename.ptr);
314                 auto fsName = fromStringz(buf.f_mntfromname.ptr);
315                 auto mountDir = fromStringz(buf.f_mntonname.ptr);
316                 
317                 if (mountDir == "/" || isSpecialFileSystem(mountDir, type)) {
318                     continue;
319                 }
320                 
321                 string label = getDeviceLabelFallback(type, fsName, mountDir);
322                 res ~= RootEntry(getDeviceRootEntryType(type), mountDir.idup, label.toUTF32);
323             }
324         }
325     }
326     
327     version (Windows) {
328         import core.sys.windows.windows;
329         uint mask = GetLogicalDrives();
330         foreach(int i; 0 .. 26) {
331             if (mask & (1 << i)) {
332                 char letter = cast(char)('A' + i);
333                 string path = "" ~ letter ~ ":\\";
334                 dstring display = ""d ~ letter ~ ":"d;
335                 // detect drive type
336                 RootEntryType type;
337                 uint wtype = GetDriveTypeA(("" ~ path).ptr);
338                 //Log.d("Drive ", path, " type ", wtype);
339                 switch (wtype) {
340                     case DRIVE_REMOVABLE:
341                         type = RootEntryType.REMOVABLE;
342                         break;
343                     case DRIVE_REMOTE:
344                         type = RootEntryType.NETWORK;
345                         break;
346                     case DRIVE_CDROM:
347                         type = RootEntryType.CDROM;
348                         break;
349                     default:
350                         type = RootEntryType.FIXED;
351                         break;
352                 }
353                 res ~= RootEntry(type, path, display);
354             }
355         }
356     }
357     return res;
358 }
359 
360 version(Windows)
361 {
362 private:
363     import core.sys.windows.windows;
364     import core.sys.windows.shlobj;
365     import core.sys.windows.wtypes;
366     import core.sys.windows.objbase;
367     import core.sys.windows.objidl;
368 
369     pragma(lib, "Ole32");
370 
371     alias GUID KNOWNFOLDERID;
372     
373     extern(Windows) @nogc @system HRESULT _dummy_SHGetKnownFolderPath(const(KNOWNFOLDERID)* rfid, DWORD dwFlags, HANDLE hToken, wchar** ppszPath) nothrow;
374     
375     enum KNOWNFOLDERID FOLDERID_Links = {0xbfb9d5e0, 0xc6a9, 0x404c, [0xb2,0xb2,0xae,0x6d,0xb6,0xaf,0x49,0x68]};
376 }
377 
378 /// returns array of user bookmarked directories
379 RootEntry[] getBookmarkPaths() nothrow
380 {
381     RootEntry[] res;
382     version(OSX) {
383         
384     } else version(Android) {
385         
386     } else version(Posix) {
387         /*
388          * Probably we should follow https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/ but it requires XML library.
389          * So for now just try to read GTK3 bookmarks. Should be compatible with GTK file dialogs, Nautilus and other GTK file managers.
390          */
391         
392         import std.string : startsWith;
393         import std.stdio : File;
394         import std.exception : collectException;
395         import std.uri : decode;
396         try {
397             enum fileProtocol = "file://";
398             auto configPath = environment.get("XDG_CONFIG_HOME");
399             if (!configPath.length) {
400                 configPath = buildPath(homePath(), ".config");
401             }
402             auto bookmarksFile = buildPath(configPath, "gtk-3.0/bookmarks");
403             foreach(line; File(bookmarksFile, "r").byLineCopy()) {
404                 if (line.startsWith(fileProtocol)) {
405                     auto splitted = line.findSplit(" ");
406                     string path;
407                     if (splitted[1].length) {
408                         path = splitted[0][fileProtocol.length..$];
409                     } else {
410                         path = line[fileProtocol.length..$];
411                     }
412                     path = decode(path);
413                     if (path.isAbsolute) {
414                         // Note: GTK supports regular files in bookmarks too, but we allow directories only.
415                         bool dirExists;
416                         collectException(path.isDir, dirExists);
417                         if (dirExists) {
418                             dstring label;
419                             if (splitted[1].length) {
420                                 label = splitted[2].toUTF32;
421                             } else {
422                                 label = path.baseName.toUTF32;
423                             }
424                             res ~= RootEntry(RootEntryType.BOOKMARK, path, label);
425                         }
426                     }
427                 }
428             }
429         } catch(Exception e) {
430             
431         }
432     } else version(Windows) {
433         /*
434          * This will not include bookmarks of special items and virtual folders like Recent Files or Recycle bin.
435          */
436         
437         import core.stdc.wchar_ : wcslen;
438         import std.exception : enforce;
439         import std.utf : toUTF16z;
440         import std.file : dirEntries, SpanMode;
441         import std.string : endsWith;
442         
443         try {
444             auto shell = enforce(LoadLibraryA("Shell32"));
445             scope(exit) FreeLibrary(shell);
446             
447             auto ptrSHGetKnownFolderPath = cast(typeof(&_dummy_SHGetKnownFolderPath))enforce(GetProcAddress(shell, "SHGetKnownFolderPath"));
448             
449             wchar* linksFolderZ;
450             const linksGuid = FOLDERID_Links;
451             enforce(ptrSHGetKnownFolderPath(&linksGuid, 0, null, &linksFolderZ) == S_OK);
452             scope(exit) CoTaskMemFree(linksFolderZ);
453             
454             string linksFolder = linksFolderZ[0..wcslen(linksFolderZ)].toUTF8;
455             
456             enforce(SUCCEEDED(CoInitialize(null)));
457             scope(exit) CoUninitialize();
458 
459             HRESULT hres; 
460             IShellLink psl; 
461             
462             auto clsidShellLink = CLSID_ShellLink;
463             auto iidShellLink = IID_IShellLinkW;
464             hres = CoCreateInstance(&clsidShellLink, null, CLSCTX.CLSCTX_INPROC_SERVER, &iidShellLink, cast(LPVOID*)&psl);
465             enforce(SUCCEEDED(hres), "Failed to create IShellLink instance");
466             scope(exit) psl.Release();
467             
468             IPersistFile ppf;
469             auto iidPersistFile = IID_IPersistFile;
470             hres = psl.QueryInterface(cast(GUID*)&iidPersistFile, cast(void**)&ppf); 
471             enforce(SUCCEEDED(hres), "Failed to query IPersistFile interface");
472             scope(exit) ppf.Release();
473             
474             foreach(linkFile; dirEntries(linksFolder, SpanMode.shallow)) {
475                 if (!linkFile.name.endsWith(".lnk")) {
476                     continue;
477                 }
478                 try {
479                     wchar[MAX_PATH] szGotPath;
480                     WIN32_FIND_DATA wfd;
481                     
482                     hres = ppf.Load(linkFile.name.toUTF16z, STGM_READ); 
483                     enforce(SUCCEEDED(hres), "Failed to load link file");
484                     
485                     hres = psl.Resolve(null, SLR_FLAGS.SLR_NO_UI);
486                     enforce(SUCCEEDED(hres), "Failed to resolve link");
487                     
488                     hres = psl.GetPath(szGotPath.ptr, szGotPath.length, &wfd, 0);
489                     enforce(SUCCEEDED(hres), "Failed to get path of link target");
490                     
491                     auto path = szGotPath[0..wcslen(szGotPath.ptr)];
492                     
493                     if (path.length && path.toUTF8.isDir) {
494                         res ~= RootEntry(RootEntryType.BOOKMARK, path.toUTF8, linkFile.name.baseName.stripExtension.toUTF32);
495                     }
496                 } catch(Exception e) {
497                     
498                 }
499             }
500         } catch(Exception e) {
501             
502         }
503     }
504     return res;
505 }
506 
507 /// returns true if directory is root directory (e.g. / or C:\)
508 bool isRoot(in string path) pure nothrow {
509     string root = rootName(path);
510     if (path.equal(root))
511         return true;
512     return false;
513 }
514 
515 /**
516  * Check if path is hidden.
517  */
518 bool isHidden(in string path) nothrow {
519     version(Windows) {
520         import core.sys.windows.winnt : FILE_ATTRIBUTE_HIDDEN;
521         import std.exception : collectException;
522         uint attrs;
523         if (collectException(path.getAttributes(), attrs) is null) {
524             return (attrs & FILE_ATTRIBUTE_HIDDEN) != 0;
525         } else {
526             return false;
527         }
528     } else version(Posix) {
529         //TODO: check for hidden attribute on macOS
530         return path.baseName.startsWith(".");
531     } else {
532         return false;
533     }
534 }
535 
536 ///
537 unittest
538 {
539     version(Posix) {
540         assert(!"path/to/normal_file".isHidden());
541         assert("path/to/.hidden_file".isHidden());
542     }
543 }
544 
545 private bool isReadable(in string filePath) nothrow
546 {
547     version(Posix) {
548         import core.sys.posix.unistd : access, R_OK;
549         import std.string : toStringz;
550         return access(toStringz(filePath), R_OK) == 0;
551     } else {
552         // TODO: Windows version
553         return true;
554     }
555 }
556 
557 private bool isWritable(in string filePath) nothrow
558 {
559     version(Posix) {
560         import core.sys.posix.unistd : access, W_OK;
561         import std.string : toStringz;
562         return access(toStringz(filePath), W_OK) == 0;
563     } else {
564         // TODO: Windows version
565         return true;
566     }
567 }
568 
569 private bool isExecutable(in string filePath) nothrow
570 {
571     version(Windows) {
572         //TODO: Use GetEffectiveRightsFromAclW? For now just check extension
573         string extension = filePath.extension;
574         foreach(ext; [".exe", ".com", ".bat", ".cmd"]) {
575             if (filenameCmp(extension, ext) == 0)
576                 return true;
577         }
578         return false;
579     } else version(Posix) {
580         import core.sys.posix.unistd : access, X_OK;
581         import std.string : toStringz;
582         return access(toStringz(filePath), X_OK) == 0;
583     } else {
584         return false;
585     }
586 }
587 
588 /// returns parent directory for specified path
589 string parentDir(in string path) pure nothrow {
590     return buildNormalizedPath(path, "..");
591 }
592 
593 /// check filename with pattern
594 bool filterFilename(in string filename, in string pattern) pure nothrow {
595     return globMatch(filename.baseName, pattern);
596 }
597 /// Filters file name by pattern list
598 bool filterFilename(in string filename, in string[] filters) pure nothrow {
599     if (filters.length == 0)
600         return true; // no filters - show all
601     foreach(pattern; filters) {
602         if (filterFilename(filename, pattern))
603             return true;
604     }
605     return false;
606 }
607 
608 enum AttrFilter
609 {
610     none = 0,
611     files      = 1 << 0, /// Include regular files that match the filters.
612     dirs       = 1 << 1, /// Include directories.
613     hidden     = 1 << 2, /// Include hidden files and directoroies.
614     parent     = 1 << 3, /// Include parent directory (..). Takes effect only with includeDirs.
615     thisDir    = 1 << 4, /// Include this directory (.). Takes effect only with  includeDirs.
616     special    = 1 << 5, /// Include special files (On Unix: socket and device files, FIFO) that match the filters.
617     readable   = 1 << 6, /// Listing only readable files and directories.
618     writable   = 1 << 7, /// Listing only writable files and directories.
619     executable = 1 << 8, /// Include only executable files. This filter does not affect directories.
620     allVisible = AttrFilter.files | AttrFilter.dirs, /// Include all non-hidden files and directories without parent directory, this directory and special files.
621     all        = AttrFilter.allVisible | AttrFilter.hidden /// Include all files and directories including hidden ones but without parent directory, this directory and special files.
622 }
623 
624 /** List directory content
625 
626     Optionally filters file names by filter (not applied to directories).
627 
628     Returns true if directory exists and listed successfully, false otherwise.
629     Throws: Exception if $(D dir) is not directory or some error occured during directory listing.
630  */
631 DirEntry[] listDirectory(in string dir, AttrFilter attrFilter = AttrFilter.all, in string[] filters = [])
632 {
633     DirEntry[] entries;
634 
635     DirEntry[] dirs;
636     DirEntry[] files;
637     foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) {
638         if (!(attrFilter & AttrFilter.hidden) && e.name.isHidden())
639             continue;
640         if ((attrFilter & AttrFilter.readable) && !e.name.isReadable())
641             continue;
642         if ((attrFilter & AttrFilter.writable) && !e.name.isWritable())
643             continue;
644         if (!e.isDir && (attrFilter & AttrFilter.executable) && !e.name.isExecutable())
645             continue;
646         if (e.isDir && (attrFilter & AttrFilter.dirs)) {
647             dirs ~= e;
648         } else if ((attrFilter & AttrFilter.files) && filterFilename(e.name, filters)) {
649             if (e.isFile) {
650                 files ~= e;
651             } else if (attrFilter & AttrFilter.special) {
652                 files ~= e;
653             }
654         }
655     }
656     if ((attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.thisDir) ) {
657         entries ~= DirEntry(appendPath(dir, ".")) ~ entries;
658     }
659     if (!isRoot(dir) && (attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.parent)) {
660         entries ~= DirEntry(appendPath(dir, ".."));
661     }
662     dirs.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0);
663     files.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0);
664     entries ~= dirs;
665     entries ~= files;
666     return entries;
667 }
668 
669 /** List directory content
670 
671     Optionally filters file names by filter (not applied to directories).
672 
673     Result will be placed into entries array.
674 
675     Returns true if directory exists and listed successfully, false otherwise.
676 */
677 deprecated bool listDirectory(in string dir, in bool includeDirs, in bool includeFiles, in bool showHiddenFiles, in string[] filters, ref DirEntry[] entries, in bool showExecutables = false) {
678     entries.length = 0;
679 
680     AttrFilter attrFilter;
681     if (includeDirs) {
682         attrFilter |= AttrFilter.dirs;
683         attrFilter |= AttrFilter.parent;
684     }
685     if (includeFiles)
686         attrFilter |= AttrFilter.files;
687     if (showHiddenFiles)
688         attrFilter |= AttrFilter.hidden;
689     if (showExecutables)
690         attrFilter |= AttrFilter.executable;
691 
692     import std.exception : collectException;
693     return collectException(listDirectory(dir, attrFilter, filters), entries) is null;
694 }
695 
696 /// Returns true if char ch is / or \ slash
697 bool isPathDelimiter(in char ch) pure nothrow {
698     return ch == '/' || ch == '\\';
699 }
700 
701 /// Returns current directory
702 alias currentDir = std.file.getcwd;
703 
704 /// Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath()
705 @property string exePath() {
706     string path = thisExePath();
707     int lastSlash = 0;
708     for (int i = cast(int)path.length - 1; i >= 0; i--)
709         if (path[i] == PATH_DELIMITER) {
710             lastSlash = i;
711             break;
712         }
713     return path[0 .. lastSlash + 1];
714 }
715 
716 /** 
717     Returns application data directory
718 
719     On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter.
720 
721     On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir.
722 */
723 string appDataPath(string subdir = null) {
724     string path;
725     version (Windows) {
726         path = environment.get("APPDATA");
727     }
728     if (path is null)
729         path = homePath;
730     if (subdir !is null) {
731         path ~= PATH_DELIMITER;
732         path ~= subdir;
733     }
734     return path;
735 }
736 
737 /// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf
738 char[] convertPathDelimiters(char[] buf) {
739     foreach(ref ch; buf) {
740         version (Windows) {
741             if (ch == '/')
742                 ch = '\\';
743         } else {
744             if (ch == '\\')
745                 ch = '/';
746         }
747     }
748     return buf;
749 }
750 
751 /// Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix)
752 string convertPathDelimiters(in string src) {
753     char[] buf = src.dup;
754     return cast(string)convertPathDelimiters(buf);
755 }
756 
757 /// Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config"
758 string appendPath(string[] pathItems ...) {
759     char[] buf;
760     foreach (s; pathItems) {
761         if (buf.length && !isPathDelimiter(buf[$-1]))
762             buf ~= PATH_DELIMITER;
763         buf ~= s;
764     }
765     return convertPathDelimiters(buf).dup;
766 }
767 
768 ///  Appends file path parts with proper delimiters (as well converts delimiters inside path to system) to buffer e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config"
769 char[] appendPath(char[] buf, string[] pathItems ...) {
770     foreach (s; pathItems) {
771         if (buf.length && !isPathDelimiter(buf[$-1]))
772             buf ~= PATH_DELIMITER;
773         buf ~= s;
774     }
775     return convertPathDelimiters(buf);
776 }
777 
778 /** Deprecated: use std.path.pathSplitter instead.
779     Splits path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"]
780 */
781 deprecated string[] splitPath(string path) {
782     string[] res;
783     int start = 0;
784     for (int i = 0; i <= path.length; i++) {
785         char ch = i < path.length ? path[i] : 0;
786         if (ch == '\\' || ch == '/' || ch == 0) {
787             if (start < i)
788                 res ~= path[start .. i].dup;
789             start = i + 1;
790         }
791     }
792     return res;
793 }
794 
795 /// for executable name w/o path, find absolute path to executable
796 string findExecutablePath(string executableName) {
797     import std.string : split;
798     version (Windows) {
799         if (!executableName.endsWith(".exe"))
800             executableName = executableName ~ ".exe";
801     }
802     string currentExeDir = dirName(thisExePath());
803     string inCurrentExeDir = absolutePath(buildNormalizedPath(currentExeDir, executableName));
804     if (exists(inCurrentExeDir) && isFile(inCurrentExeDir))
805         return inCurrentExeDir; // found in current directory
806     string pathVariable = environment.get("PATH");
807     if (!pathVariable)
808         return null;
809     string[] paths = pathVariable.split(pathSeparator);
810     foreach(path; paths) {
811         string pathname = absolutePath(buildNormalizedPath(path, executableName));
812         if (exists(pathname) && isFile(pathname))
813             return pathname;
814     }
815     return null;
816 }