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