1 /**
2  * Functions for retrieving standard paths in cross-platform manner.
3  * 
4  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
5  */
6 
7 module standardpaths;
8 
9 private {
10     import std.process : environment;
11     import std.array;
12     import std.path;
13     import std.file;
14     import std.algorithm : splitter;
15     
16     debug(standardpaths) {
17         import std.stdio : stderr;
18     }
19 }
20 
21 version(Windows) {
22     private {
23         import std.c.windows.windows;
24         import std.utf;
25         import std.algorithm : canFind;
26         import std.uni : toLower, sicmp;
27     }
28 } else version(OSX) {
29     private {
30         //what to import?
31     }
32 } else version(Posix) {
33     private {
34         import std.stdio : File, StdioException;
35         import std.exception : assumeUnique, assumeWontThrow;
36         import std.conv : octal;
37     }
38 } else {
39     static assert(false, "Unsupported platform");
40 }
41 
42 /** 
43  * Locations that can be passed to writablePath and standardPaths functions.
44  * See_Also:
45  *  writablePath, standardPaths
46  */
47 enum StandardPath {
48     /**
49      * Location of persisted application data.
50      */
51     Data, 
52     /**
53      * Location of configuration files.
54      * Note: on Windows it's the same as $(B Data) path.
55      */
56     Config, 
57     /**
58      * Location of cached data.
59      * Note: on Windows it's the same as $(B Data)/cache.
60      */
61     Cache,  
62     Desktop, ///User's desktop directory
63     Documents, ///User's documents
64     Pictures, ///User's pictures
65     Music, ///User's music
66     Videos, ///User's videos (movies)
67     
68     /**
69      * Directory for user's downloaded files.
70      * Note: currently always return null on Windows.
71      */
72     Download, 
73     Templates, ///Location of templates.
74     
75     /**
76      * Public share folder.
77      * Note: available only on systems with xdg-user-dirs (Linux, FreeBSD)
78      */
79     PublicShare, 
80     /**
81      * Location of fonts files.
82      * Note: don't relie on this on freedesktop. Better consider using $(LINK2 http://www.freedesktop.org/wiki/Software/fontconfig/, fontconfig library)
83      */
84     Fonts, 
85     Applications, ///User's applications.
86 }
87 
88 /**
89  * Returns: path to user home directory, or an empty string if could not determine home directory.
90  * Relies on environment variables.
91  * Note: this function does not provide caching of its result.
92  */
93 string homeDir() nothrow @safe
94 {
95     version(Windows) {
96         try { //environment.get may throw on Windows
97             
98             //Use GetUserProfileDirectoryW from Userenv.dll?
99             string home = environment.get("USERPROFILE");
100             if (home.empty) {
101                 string homeDrive = environment.get("HOMEDRIVE");
102                 string homePath = environment.get("HOMEPATH");
103                 if (homeDrive.length && homePath.length) {
104                     home = homeDrive ~ homePath;
105                 }
106             }
107             return home;
108         }
109         catch(Exception e) {
110             debug(standardpaths) stderr.writeln(e.msg);
111             return null;
112         }
113     } else {
114         string home = assumeWontThrow(environment.get("HOME"));
115         return home;
116     }
117     
118 }
119 
120 /**
121  * Returns: path where files of $(U type) should be written to by current user, or an empty string if could not determine path.
122  * This function does not ensure if the returned path exists and appears to be accessible directory.
123  * Note: this function does not provide caching of its results.
124  */
125 string writablePath(StandardPath type) nothrow @safe;
126 
127 /**
128  * Returns: array of paths where files of $(U type) belong including one returned by $(B writablePath), or an empty array if no paths are defined for $(U type).
129  * This function does not ensure if all returned paths exist and appear to be accessible directories.
130  * Note: this function does not provide caching of its results. Also returned strings are not required to be unique.
131  * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation.
132  * See_Also:
133  *  writablePath
134  */
135 string[] standardPaths(StandardPath type) nothrow @safe;
136 
137 version(Windows) {
138     private enum pathVarSeparator = ';';
139 } else version(Posix) {
140     private enum pathVarSeparator = ':';
141 }
142 
143 version(Windows) {
144     
145     private {
146         enum {
147             CSIDL_DESKTOP            =  0,
148             CSIDL_INTERNET,
149             CSIDL_PROGRAMS,
150             CSIDL_CONTROLS,
151             CSIDL_PRINTERS,
152             CSIDL_PERSONAL,
153             CSIDL_FAVORITES,
154             CSIDL_STARTUP,
155             CSIDL_RECENT,
156             CSIDL_SENDTO,
157             CSIDL_BITBUCKET,
158             CSIDL_STARTMENU,      // = 11
159             CSIDL_MYMUSIC            = 13,
160             CSIDL_MYVIDEO,        // = 14
161             CSIDL_DESKTOPDIRECTORY   = 16,
162             CSIDL_DRIVES,
163             CSIDL_NETWORK,
164             CSIDL_NETHOOD,
165             CSIDL_FONTS,
166             CSIDL_TEMPLATES,
167             CSIDL_COMMON_STARTMENU,
168             CSIDL_COMMON_PROGRAMS,
169             CSIDL_COMMON_STARTUP,
170             CSIDL_COMMON_DESKTOPDIRECTORY,
171             CSIDL_APPDATA,
172             CSIDL_PRINTHOOD,
173             CSIDL_LOCAL_APPDATA,
174             CSIDL_ALTSTARTUP,
175             CSIDL_COMMON_ALTSTARTUP,
176             CSIDL_COMMON_FAVORITES,
177             CSIDL_INTERNET_CACHE,
178             CSIDL_COOKIES,
179             CSIDL_HISTORY,
180             CSIDL_COMMON_APPDATA,
181             CSIDL_WINDOWS,
182             CSIDL_SYSTEM,
183             CSIDL_PROGRAM_FILES,
184             CSIDL_MYPICTURES,
185             CSIDL_PROFILE,
186             CSIDL_SYSTEMX86,
187             CSIDL_PROGRAM_FILESX86,
188             CSIDL_PROGRAM_FILES_COMMON,
189             CSIDL_PROGRAM_FILES_COMMONX86,
190             CSIDL_COMMON_TEMPLATES,
191             CSIDL_COMMON_DOCUMENTS,
192             CSIDL_COMMON_ADMINTOOLS,
193             CSIDL_ADMINTOOLS,
194             CSIDL_CONNECTIONS,  // = 49
195             CSIDL_COMMON_MUSIC     = 53,
196             CSIDL_COMMON_PICTURES,
197             CSIDL_COMMON_VIDEO,
198             CSIDL_RESOURCES,
199             CSIDL_RESOURCES_LOCALIZED,
200             CSIDL_COMMON_OEM_LINKS,
201             CSIDL_CDBURN_AREA,  // = 59
202             CSIDL_COMPUTERSNEARME  = 61,
203             CSIDL_FLAG_DONT_VERIFY = 0x4000,
204             CSIDL_FLAG_CREATE      = 0x8000,
205             CSIDL_FLAG_MASK        = 0xFF00
206         }
207     }
208     
209     private  {
210         alias GetSpecialFolderPath = extern(Windows) BOOL function (HWND, wchar*, int, BOOL) nothrow @system;
211         
212         version(LinkedShell32) {
213             extern(Windows) BOOL SHGetSpecialFolderPathW(HWND, wchar*, int, BOOL) nothrow @system;
214             __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = &SHGetSpecialFolderPathW;
215         } else {
216             __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = null;
217         }
218         
219         bool hasSHGetSpecialFolderPath() nothrow @trusted {
220             return ptrSHGetSpecialFolderPath != null;
221         }
222     }
223     
224     version(LinkedShell32) {} else {
225         shared static this() 
226         {
227             HMODULE lib = LoadLibraryA("Shell32");
228             if (lib) {
229                 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(lib, "SHGetSpecialFolderPathW");
230             }
231         }
232     }
233     
234     
235     private string getCSIDLFolder(int csidl) nothrow @trusted
236     {
237         import core.stdc.wchar_ : wcslen;
238         
239         wchar[MAX_PATH] path = void;
240         if (ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) {
241             size_t len = wcslen(path.ptr);
242             try {
243                 return toUTF8(path[0..len]);
244             } catch(Exception e) {
245                 
246             }
247         }
248         return null;
249     }
250     
251     /// Path to $(B Roaming) data directory. This function is Windows only.
252     string roamingPath() nothrow @safe
253     {
254         return getCSIDLFolder(CSIDL_APPDATA);
255     }
256     
257     string writablePath(StandardPath type) nothrow @safe
258     {
259         if (!hasSHGetSpecialFolderPath()) {
260             return null;
261         }
262         
263         final switch(type) {
264             case StandardPath.Config:
265             case StandardPath.Data:
266                 return getCSIDLFolder(CSIDL_LOCAL_APPDATA);
267             case StandardPath.Cache:
268             {
269                 string path = getCSIDLFolder(CSIDL_LOCAL_APPDATA);
270                 if (path.length) {
271                     return buildPath(getCSIDLFolder(CSIDL_LOCAL_APPDATA), "cache");
272                 }
273                 return null;
274             }
275             case StandardPath.Desktop:
276                 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY);
277             case StandardPath.Documents:
278                 return getCSIDLFolder(CSIDL_PERSONAL);
279             case StandardPath.Pictures:
280                 return getCSIDLFolder(CSIDL_MYPICTURES);
281             case StandardPath.Music:
282                 return getCSIDLFolder(CSIDL_MYMUSIC);
283             case StandardPath.Videos:
284                 return getCSIDLFolder(CSIDL_MYVIDEO);
285             case StandardPath.Download:
286                 return null;
287             case StandardPath.Templates:
288                 return getCSIDLFolder(CSIDL_TEMPLATES);
289             case StandardPath.PublicShare:
290                 return null;
291             case StandardPath.Fonts:
292                 return null;
293             case StandardPath.Applications:
294                 return getCSIDLFolder(CSIDL_PROGRAMS);
295         }
296     }
297     
298     string[] standardPaths(StandardPath type) nothrow @safe
299     {
300         if (!hasSHGetSpecialFolderPath()) {
301             return null;
302         }
303         
304         string commonPath;
305         
306         switch(type) {
307             case StandardPath.Config:
308             case StandardPath.Data:
309                 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA);
310                 break;
311             case StandardPath.Desktop:
312                 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY);
313                 break;
314             case StandardPath.Documents:
315                 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS);
316                 break;
317             case StandardPath.Pictures:
318                 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES);
319                 break;
320             case StandardPath.Music:
321                 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC);
322                 break;
323             case StandardPath.Videos:
324                 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO);
325                 break;
326             case StandardPath.Templates:
327                 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES);
328                 break;
329             case StandardPath.Fonts:
330                 commonPath = getCSIDLFolder(CSIDL_FONTS);
331                 break;
332             case StandardPath.Applications:
333                 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS);
334                 break;
335             default:
336                 break;
337         }
338         
339         string[] paths;
340         string userPath = writablePath(type);
341         if (userPath.length) 
342             paths ~= userPath;
343         if (commonPath.length)
344             paths ~= commonPath;
345         return paths;
346     }
347     
348     private string[] executableExtensions() nothrow @safe
349     {
350         static bool filenamesEqual(string first, string second) nothrow {
351             try {
352                 return filenameCmp(first, second) == 0;
353             } catch(Exception e) {
354                 return false;
355             }
356         }
357         
358         string[] extensions;
359         try {
360             extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array;
361             if (canFind!(filenamesEqual)(extensions, ".exe") == false) {
362                 extensions = [];
363             }
364         } catch (Exception e) {
365             
366         }
367         if (extensions.empty) {
368             extensions = [".exe", ".com", ".bat", ".cmd"];
369         }
370         return extensions;
371     }
372 } else version(OSX) {
373     
374     string fsPath(short domain, OSType type) nothrow @trusted
375     {
376         import std.string : fromStringz;
377         
378         FSRef fsref;
379         OSErr err = FSFindFolder(domain, type, false, &fsref);
380         if (err) {
381             return null;
382         } else {
383             ubyte[2048] buf;
384             ubyte* path = buf.ptr;
385             if (FSRefMakePath(&fsref, path, path.sizeof) == noErr) {
386                 auto cpath = cast(const(char)*)path;
387                 return fromStringz(cpath).idup;
388             } else {
389                 return null;
390             }
391         }
392     }
393     
394     string writablePath(StandardPath type) nothrow @safe
395     {
396         final switch(type) {
397             case StandardPath.Config:
398                 return fsPath(kUserDomain, kPreferencesFolderType);
399             case StandardPath.Cache:
400                 return fsPath(kUserDomain, kCachedDataFolderType);
401             case StandardPath.Data:
402                 return fsPath(kUserDomain, kApplicationSupportFolderType);
403             case StandardPath.Desktop:
404                 return fsPath(kUserDomain, kDesktopFolderType);
405             case StandardPath.Documents:
406                 return fsPath(kUserDomain, kDocumentsFolderType);
407             case StandardPath.Pictures:
408                 return fsPath(kUserDomain, kPictureDocumentsFolderType);
409             case StandardPath.Music:
410                 return fsPath(kUserDomain, kMusicDocumentsFolderType);
411             case StandardPath.Videos:
412                 return fsPath(kUserDomain, kMovieDocumentsFolderType);
413             case StandardPath.Download:
414                 return null;
415             case StandardPath.Templates:
416                 return null;
417             case StandardPath.PublicShare:
418                 return fsPath(kUserDomain, kPublicFolderType );
419             case StandardPath.Fonts:
420                 return fsPath(kUserDomain, kFontsFolderType);
421             case StandardPath.Applications:
422                 return fsPath(kUserDomain, kApplicationsFolderType);
423         }
424     }
425     
426     string[] standardPaths(StandardPath type) nothrow @safe
427     {
428         string commonPath;
429         
430         switch(type) {
431             case StandardPath.Fonts:
432                 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType);
433             case StandardPath.Applications:
434                 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType);
435             case StandardPath.Data:
436                 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType);
437             case StandardPath.Cache:
438                 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType);
439         }
440         
441         string[] paths;
442         string userPath = writablePath(type);
443         if (userPath.length)
444             paths ~= userPath;
445         if (commonPath.length)
446             paths ~= commonPath;
447         return paths;
448     }
449     
450 } else version(Posix) {
451     
452     //Concat two strings, but if the first one is empty, then null string is returned.
453     private string maybeConcat(string start, string path) nothrow @safe
454     {
455         return start.empty ? null : start ~ path;
456     }
457     
458     private string xdgBaseDir(in char[] envvar, string fallback) nothrow @trusted {
459         string dir = assumeWontThrow(environment.get(envvar));
460         if (!dir.length) {
461             dir = maybeConcat(homeDir(), fallback);
462         }
463         return dir;
464     }
465     
466     private string xdgUserDir(in char[] key, string fallback = null) nothrow @trusted {
467         import std.algorithm : startsWith;
468         import std.string : strip;
469         
470         string fileName = maybeConcat(writablePath(StandardPath.Config), "/user-dirs.dirs");
471         string home = homeDir();
472         try {
473             auto f = File(fileName, "r");
474             
475             auto xdgdir = "XDG_" ~ key ~ "_DIR";
476             
477             char[] buf;
478             while(f.readln(buf)) {
479                 char[] line = strip(buf);
480                 auto index = xdgdir.length;
481                 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') {
482                     line = line[index+1..$];
483                     if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 
484                     {
485                         line = line[1..$-1];
486                     
487                         if (line.startsWith("$HOME")) {
488                             return maybeConcat(home, assumeUnique(line[5..$]));
489                         }
490                         if (line.length == 0 || line[0] != '/') {
491                             continue;
492                         }
493                         return assumeUnique(line);
494                     }
495                 }
496             }
497         } catch(Exception e) {
498             
499         }
500         
501         if (home.length) {
502             try {
503                 auto f = File("/etc/xdg/user-dirs.defaults", "r");
504                 char[] buf;
505                 while(f.readln(buf)) {
506                     char[] line = strip(buf);
507                     auto index = key.length;
508                     if (line.startsWith(key) && line.length > index && line[index] == '=') 
509                     {
510                         line = line[index+1..$];
511                         return home ~ "/" ~ assumeUnique(line);
512                     }
513                 }
514             } catch (Exception e) {
515                 
516             }
517             if (fallback.length) {
518                 return home ~ fallback;
519             }
520         }
521         return null;
522     }
523     
524     private string[] xdgConfigDirs() nothrow @trusted {
525         string configDirs = assumeWontThrow(environment.get("XDG_CONFIG_DIRS"));
526         try {
527             if (configDirs.length) {
528                 return splitter(configDirs, pathVarSeparator).array;
529             }
530         }
531         catch(Exception e) {
532             
533         }
534         return ["/etc/xdg"];
535     }
536     
537     private string[] xdgDataDirs() nothrow @trusted {
538         string dataDirs = assumeWontThrow(environment.get("XDG_DATA_DIRS"));
539         try {
540             if (dataDirs.length) {
541                 return splitter(dataDirs, pathVarSeparator).array;
542             }
543         } catch(Exception e) {
544             
545         }
546         return ["/usr/local/share", "/usr/share"];
547     }
548     
549     
550     version(fontsconf) {
551         private string[] readFontsConfig(string configFile) nothrow @trusted
552         {
553             //Should be changed in future since std.xml is deprecated
554             import std.xml;
555             
556             string[] paths;
557             try {
558                 string contents = cast(string)read(configFile);
559                 check(contents);
560                 auto parser = new DocumentParser(contents);
561                 parser.onEndTag["dir"] = (in Element xml)
562                 {
563                     string path = xml.text;
564                     
565                     if (path.length && path[0] == '~') {
566                         path = maybeConcat(homeDir(), path[1..$]);
567                     } else {
568                         const(string)* prefix = "prefix" in xml.tag.attr;
569                         if (prefix && *prefix == "xdg") {
570                             string dataPath = writablePath(StandardPath.Data);
571                             if (dataPath.length) {
572                                 path = buildPath(dataPath, path);
573                             }
574                         }
575                     }
576                     if (path.length) {
577                         paths ~= path;
578                     }
579                 };
580                 parser.parse();
581             }
582             catch(Exception e) {
583                 
584             }
585             return paths;
586         }
587         
588         private string[] fontPaths() nothrow @trusted
589         {
590             string[] paths;
591             
592             string homeConfig = homeFontsConfig();
593             if (homeConfig.length) {
594                 paths ~= readFontsConfig(homeConfig);
595             }
596             
597             enum configs = ["/etc/fonts/fonts.conf", //path on linux
598                             "/usr/local/etc/fonts/fonts.conf"]; //path on freebsd
599             foreach(config; configs) {
600                 paths ~= readFontsConfig(config);
601             }
602             return paths;
603         }
604         
605         private string homeFontsConfig() nothrow @trusted {
606             return maybeConcat(writablePath(StandardPath.Config), "/fontconfig/fonts.conf");
607         }
608         
609         private string homeFontsPath() nothrow @trusted {
610             string[] paths = readFontsConfig(homeFontsConfig());
611             if (paths.length)
612                 return paths[0];
613             return null;
614         }
615         
616     } else {
617         private string homeFontsPath() nothrow @trusted {
618             return maybeConcat(homeDir(), "/.fonts");
619         }
620         
621         private string[] fontPaths() nothrow @trusted
622         {
623             enum localShare = "/usr/local/share/fonts";
624             enum share = "/usr/share/fonts";
625             
626             string homeFonts = homeFontsPath();
627             if (homeFonts.length) {
628                 return [homeFonts, localShare, share];
629             } else {
630                 return [localShare, share];
631             }
632         }
633         
634     }
635     
636     
637     
638     /**
639      * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 
640      * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string.
641      * Note: this function is defined only on $(B Posix) systems (except for OS X)
642      */
643     string runtimeDir() nothrow @trusted
644     {
645         // Do we need it on BSD systems?
646         
647         import core.sys.posix.pwd;
648         import core.sys.posix.unistd;
649         import core.sys.posix.sys.stat;
650         import core.sys.posix.sys.types;
651         import core.stdc.errno;
652         import core.stdc.string;
653         
654         import std.string : fromStringz, toStringz;
655         
656         const uid_t uid = getuid();
657         string runtime = assumeWontThrow(environment.get("XDG_RUNTIME_DIR"));
658         
659         mode_t runtimeMode = octal!700;
660         
661         if (!runtime.length) {
662             setpwent();
663             passwd* pw = getpwuid(uid);
664             endpwent();
665             
666             try {
667                 if (pw && pw.pw_name) {
668                     runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
669                     
670                     if (!(runtime.exists && runtime.isDir)) {
671                         if (mkdir(runtime.toStringz, runtimeMode) != 0) {
672                             debug(standardpaths) stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
673                             return null;
674                         }
675                     }
676                 } else {
677                     debug(standardpaths) stderr.writefln("Failed to get user name to create runtime directory");
678                     return null;
679                 }
680             } catch(Exception e) {
681                 debug(standardpaths) stderr.writeln(e.msg);
682                 return null;
683             }
684         }
685         stat_t statbuf;
686         stat(runtime.toStringz, &statbuf);
687         if (statbuf.st_uid != uid) {
688             debug(standardpaths) stderr.writefln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid);
689             return null;
690         }
691         if ((statbuf.st_mode & octal!777) != runtimeMode) {
692             debug(standardpaths) stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode);
693             return null;
694         }
695         
696         return runtime;
697     }
698     
699     string writablePath(StandardPath type) nothrow @safe
700     {
701         final switch(type) {
702             case StandardPath.Config:
703                 return xdgBaseDir("XDG_CONFIG_HOME", "/.config");
704             case StandardPath.Cache:
705                 return xdgBaseDir("XDG_CACHE_HOME", "/.cache");
706             case StandardPath.Data:
707                 return xdgBaseDir("XDG_DATA_HOME", "/.local/share");
708             case StandardPath.Desktop:
709                 return xdgUserDir("DESKTOP", "/Desktop");
710             case StandardPath.Documents:
711                 return xdgUserDir("DOCUMENTS");
712             case StandardPath.Pictures:
713                 return xdgUserDir("PICTURES");
714             case StandardPath.Music:
715                 return xdgUserDir("MUSIC");
716             case StandardPath.Videos:
717                 return xdgUserDir("VIDEOS");
718             case StandardPath.Download:
719                 return xdgUserDir("DOWNLOAD");
720             case StandardPath.Templates:
721                 return xdgUserDir("TEMPLATES", "/Templates");
722             case StandardPath.PublicShare:
723                 return xdgUserDir("PUBLICSHARE", "/Public");
724             case StandardPath.Fonts:
725                 return homeFontsPath();
726             case StandardPath.Applications:
727                 return maybeConcat(writablePath(StandardPath.Data), "/applications");
728         }
729     }
730     
731     string[] standardPaths(StandardPath type) nothrow @safe
732     {
733         string[] paths;
734         
735         switch(type) {
736             case StandardPath.Data:
737                 paths = xdgDataDirs();
738                 break;
739             case StandardPath.Config:
740                 paths = xdgConfigDirs();
741                 break;
742             case StandardPath.Applications:
743                 paths = ["/usr/local/share/applications", "/usr/share/applications"];
744                 break;
745             case StandardPath.Fonts:
746                 return fontPaths();
747             default:
748                 break;
749         }
750         
751         string userPath = writablePath(type);
752         if (userPath.length) {
753             paths = userPath ~ paths;
754         }
755         return paths;
756     }
757 } else {
758     static assert(false, "Unsupported platform");
759 }
760 
761 private bool isExecutable(string filePath) nothrow @trusted {
762     try {
763         version(Posix) {
764             return (getAttributes(filePath) & octal!100) != 0;
765         } else version(Windows) {
766             //Use GetEffectiveRightsFromAclW?
767             
768             string extension = filePath.extension;
769             const(string)[] exeExtensions = executableExtensions();
770             foreach(ext; exeExtensions) {
771                 if (sicmp(extension, ext) == 0)
772                     return true;
773             }
774             return false;
775             
776         } else {
777             static assert(false, "Unsupported platform");
778         }
779     } catch(Exception e) {
780         return false;
781     }
782 }
783 
784 private string checkExecutable(string filePath) nothrow @trusted {
785     try {
786         if (filePath.isFile && filePath.isExecutable) {
787             return buildNormalizedPath(filePath);
788         } else {
789             return null;
790         }
791     }
792     catch(Exception e) {
793         return null;
794     }
795 }
796 
797 /**
798  * Finds executable by $(B fileName) in the paths specified by $(B paths).
799  * Returns: absolute path to the existing executable file or an empty string if not found.
800  * Params:
801  *  fileName = name of executable to search
802  *  paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable
803  * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search.
804  */
805 string findExecutable(string fileName, in string[] paths = []) nothrow @safe
806 {
807     try {
808         if (fileName.isAbsolute()) {
809             return checkExecutable(fileName);
810         }
811         
812         const(string)[] searchPaths = paths;
813         if (searchPaths.empty) {
814             string pathVar = environment.get("PATH");
815             if (pathVar.length) {
816                 searchPaths = splitter(pathVar, pathVarSeparator).array;
817             }
818         }
819         
820         if (searchPaths.empty) {
821             return null;
822         }
823         
824         string toReturn;
825         foreach(string path; searchPaths) {
826             string candidate = buildPath(absolutePath(path), fileName);
827             
828             version(Windows) {
829                 if (candidate.extension.empty) {
830                     foreach(exeExtension; executableExtensions()) {
831                         toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower()));
832                         if (toReturn.length) {
833                             return toReturn;
834                         }
835                     }
836                 }
837             }
838             
839             toReturn = checkExecutable(candidate);
840             if (toReturn.length) {
841                 return toReturn;
842             }
843         }
844     } catch (Exception e) {
845         
846     }
847     return null;
848 }
849 
850