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, canFind;
15     import std.exception;
16     
17     debug {
18         import std.stdio : stderr;
19     }
20 }
21 
22 version(Windows) {
23     private {
24         import std.c.windows.windows;
25         import std.utf;
26         import std.uni : toLower, sicmp;
27     }
28 } else version(Posix) {
29     private {
30         import std.stdio : File, StdioException;
31         import std.conv : octal;
32         import std.string : toStringz, fromStringz;
33     }
34 } else {
35     static assert(false, "Unsupported platform");
36 }
37 
38 /** 
39  * Locations that can be passed to writablePath and standardPaths functions.
40  * See_Also:
41  *  writablePath, standardPaths
42  */
43 enum StandardPath {
44     /**
45      * Location of persisted application data.
46      */
47     Data, 
48     /**
49      * Location of configuration files.
50      * Note: on Windows it's the same as $(B Data) path.
51      */
52     Config, 
53     /**
54      * Location of cached data.
55      * Note: on Windows it's the same as $(B Data)/cache.
56      */
57     Cache,  
58     Desktop, ///User's desktop directory
59     Documents, ///User's documents
60     Pictures, ///User's pictures
61     Music, ///User's music
62     Videos, ///User's videos (movies)
63     
64     /**
65      * Directory for user's downloaded files.
66      * Note: currently always return null on Windows.
67      */
68     Download, 
69     Templates, ///Location of templates.
70     
71     /**
72      * Public share folder.
73      * Note: available only on systems with xdg-user-dirs (Linux, FreeBSD)
74      */
75     PublicShare, 
76     /**
77      * Location of fonts files.
78      * Note: don't relie on this on freedesktop. Better consider using $(LINK2 http://www.freedesktop.org/wiki/Software/fontconfig/, fontconfig library)
79      */
80     Fonts, 
81     Applications, ///User's applications.
82 }
83 
84 /**
85  * Returns: path to user home directory, or an empty string if could not determine home directory.
86  * Relies on environment variables.
87  * Note: this function does not provide caching of its result.
88  */
89 string homeDir() nothrow @safe
90 {
91     try {
92         version(Windows) {
93             //Use GetUserProfileDirectoryW from Userenv.dll?
94             string home = environment.get("USERPROFILE");
95             if (home.empty) {
96                 string homeDrive = environment.get("HOMEDRIVE");
97                 string homePath = environment.get("HOMEPATH");
98                 if (homeDrive.length && homePath.length) {
99                     home = homeDrive ~ homePath;
100                 }
101             }
102             return home;
103         } else {
104             string home = environment.get("HOME");
105             return home;
106         }
107     }
108     catch (Exception e) {
109         debug collectException(stderr.writefln("Error when getting home directory %s", e.msg));
110         return null;
111     }
112 }
113 
114 /**
115  * Returns: path where files of $(U type) should be written to by current user, or an empty string if could not determine path.
116  * This function does not ensure if the returned path exists and appears to be accessible directory.
117  * Note: this function does not provide caching of its results.
118  */
119 string writablePath(StandardPath type) nothrow @safe;
120 
121 /**
122  * 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).
123  * This function does not ensure if all returned paths exist and appear to be accessible directories.
124  * Note: this function does not provide caching of its results. Also returned strings are not required to be unique.
125  * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation.
126  * See_Also:
127  *  writablePath
128  */
129 string[] standardPaths(StandardPath type) nothrow @safe;
130 
131 version(Windows) {
132     private enum pathVarSeparator = ';';
133 } else version(Posix) {
134     private enum pathVarSeparator = ':';
135 }
136 
137 version(Windows) {
138     
139     private {
140         enum {
141             CSIDL_DESKTOP            =  0,
142             CSIDL_INTERNET,
143             CSIDL_PROGRAMS,
144             CSIDL_CONTROLS,
145             CSIDL_PRINTERS,
146             CSIDL_PERSONAL,
147             CSIDL_FAVORITES,
148             CSIDL_STARTUP,
149             CSIDL_RECENT,
150             CSIDL_SENDTO,
151             CSIDL_BITBUCKET,
152             CSIDL_STARTMENU,      // = 11
153             CSIDL_MYMUSIC            = 13,
154             CSIDL_MYVIDEO,        // = 14
155             CSIDL_DESKTOPDIRECTORY   = 16,
156             CSIDL_DRIVES,
157             CSIDL_NETWORK,
158             CSIDL_NETHOOD,
159             CSIDL_FONTS,
160             CSIDL_TEMPLATES,
161             CSIDL_COMMON_STARTMENU,
162             CSIDL_COMMON_PROGRAMS,
163             CSIDL_COMMON_STARTUP,
164             CSIDL_COMMON_DESKTOPDIRECTORY,
165             CSIDL_APPDATA,
166             CSIDL_PRINTHOOD,
167             CSIDL_LOCAL_APPDATA,
168             CSIDL_ALTSTARTUP,
169             CSIDL_COMMON_ALTSTARTUP,
170             CSIDL_COMMON_FAVORITES,
171             CSIDL_INTERNET_CACHE,
172             CSIDL_COOKIES,
173             CSIDL_HISTORY,
174             CSIDL_COMMON_APPDATA,
175             CSIDL_WINDOWS,
176             CSIDL_SYSTEM,
177             CSIDL_PROGRAM_FILES,
178             CSIDL_MYPICTURES,
179             CSIDL_PROFILE,
180             CSIDL_SYSTEMX86,
181             CSIDL_PROGRAM_FILESX86,
182             CSIDL_PROGRAM_FILES_COMMON,
183             CSIDL_PROGRAM_FILES_COMMONX86,
184             CSIDL_COMMON_TEMPLATES,
185             CSIDL_COMMON_DOCUMENTS,
186             CSIDL_COMMON_ADMINTOOLS,
187             CSIDL_ADMINTOOLS,
188             CSIDL_CONNECTIONS,  // = 49
189             CSIDL_COMMON_MUSIC     = 53,
190             CSIDL_COMMON_PICTURES,
191             CSIDL_COMMON_VIDEO,
192             CSIDL_RESOURCES,
193             CSIDL_RESOURCES_LOCALIZED,
194             CSIDL_COMMON_OEM_LINKS,
195             CSIDL_CDBURN_AREA,  // = 59
196             CSIDL_COMPUTERSNEARME  = 61,
197             CSIDL_FLAG_DONT_VERIFY = 0x4000,
198             CSIDL_FLAG_CREATE      = 0x8000,
199             CSIDL_FLAG_MASK        = 0xFF00
200         }
201     }
202     
203     private  {
204         alias GetSpecialFolderPath = extern(Windows) BOOL function (HWND, wchar*, int, BOOL) nothrow @nogc @system;
205         
206         version(LinkedShell32) {
207             extern(Windows) BOOL SHGetSpecialFolderPathW(HWND, wchar*, int, BOOL) nothrow @nogc @system;
208             __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = &SHGetSpecialFolderPathW;
209         } else {
210             __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = null;
211         }
212         
213         bool hasSHGetSpecialFolderPath() nothrow @nogc @trusted {
214             return ptrSHGetSpecialFolderPath != null;
215         }
216     }
217     
218     version(LinkedShell32) {} else {
219         shared static this() 
220         {
221             HMODULE lib = LoadLibraryA("Shell32");
222             if (lib) {
223                 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(lib, "SHGetSpecialFolderPathW");
224             }
225         }
226     }
227     
228     
229     private string getCSIDLFolder(int csidl) nothrow @trusted
230     {
231         import core.stdc.wchar_ : wcslen;
232         
233         wchar[MAX_PATH] path = void;
234         if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) {
235             size_t len = wcslen(path.ptr);
236             try {
237                 return toUTF8(path[0..len]);
238             } catch(Exception e) {
239                 
240             }
241         }
242         return null;
243     }
244     
245     /// Path to $(B Roaming) data directory. This function is Windows only.
246     string roamingPath() nothrow @safe
247     {
248         return getCSIDLFolder(CSIDL_APPDATA);
249     }
250     
251     string writablePath(StandardPath type) nothrow @safe
252     {
253         final switch(type) {
254             case StandardPath.Config:
255             case StandardPath.Data:
256                 return getCSIDLFolder(CSIDL_LOCAL_APPDATA);
257             case StandardPath.Cache:
258             {
259                 string path = getCSIDLFolder(CSIDL_LOCAL_APPDATA);
260                 if (path.length) {
261                     return buildPath(path, "cache");
262                 }
263                 return null;
264             }
265             case StandardPath.Desktop:
266                 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY);
267             case StandardPath.Documents:
268                 return getCSIDLFolder(CSIDL_PERSONAL);
269             case StandardPath.Pictures:
270                 return getCSIDLFolder(CSIDL_MYPICTURES);
271             case StandardPath.Music:
272                 return getCSIDLFolder(CSIDL_MYMUSIC);
273             case StandardPath.Videos:
274                 return getCSIDLFolder(CSIDL_MYVIDEO);
275             case StandardPath.Download:
276                 return null;
277             case StandardPath.Templates:
278                 return getCSIDLFolder(CSIDL_TEMPLATES);
279             case StandardPath.PublicShare:
280                 return null;
281             case StandardPath.Fonts:
282                 return null;
283             case StandardPath.Applications:
284                 return getCSIDLFolder(CSIDL_PROGRAMS);
285         }
286     }
287     
288     string[] standardPaths(StandardPath type) nothrow @safe
289     {   
290         string commonPath;
291         
292         switch(type) {
293             case StandardPath.Config:
294             case StandardPath.Data:
295                 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA);
296                 break;
297             case StandardPath.Desktop:
298                 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY);
299                 break;
300             case StandardPath.Documents:
301                 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS);
302                 break;
303             case StandardPath.Pictures:
304                 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES);
305                 break;
306             case StandardPath.Music:
307                 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC);
308                 break;
309             case StandardPath.Videos:
310                 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO);
311                 break;
312             case StandardPath.Templates:
313                 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES);
314                 break;
315             case StandardPath.Fonts:
316                 commonPath = getCSIDLFolder(CSIDL_FONTS);
317                 break;
318             case StandardPath.Applications:
319                 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS);
320                 break;
321             default:
322                 break;
323         }
324         
325         string[] paths;
326         string userPath = writablePath(type);
327         if (userPath.length) 
328             paths ~= userPath;
329         if (commonPath.length)
330             paths ~= commonPath;
331         return paths;
332     }
333     
334     private string[] executableExtensions() nothrow @trusted
335     {
336         static bool filenamesEqual(string first, string second) nothrow {
337             try {
338                 return filenameCmp(first, second) == 0;
339             } catch(Exception e) {
340                 return false;
341             }
342         }
343         
344         string[] extensions;
345         try {
346             extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array;
347             if (canFind!(filenamesEqual)(extensions, ".exe") == false) {
348                 extensions = [];
349             }
350         } catch (Exception e) {
351             
352         }
353         if (extensions.empty) {
354             extensions = [".exe", ".com", ".bat", ".cmd"];
355         }
356         return extensions;
357     }
358 } else version(OSX) {
359     
360     private enum : short {
361       kOnSystemDisk                 = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/
362       kOnAppropriateDisk            = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/
363                                             /* Folder Domains - Carbon only.  The constants above can continue to be used, but the folder/volume returned will*/
364                                             /* be from one of the domains below.*/
365       kSystemDomain                 = -32766, /* Read-only system hierarchy.*/
366       kLocalDomain                  = -32765, /* All users of a single machine have access to these resources.*/
367       kNetworkDomain                = -32764, /* All users configured to use a common network server has access to these resources.*/
368       kUserDomain                   = -32763, /* Read/write. Resources that are private to the user.*/
369       kClassicDomain                = -32762, /* Domain referring to the currently configured Classic System Folder.  Not supported in Mac OS X Leopard and later.*/
370       kFolderManagerLastDomain      = -32760
371     }
372 
373     private int k(string s) nothrow @nogc {
374         return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3];
375     }
376 
377     private enum {
378   kDesktopFolderType            = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */
379   kTrashFolderType              = k("trsh"), /* the trash folder; objects in this folder show up in the trash */
380   kWhereToEmptyTrashFolderType  = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */
381   kFontsFolderType              = k("font"), /* Fonts go here */
382   kPreferencesFolderType        = k("pref"), /* preferences for applications go here */
383   kSystemPreferencesFolderType  = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */
384   kTemporaryFolderType          = k("temp"), /*    On Mac OS X, each user has their own temporary items folder, and the Folder Manager attempts to set permissions of these*/
385                                         /*    folders such that other users can not access the data inside.  On Mac OS X 10.4 and later the data inside the temporary*/
386                                         /*    items folder is deleted at logout and at boot, but not otherwise.  Earlier version of Mac OS X would delete items inside*/
387                                         /*    the temporary items folder after a period of inaccess.  You can ask for a temporary item in a specific domain or on a */
388                                         /*    particular volume by FSVolumeRefNum.  If you want a location for temporary items for a short time, then use either*/
389                                         /*    ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ).  The kUserDomain varient will always be*/
390                                         /*    on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/
391                                         /*    will probably be on the local hard drive in case the user's home is a network volume ).  If you want a location for a temporary*/
392                                         /*    file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/
393                                         /*    ask for the temporary items folder on the same volume as the file you are safe saving.*/
394                                         /*    However, be prepared for a failure to find a temporary folder in any domain or on any volume.  Some volumes may not have*/
395                                         /*    a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/
396                                         /*    a temporary folder for the volume.*/
397                                         /*    If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/
398                                         /*    and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/
399                                         /*    restarts.  Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/
400                                         /*    when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/
401   kChewableItemsFolderType      = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */
402   kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/
403   kApplicationsFolderType       = k("apps"), /*    Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/
404   kVolumeRootFolderType         = k("root"), /* root folder of a volume or domain */
405   kDomainTopLevelFolderType     = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/
406   kDomainLibraryFolderType      = k("dlib"), /* the Library subfolder of a particular domain*/
407   kUsersFolderType              = k("usrs"), /* "Users" folder, usually contains one folder for each user. */
408   kCurrentUserFolderType        = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */
409   kSharedUserDataFolderType     = k("sdat"), /* A Shared folder, readable & writeable by all users */
410   kCachedDataFolderType         = k("cach"), /* Contains various cache files for different clients*/
411   kDownloadsFolderType          = k("down"), /* Refers to the ~/Downloads folder*/
412   kApplicationSupportFolderType = k("asup"), /* third-party items and folders */
413 
414 
415   kDocumentsFolderType          = k("docs"), /*    User documents are typically put in this folder ( or a subfolder ).*/
416   kPictureDocumentsFolderType   = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/
417   kMovieDocumentsFolderType     = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/
418   kMusicDocumentsFolderType     = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/
419   kInternetSitesFolderType      = k("site"), /* Refers to the "Sites" folder in a users home directory*/
420   kPublicFolderType             = k("pubb"), /* Refers to the "Public" folder in a users home directory*/
421 
422   kDropBoxFolderType            = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/
423 };
424 
425     private {
426         struct FSRef {
427           char[80] hidden;    /* private to File Manager*/
428         };
429 
430         alias int Boolean;
431         alias int OSType;
432         alias int OSerr;
433 
434         alias da_FSFindFolder = extern(C) int function(short, int, int, FSRef*) nothrow @nogc @system;
435         alias da_FSRefMakePath = extern(C) int function(const(FSRef)*, char*, uint) nothrow @nogc @system;
436 
437         __gshared da_FSFindFolder ptrFSFindFolder = null;
438         __gshared da_FSRefMakePath ptrFSRefMakePath = null;
439     }
440 
441     shared static this()
442     {
443         enum carbonPath = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore";
444 
445         import core.sys.posix.dlfcn;
446 
447         void* handle = dlopen(toStringz(carbonPath), RTLD_NOW | RTLD_LOCAL);
448         if (handle) {
449             ptrFSFindFolder = cast(da_FSFindFolder)dlsym(handle, "FSFindFolder");
450             ptrFSRefMakePath = cast(da_FSRefMakePath)dlsym(handle, "FSRefMakePath");
451         }
452         if (ptrFSFindFolder == null || ptrFSRefMakePath == null) {
453             debug collectException(stderr.writeln("Could not load carbon functions"));
454         }
455     }
456 
457     bool isCarbonLoaded() @trusted @nogc nothrow
458     {
459         return ptrFSFindFolder != null && ptrFSRefMakePath != null;
460     }
461 
462     enum noErr = 0;
463 
464     string fsPath(short domain, OSType type) nothrow @trusted
465     {
466         import std.stdio;   
467         FSRef fsref;
468         if (isCarbonLoaded() && ptrFSFindFolder(domain, type, false, &fsref) == noErr) {
469 
470             char[2048] buf;
471             char* path = buf.ptr;
472             if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) {
473                 try {
474 
475                     return fromStringz(path).idup;
476                 }
477                 catch(Exception e) {
478 
479                 }
480             }
481         }
482         return null;
483     }
484     
485     string writablePath(StandardPath type) nothrow @safe
486     {
487         final switch(type) {
488             case StandardPath.Config:
489                 return fsPath(kUserDomain, kPreferencesFolderType);
490             case StandardPath.Cache:
491                 return fsPath(kUserDomain, kCachedDataFolderType);
492             case StandardPath.Data:
493                 return fsPath(kUserDomain, kApplicationSupportFolderType);
494             case StandardPath.Desktop:
495                 return fsPath(kUserDomain, kDesktopFolderType);
496             case StandardPath.Documents:
497                 return fsPath(kUserDomain, kDocumentsFolderType);
498             case StandardPath.Pictures:
499                 return fsPath(kUserDomain, kPictureDocumentsFolderType);
500             case StandardPath.Music:
501                 return fsPath(kUserDomain, kMusicDocumentsFolderType);
502             case StandardPath.Videos:
503                 return fsPath(kUserDomain, kMovieDocumentsFolderType);
504             case StandardPath.Download:
505                 return fsPath(kUserDomain, kDownloadsFolderType);
506             case StandardPath.Templates:
507                 return null;
508             case StandardPath.PublicShare:
509                 return fsPath(kUserDomain, kPublicFolderType );
510             case StandardPath.Fonts:
511                 return fsPath(kUserDomain, kFontsFolderType);
512             case StandardPath.Applications:
513                 return fsPath(kUserDomain, kApplicationsFolderType);
514         }
515     }
516     
517     string[] standardPaths(StandardPath type) nothrow @safe
518     {
519         string commonPath;
520         
521         switch(type) {
522             case StandardPath.Fonts:
523                 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType);
524                 break;
525             case StandardPath.Applications:
526                 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType);
527                 break;
528             case StandardPath.Data:
529                 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType);
530                 break;
531             case StandardPath.Cache:
532                 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType);
533                 break;
534             default:
535                 break;
536         }
537         
538         string[] paths;
539         string userPath = writablePath(type);
540         if (userPath.length)
541             paths ~= userPath;
542         if (commonPath.length)
543             paths ~= commonPath;
544         return paths;
545     }
546     
547 } else version(Posix) {
548     
549     //Concat two strings, but if the first one is empty, then null string is returned.
550     private string maybeConcat(string start, string path) nothrow @safe
551     {
552         return start.empty ? null : start ~ path;
553     }
554     
555     private string xdgBaseDir(in char[] envvar, string fallback) nothrow @trusted {
556         string dir;
557         collectException(environment.get(envvar), dir);
558         if (!dir.length) {
559             dir = maybeConcat(homeDir(), fallback);
560         }
561         return dir;
562     }
563     
564     private string xdgUserDir(in char[] key, string fallback = null) nothrow @trusted {
565         import std.algorithm : startsWith;
566         import std.string : strip;
567         
568         string fileName = maybeConcat(writablePath(StandardPath.Config), "/user-dirs.dirs");
569         string home = homeDir();
570         try {
571             auto f = File(fileName, "r");
572             
573             auto xdgdir = "XDG_" ~ key ~ "_DIR";
574             
575             char[] buf;
576             while(f.readln(buf)) {
577                 char[] line = strip(buf);
578                 auto index = xdgdir.length;
579                 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') {
580                     line = line[index+1..$];
581                     if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 
582                     {
583                         line = line[1..$-1];
584                     
585                         if (line.startsWith("$HOME")) {
586                             return maybeConcat(home, assumeUnique(line[5..$]));
587                         }
588                         if (line.length == 0 || line[0] != '/') {
589                             continue;
590                         }
591                         return assumeUnique(line);
592                     }
593                 }
594             }
595         } catch(Exception e) {
596             
597         }
598         
599         if (home.length) {
600             try {
601                 auto f = File("/etc/xdg/user-dirs.defaults", "r");
602                 char[] buf;
603                 while(f.readln(buf)) {
604                     char[] line = strip(buf);
605                     auto index = key.length;
606                     if (line.startsWith(key) && line.length > index && line[index] == '=') 
607                     {
608                         line = line[index+1..$];
609                         return home ~ "/" ~ assumeUnique(line);
610                     }
611                 }
612             } catch (Exception e) {
613                 
614             }
615             if (fallback.length) {
616                 return home ~ fallback;
617             }
618         }
619         return null;
620     }
621     
622     private string[] xdgConfigDirs() nothrow @trusted {
623         try {
624             string configDirs = environment.get("XDG_CONFIG_DIRS");
625             if (configDirs.length) {
626                 return splitter(configDirs, pathVarSeparator).array;
627             }
628         }
629         catch(Exception e) {
630             
631         }
632         return ["/etc/xdg"];
633     }
634     
635     private string[] xdgDataDirs() nothrow @trusted {
636         try {
637             string dataDirs = environment.get("XDG_DATA_DIRS");
638             if (dataDirs.length) {
639                 return splitter(dataDirs, pathVarSeparator).array;
640             }
641         } catch(Exception e) {
642             
643         }
644         return ["/usr/local/share", "/usr/share"];
645     }
646     
647     
648     version(fontsconf) {
649         private string[] readFontsConfig(string configFile) nothrow @trusted
650         {
651             //Should be changed in future since std.xml is deprecated
652             import std.xml;
653             
654             string[] paths;
655             try {
656                 string contents = cast(string)read(configFile);
657                 check(contents);
658                 auto parser = new DocumentParser(contents);
659                 parser.onEndTag["dir"] = (in Element xml)
660                 {
661                     string path = xml.text;
662                     
663                     if (path.length && path[0] == '~') {
664                         path = maybeConcat(homeDir(), path[1..$]);
665                     } else {
666                         const(string)* prefix = "prefix" in xml.tag.attr;
667                         if (prefix && *prefix == "xdg") {
668                             string dataPath = writablePath(StandardPath.Data);
669                             if (dataPath.length) {
670                                 path = buildPath(dataPath, path);
671                             }
672                         }
673                     }
674                     if (path.length) {
675                         paths ~= path;
676                     }
677                 };
678                 parser.parse();
679             }
680             catch(Exception e) {
681                 
682             }
683             return paths;
684         }
685         
686         private string[] fontPaths() nothrow @trusted
687         {
688             string[] paths;
689             
690             string homeConfig = homeFontsConfig();
691             if (homeConfig.length) {
692                 paths ~= readFontsConfig(homeConfig);
693             }
694             
695             enum configs = ["/etc/fonts/fonts.conf", //path on linux
696                             "/usr/local/etc/fonts/fonts.conf"]; //path on freebsd
697             foreach(config; configs) {
698                 paths ~= readFontsConfig(config);
699             }
700             return paths;
701         }
702         
703         private string homeFontsConfig() nothrow @trusted {
704             return maybeConcat(writablePath(StandardPath.Config), "/fontconfig/fonts.conf");
705         }
706         
707         private string homeFontsPath() nothrow @trusted {
708             string[] paths = readFontsConfig(homeFontsConfig());
709             if (paths.length)
710                 return paths[0];
711             return null;
712         }
713         
714     } else {
715         private string homeFontsPath() nothrow @trusted {
716             return maybeConcat(homeDir(), "/.fonts");
717         }
718         
719         private string[] fontPaths() nothrow @trusted
720         {
721             enum localShare = "/usr/local/share/fonts";
722             enum share = "/usr/share/fonts";
723             
724             string homeFonts = homeFontsPath();
725             if (homeFonts.length) {
726                 return [homeFonts, localShare, share];
727             } else {
728                 return [localShare, share];
729             }
730         }
731         
732     }
733     
734     
735     
736     /**
737      * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 
738      * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string.
739      * Note: this function is defined only on $(B Posix) systems (except for OS X)
740      */
741     string runtimeDir() nothrow @trusted
742     {
743         // Do we need it on BSD systems?
744         
745         import core.sys.posix.pwd;
746         import core.sys.posix.unistd;
747         import core.sys.posix.sys.stat;
748         import core.sys.posix.sys.types;
749         import core.stdc.errno;
750         import core.stdc.string;
751         
752         const uid_t uid = getuid();
753         string runtime;
754         collectException(environment.get("XDG_RUNTIME_DIR"), runtime);
755         
756         mode_t runtimeMode = octal!700;
757         
758         if (!runtime.length) {
759             setpwent();
760             passwd* pw = getpwuid(uid);
761             endpwent();
762             
763             try {
764                 if (pw && pw.pw_name) {
765                     runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
766                     
767                     if (!(runtime.exists && runtime.isDir)) {
768                         if (mkdir(runtime.toStringz, runtimeMode) != 0) {
769                             debug stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
770                             return null;
771                         }
772                     }
773                 } else {
774                     debug stderr.writeln("Failed to get user name to create runtime directory");
775                     return null;
776                 }
777             } catch(Exception e) {
778                 debug collectException(stderr.writeln("Error when creating runtime directory", e.msg));
779                 return null;
780             }
781         }
782         stat_t statbuf;
783         stat(runtime.toStringz, &statbuf);
784         if (statbuf.st_uid != uid) {
785             debug collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid));
786             return null;
787         }
788         if ((statbuf.st_mode & octal!777) != runtimeMode) {
789             debug collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode));
790             return null;
791         }
792         
793         return runtime;
794     }
795     
796     string writablePath(StandardPath type) nothrow @safe
797     {
798         final switch(type) {
799             case StandardPath.Config:
800                 return xdgBaseDir("XDG_CONFIG_HOME", "/.config");
801             case StandardPath.Cache:
802                 return xdgBaseDir("XDG_CACHE_HOME", "/.cache");
803             case StandardPath.Data:
804                 return xdgBaseDir("XDG_DATA_HOME", "/.local/share");
805             case StandardPath.Desktop:
806                 return xdgUserDir("DESKTOP", "/Desktop");
807             case StandardPath.Documents:
808                 return xdgUserDir("DOCUMENTS");
809             case StandardPath.Pictures:
810                 return xdgUserDir("PICTURES");
811             case StandardPath.Music:
812                 return xdgUserDir("MUSIC");
813             case StandardPath.Videos:
814                 return xdgUserDir("VIDEOS");
815             case StandardPath.Download:
816                 return xdgUserDir("DOWNLOAD");
817             case StandardPath.Templates:
818                 return xdgUserDir("TEMPLATES", "/Templates");
819             case StandardPath.PublicShare:
820                 return xdgUserDir("PUBLICSHARE", "/Public");
821             case StandardPath.Fonts:
822                 return homeFontsPath();
823             case StandardPath.Applications:
824                 return maybeConcat(writablePath(StandardPath.Data), "/applications");
825         }
826     }
827     
828     string[] standardPaths(StandardPath type) nothrow @safe
829     {
830         string[] paths;
831         
832         switch(type) {
833             case StandardPath.Data:
834                 paths = xdgDataDirs();
835                 break;
836             case StandardPath.Config:
837                 paths = xdgConfigDirs();
838                 break;
839             case StandardPath.Applications:
840             {
841                 paths = xdgDataDirs();
842                 foreach(ref path; paths) {
843                     path ~= "/applications";
844                 }
845             }
846                 break;
847             case StandardPath.Fonts:
848                 return fontPaths();
849             default:
850                 break;
851         }
852         
853         string userPath = writablePath(type);
854         if (userPath.length) {
855             paths = userPath ~ paths;
856         }
857         return paths;
858     }
859 } else {
860     static assert(false, "Unsupported platform");
861 }
862 
863 private bool isExecutable(string filePath) nothrow @trusted {
864     try {
865         version(Posix) {
866             import core.sys.posix.unistd;
867             return access(toStringz(filePath), X_OK) == 0;
868         } else version(Windows) {
869             //Use GetEffectiveRightsFromAclW?
870             
871             string extension = filePath.extension;
872             const(string)[] exeExtensions = executableExtensions();
873             foreach(ext; exeExtensions) {
874                 if (sicmp(extension, ext) == 0)
875                     return true;
876             }
877             return false;
878             
879         } else {
880             static assert(false, "Unsupported platform");
881         }
882     } catch(Exception e) {
883         return false;
884     }
885 }
886 
887 private string checkExecutable(string filePath) nothrow @trusted {
888     try {
889         if (filePath.isFile && filePath.isExecutable) {
890             return buildNormalizedPath(filePath);
891         } else {
892             return null;
893         }
894     }
895     catch(Exception e) {
896         return null;
897     }
898 }
899 
900 /**
901  * Finds executable by $(B fileName) in the paths specified by $(B paths).
902  * Returns: absolute path to the existing executable file or an empty string if not found.
903  * Params:
904  *  fileName = name of executable to search
905  *  paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable
906  * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search.
907  */
908 string findExecutable(string fileName, in string[] paths = []) nothrow @safe
909 {
910     try {
911         if (fileName.isAbsolute()) {
912             return checkExecutable(fileName);
913         }
914         
915         const(string)[] searchPaths = paths;
916         if (searchPaths.empty) {
917             string pathVar = environment.get("PATH");
918             if (pathVar.length) {
919                 searchPaths = splitter(pathVar, pathVarSeparator).array;
920             }
921         }
922         
923         if (searchPaths.empty) {
924             return null;
925         }
926         
927         string toReturn;
928         foreach(string path; searchPaths) {
929             string candidate = buildPath(absolutePath(path), fileName);
930             
931             version(Windows) {
932                 if (candidate.extension.empty) {
933                     foreach(exeExtension; executableExtensions()) {
934                         toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower()));
935                         if (toReturn.length) {
936                             return toReturn;
937                         }
938                     }
939                 }
940             }
941             
942             toReturn = checkExecutable(candidate);
943             if (toReturn.length) {
944                 return toReturn;
945             }
946         }
947     } catch (Exception e) {
948         
949     }
950     return null;
951 }
952 
953