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         alias typeof(&RegOpenKeyExW) func_RegOpenKeyEx;
232         alias typeof(&RegQueryValueExW) func_RegQueryValueEx;
233         alias typeof(&RegCloseKey) func_RegCloseKey;
234 
235         __gshared func_RegOpenKeyEx ptrRegOpenKeyEx;
236         __gshared func_RegQueryValueEx ptrRegQueryValueEx;
237         __gshared func_RegCloseKey ptrRegCloseKey;
238         
239         @nogc @trusted bool hasSHGetSpecialFolderPath() nothrow {
240             return ptrSHGetSpecialFolderPath != null;
241         }
242         
243         @nogc @trusted bool isAdvApiLoaded() nothrow {
244             return ptrRegOpenKeyEx && ptrRegQueryValueEx && ptrRegCloseKey;
245         }
246     }
247     
248     shared static this() 
249     {
250         version(LinkedShell32) {} else {
251             HMODULE shellLib = LoadLibraryA("Shell32");
252             if (shellLib) {
253                 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(shellLib, "SHGetSpecialFolderPathW");
254             }
255         }
256         
257         HMODULE advApi = LoadLibraryA("Advapi32.dll");
258         if (advApi) {
259             ptrRegOpenKeyEx = cast(func_RegOpenKeyEx)GetProcAddress(advApi, "RegOpenKeyExW");
260             ptrRegQueryValueEx = cast(func_RegQueryValueEx)GetProcAddress(advApi, "RegQueryValueExW");
261             ptrRegCloseKey = cast(func_RegCloseKey)GetProcAddress(advApi, "RegCloseKey");
262         }
263     }
264     
265     private string getShellFolder(const(wchar)* key) nothrow @trusted
266     {
267         HKEY hKey;
268         if (isAdvApiLoaded()) {    
269             auto result = ptrRegOpenKeyEx(HKEY_CURRENT_USER, 
270                 "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\0"w.ptr,
271                 0,
272                 KEY_QUERY_VALUE,
273                 &hKey
274             );
275             scope(exit) ptrRegCloseKey(hKey);
276             
277             if (result == ERROR_SUCCESS) {
278                 DWORD type;
279                 BYTE[MAX_PATH*wchar.sizeof] buf = void;
280                 DWORD length = cast(DWORD)buf.length;
281                 result = ptrRegQueryValueEx(hKey, key, null, &type, buf.ptr, &length);
282                 if (result == ERROR_SUCCESS && type == REG_SZ && (length % 2 == 0)) {
283                     auto str = cast(wstring)buf[0..length];
284                     try {
285                         return toUTF8(str);
286                     } catch(Exception e) {
287                         
288                     }
289                 }
290             }
291         }
292         
293         return null;
294     }
295     
296     
297     private string getCSIDLFolder(int csidl) nothrow @trusted
298     {
299         import core.stdc.wchar_ : wcslen;
300         
301         wchar[MAX_PATH] path = void;
302         if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) {
303             size_t len = wcslen(path.ptr);
304             try {
305                 return toUTF8(path[0..len]);
306             } catch(Exception e) {
307                 
308             }
309         }
310         return null;
311     }
312     
313     /// Path to $(B Roaming) data directory. This function is Windows only.
314     string roamingPath() nothrow @safe
315     {
316         return getCSIDLFolder(CSIDL_APPDATA);
317     }
318     
319     string writablePath(StandardPath type) nothrow @safe
320     {
321         final switch(type) {
322             case StandardPath.Config:
323             case StandardPath.Data:
324                 return getCSIDLFolder(CSIDL_LOCAL_APPDATA);
325             case StandardPath.Cache:
326             {
327                 string path = getCSIDLFolder(CSIDL_LOCAL_APPDATA);
328                 if (path.length) {
329                     return buildPath(path, "cache");
330                 }
331                 return null;
332             }
333             case StandardPath.Desktop:
334                 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY);
335             case StandardPath.Documents:
336                 return getCSIDLFolder(CSIDL_PERSONAL);
337             case StandardPath.Pictures:
338                 return getCSIDLFolder(CSIDL_MYPICTURES);
339             case StandardPath.Music:
340                 return getCSIDLFolder(CSIDL_MYMUSIC);
341             case StandardPath.Videos:
342                 return getCSIDLFolder(CSIDL_MYVIDEO);
343             case StandardPath.Download:
344                 return getShellFolder("{374DE290-123F-4565-9164-39C4925E467B}\0"w.ptr);
345             case StandardPath.Templates:
346                 return getCSIDLFolder(CSIDL_TEMPLATES);
347             case StandardPath.PublicShare:
348                 return null;
349             case StandardPath.Fonts:
350                 return null;
351             case StandardPath.Applications:
352                 return getCSIDLFolder(CSIDL_PROGRAMS);
353         }
354     }
355     
356     string[] standardPaths(StandardPath type) nothrow @safe
357     {   
358         string commonPath;
359         
360         switch(type) {
361             case StandardPath.Config:
362             case StandardPath.Data:
363                 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA);
364                 break;
365             case StandardPath.Desktop:
366                 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY);
367                 break;
368             case StandardPath.Documents:
369                 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS);
370                 break;
371             case StandardPath.Pictures:
372                 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES);
373                 break;
374             case StandardPath.Music:
375                 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC);
376                 break;
377             case StandardPath.Videos:
378                 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO);
379                 break;
380             case StandardPath.Templates:
381                 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES);
382                 break;
383             case StandardPath.Fonts:
384                 commonPath = getCSIDLFolder(CSIDL_FONTS);
385                 break;
386             case StandardPath.Applications:
387                 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS);
388                 break;
389             default:
390                 break;
391         }
392         
393         string[] paths;
394         string userPath = writablePath(type);
395         if (userPath.length) 
396             paths ~= userPath;
397         if (commonPath.length)
398             paths ~= commonPath;
399         return paths;
400     }
401     
402     private string[] executableExtensions() nothrow @trusted
403     {
404         static bool filenamesEqual(string first, string second) nothrow {
405             try {
406                 return filenameCmp(first, second) == 0;
407             } catch(Exception e) {
408                 return false;
409             }
410         }
411         
412         string[] extensions;
413         try {
414             extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array;
415             if (canFind!(filenamesEqual)(extensions, ".exe") == false) {
416                 extensions = [];
417             }
418         } catch (Exception e) {
419             
420         }
421         if (extensions.empty) {
422             extensions = [".exe", ".com", ".bat", ".cmd"];
423         }
424         return extensions;
425     }
426 } else version(OSX) {
427     
428     private enum : short {
429         kOnSystemDisk                 = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/
430         kOnAppropriateDisk            = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/
431                                                 /* Folder Domains - Carbon only.  The constants above can continue to be used, but the folder/volume returned will*/
432                                                 /* be from one of the domains below.*/
433         kSystemDomain                 = -32766, /* Read-only system hierarchy.*/
434         kLocalDomain                  = -32765, /* All users of a single machine have access to these resources.*/
435         kNetworkDomain                = -32764, /* All users configured to use a common network server has access to these resources.*/
436         kUserDomain                   = -32763, /* Read/write. Resources that are private to the user.*/
437         kClassicDomain                = -32762, /* Domain referring to the currently configured Classic System Folder.  Not supported in Mac OS X Leopard and later.*/
438         kFolderManagerLastDomain      = -32760
439     }
440 
441     private @nogc int k(string s) nothrow {
442         return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3];
443     }
444 
445     private enum {
446         kDesktopFolderType            = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */
447         kTrashFolderType              = k("trsh"), /* the trash folder; objects in this folder show up in the trash */
448         kWhereToEmptyTrashFolderType  = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */
449         kFontsFolderType              = k("font"), /* Fonts go here */
450         kPreferencesFolderType        = k("pref"), /* preferences for applications go here */
451         kSystemPreferencesFolderType  = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */
452         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*/
453                                                 /*    folders such that other users can not access the data inside.  On Mac OS X 10.4 and later the data inside the temporary*/
454                                                 /*    items folder is deleted at logout and at boot, but not otherwise.  Earlier version of Mac OS X would delete items inside*/
455                                                 /*    the temporary items folder after a period of inaccess.  You can ask for a temporary item in a specific domain or on a */
456                                                 /*    particular volume by FSVolumeRefNum.  If you want a location for temporary items for a short time, then use either*/
457                                                 /*    ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ).  The kUserDomain varient will always be*/
458                                                 /*    on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/
459                                                 /*    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*/
460                                                 /*    file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/
461                                                 /*    ask for the temporary items folder on the same volume as the file you are safe saving.*/
462                                                 /*    However, be prepared for a failure to find a temporary folder in any domain or on any volume.  Some volumes may not have*/
463                                                 /*    a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/
464                                                 /*    a temporary folder for the volume.*/
465                                                 /*    If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/
466                                                 /*    and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/
467                                                 /*    restarts.  Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/
468                                                 /*    when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/
469         kChewableItemsFolderType      = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */
470         kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/
471         kApplicationsFolderType       = k("apps"), /*    Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/
472         kVolumeRootFolderType         = k("root"), /* root folder of a volume or domain */
473         kDomainTopLevelFolderType     = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/
474         kDomainLibraryFolderType      = k("dlib"), /* the Library subfolder of a particular domain*/
475         kUsersFolderType              = k("usrs"), /* "Users" folder, usually contains one folder for each user. */
476         kCurrentUserFolderType        = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */
477         kSharedUserDataFolderType     = k("sdat"), /* A Shared folder, readable & writeable by all users */
478         kCachedDataFolderType         = k("cach"), /* Contains various cache files for different clients*/
479         kDownloadsFolderType          = k("down"), /* Refers to the ~/Downloads folder*/
480         kApplicationSupportFolderType = k("asup"), /* third-party items and folders */
481 
482 
483         kDocumentsFolderType          = k("docs"), /*    User documents are typically put in this folder ( or a subfolder ).*/
484         kPictureDocumentsFolderType   = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/
485         kMovieDocumentsFolderType     = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/
486         kMusicDocumentsFolderType     = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/
487         kInternetSitesFolderType      = k("site"), /* Refers to the "Sites" folder in a users home directory*/
488         kPublicFolderType             = k("pubb"), /* Refers to the "Public" folder in a users home directory*/
489 
490         kDropBoxFolderType            = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/
491     };
492 
493     private {
494         struct FSRef {
495           char[80] hidden;    /* private to File Manager*/
496         };
497 
498         alias int Boolean;
499         alias int OSType;
500         alias int OSerr;
501         
502         extern(C) @nogc @system int dummy(short, int, int, FSRef*) nothrow { return 0; }
503         extern(C) @nogc @system int dummy2(const(FSRef)*, char*, uint) nothrow { return 0; }
504 
505         alias da_FSFindFolder = typeof(&dummy);
506         alias da_FSRefMakePath = typeof(&dummy2);
507 
508         __gshared da_FSFindFolder ptrFSFindFolder = null;
509         __gshared da_FSRefMakePath ptrFSRefMakePath = null;
510     }
511 
512     shared static this()
513     {
514         enum carbonPath = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore";
515 
516         import core.sys.posix.dlfcn;
517 
518         void* handle = dlopen(toStringz(carbonPath), RTLD_NOW | RTLD_LOCAL);
519         if (handle) {
520             ptrFSFindFolder = cast(da_FSFindFolder)dlsym(handle, "FSFindFolder");
521             ptrFSRefMakePath = cast(da_FSRefMakePath)dlsym(handle, "FSRefMakePath");
522         }
523         if (ptrFSFindFolder == null || ptrFSRefMakePath == null) {
524             debug collectException(stderr.writeln("Could not load carbon functions"));
525         }
526     }
527 
528     @nogc @trusted bool isCarbonLoaded() nothrow
529     {
530         return ptrFSFindFolder != null && ptrFSRefMakePath != null;
531     }
532 
533     enum noErr = 0;
534 
535     string fsPath(short domain, OSType type) nothrow @trusted
536     {
537         import std.stdio;   
538         FSRef fsref;
539         if (isCarbonLoaded() && ptrFSFindFolder(domain, type, false, &fsref) == noErr) {
540 
541             char[2048] buf;
542             char* path = buf.ptr;
543             if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) {
544                 try {
545 
546                     return fromStringz(path).idup;
547                 }
548                 catch(Exception e) {
549 
550                 }
551             }
552         }
553         return null;
554     }
555     
556     string writablePath(StandardPath type) nothrow @safe
557     {
558         final switch(type) {
559             case StandardPath.Config:
560                 return fsPath(kUserDomain, kPreferencesFolderType);
561             case StandardPath.Cache:
562                 return fsPath(kUserDomain, kCachedDataFolderType);
563             case StandardPath.Data:
564                 return fsPath(kUserDomain, kApplicationSupportFolderType);
565             case StandardPath.Desktop:
566                 return fsPath(kUserDomain, kDesktopFolderType);
567             case StandardPath.Documents:
568                 return fsPath(kUserDomain, kDocumentsFolderType);
569             case StandardPath.Pictures:
570                 return fsPath(kUserDomain, kPictureDocumentsFolderType);
571             case StandardPath.Music:
572                 return fsPath(kUserDomain, kMusicDocumentsFolderType);
573             case StandardPath.Videos:
574                 return fsPath(kUserDomain, kMovieDocumentsFolderType);
575             case StandardPath.Download:
576                 return fsPath(kUserDomain, kDownloadsFolderType);
577             case StandardPath.Templates:
578                 return null;
579             case StandardPath.PublicShare:
580                 return fsPath(kUserDomain, kPublicFolderType );
581             case StandardPath.Fonts:
582                 return fsPath(kUserDomain, kFontsFolderType);
583             case StandardPath.Applications:
584                 return fsPath(kUserDomain, kApplicationsFolderType);
585         }
586     }
587     
588     string[] standardPaths(StandardPath type) nothrow @safe
589     {
590         string commonPath;
591         
592         switch(type) {
593             case StandardPath.Fonts:
594                 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType);
595                 break;
596             case StandardPath.Applications:
597                 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType);
598                 break;
599             case StandardPath.Data:
600                 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType);
601                 break;
602             case StandardPath.Cache:
603                 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType);
604                 break;
605             default:
606                 break;
607         }
608         
609         string[] paths;
610         string userPath = writablePath(type);
611         if (userPath.length)
612             paths ~= userPath;
613         if (commonPath.length)
614             paths ~= commonPath;
615         return paths;
616     }
617     
618 } else version(Posix) {
619     
620     //Concat two strings, but if the first one is empty, then null string is returned.
621     private string maybeConcat(string start, string path) nothrow @safe
622     {
623         return start.empty ? null : start ~ path;
624     }
625     
626     private string xdgBaseDir(string envvar, string fallback) nothrow @trusted {
627         string dir;
628         collectException(environment.get(envvar), dir);
629         if (!dir.length) {
630             dir = maybeConcat(homeDir(), fallback);
631         }
632         return dir;
633     }
634     
635     private string xdgUserDir(string key, string fallback = null) nothrow @trusted {
636         import std.algorithm : startsWith;
637         import std.string : strip;
638         
639         string fileName = maybeConcat(writablePath(StandardPath.Config), "/user-dirs.dirs");
640         string home = homeDir();
641         try {
642             auto f = File(fileName, "r");
643             
644             auto xdgdir = "XDG_" ~ key ~ "_DIR";
645             
646             char[] buf;
647             while(f.readln(buf)) {
648                 char[] line = strip(buf);
649                 auto index = xdgdir.length;
650                 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') {
651                     line = line[index+1..$];
652                     if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 
653                     {
654                         line = line[1..$-1];
655                     
656                         if (line.startsWith("$HOME")) {
657                             return maybeConcat(home, assumeUnique(line[5..$]));
658                         }
659                         if (line.length == 0 || line[0] != '/') {
660                             continue;
661                         }
662                         return assumeUnique(line);
663                     }
664                 }
665             }
666         } catch(Exception e) {
667             
668         }
669         
670         if (home.length) {
671             try {
672                 auto f = File("/etc/xdg/user-dirs.defaults", "r");
673                 char[] buf;
674                 while(f.readln(buf)) {
675                     char[] line = strip(buf);
676                     auto index = key.length;
677                     if (line.startsWith(key) && line.length > index && line[index] == '=') 
678                     {
679                         line = line[index+1..$];
680                         return home ~ "/" ~ assumeUnique(line);
681                     }
682                 }
683             } catch (Exception e) {
684                 
685             }
686             if (fallback.length) {
687                 return home ~ fallback;
688             }
689         }
690         return null;
691     }
692     
693     private string[] xdgConfigDirs() nothrow @trusted {
694         try {
695             string configDirs = environment.get("XDG_CONFIG_DIRS");
696             if (configDirs.length) {
697                 return splitter(configDirs, pathVarSeparator).array;
698             }
699         }
700         catch(Exception e) {
701             
702         }
703         return ["/etc/xdg"];
704     }
705     
706     private string[] xdgDataDirs() nothrow @trusted {
707         try {
708             string dataDirs = environment.get("XDG_DATA_DIRS");
709             if (dataDirs.length) {
710                 return splitter(dataDirs, pathVarSeparator).array;
711             }
712         } catch(Exception e) {
713             
714         }
715         return ["/usr/local/share", "/usr/share"];
716     }
717     
718     
719     version(fontsconf) {
720         private string[] readFontsConfig(string configFile) nothrow @trusted
721         {
722             //Should be changed in future since std.xml is deprecated
723             import std.xml;
724             
725             string[] paths;
726             try {
727                 string contents = cast(string)read(configFile);
728                 check(contents);
729                 auto parser = new DocumentParser(contents);
730                 parser.onEndTag["dir"] = (in Element xml)
731                 {
732                     string path = xml.text;
733                     
734                     if (path.length && path[0] == '~') {
735                         path = maybeConcat(homeDir(), path[1..$]);
736                     } else {
737                         const(string)* prefix = "prefix" in xml.tag.attr;
738                         if (prefix && *prefix == "xdg") {
739                             string dataPath = writablePath(StandardPath.Data);
740                             if (dataPath.length) {
741                                 path = buildPath(dataPath, path);
742                             }
743                         }
744                     }
745                     if (path.length) {
746                         paths ~= path;
747                     }
748                 };
749                 parser.parse();
750             }
751             catch(Exception e) {
752                 
753             }
754             return paths;
755         }
756         
757         private string[] fontPaths() nothrow @trusted
758         {
759             string[] paths;
760             
761             string homeConfig = homeFontsConfig();
762             if (homeConfig.length) {
763                 paths ~= readFontsConfig(homeConfig);
764             }
765             
766             enum configs = ["/etc/fonts/fonts.conf", //path on linux
767                             "/usr/local/etc/fonts/fonts.conf"]; //path on freebsd
768             foreach(config; configs) {
769                 paths ~= readFontsConfig(config);
770             }
771             return paths;
772         }
773         
774         private string homeFontsConfig() nothrow @trusted {
775             return maybeConcat(writablePath(StandardPath.Config), "/fontconfig/fonts.conf");
776         }
777         
778         private string homeFontsPath() nothrow @trusted {
779             string[] paths = readFontsConfig(homeFontsConfig());
780             if (paths.length)
781                 return paths[0];
782             return null;
783         }
784         
785     } else {
786         private string homeFontsPath() nothrow @trusted {
787             return maybeConcat(homeDir(), "/.fonts");
788         }
789         
790         private string[] fontPaths() nothrow @trusted
791         {
792             enum localShare = "/usr/local/share/fonts";
793             enum share = "/usr/share/fonts";
794             
795             string homeFonts = homeFontsPath();
796             if (homeFonts.length) {
797                 return [homeFonts, localShare, share];
798             } else {
799                 return [localShare, share];
800             }
801         }
802         
803     }
804     
805     
806     
807     /**
808      * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 
809      * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string.
810      * Note: this function is defined only on $(B Posix) systems (except for OS X)
811      */
812     string runtimeDir() nothrow @trusted
813     {
814         // Do we need it on BSD systems?
815         
816         import core.sys.posix.pwd;
817         import core.sys.posix.unistd;
818         import core.sys.posix.sys.stat;
819         import core.sys.posix.sys.types;
820         import core.stdc.errno;
821         import core.stdc.string;
822         
823         try { //one try to rule them all and for compatibility reasons
824             const uid_t uid = getuid();
825             string runtime;
826             collectException(environment.get("XDG_RUNTIME_DIR"), runtime);
827             
828             mode_t runtimeMode = octal!700;
829             
830             if (!runtime.length) {
831                 setpwent();
832                 passwd* pw = getpwuid(uid);
833                 endpwent();
834                 
835                 try {
836                     if (pw && pw.pw_name) {
837                         runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
838                         
839                         if (!(runtime.exists && runtime.isDir)) {
840                             if (mkdir(runtime.toStringz, runtimeMode) != 0) {
841                                 debug stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
842                                 return null;
843                             }
844                         }
845                     } else {
846                         debug stderr.writeln("Failed to get user name to create runtime directory");
847                         return null;
848                     }
849                 } catch(Exception e) {
850                     debug collectException(stderr.writefln("Error when creating runtime directory: %s", e.msg));
851                     return null;
852                 }
853             }
854             stat_t statbuf;
855             stat(runtime.toStringz, &statbuf);
856             if (statbuf.st_uid != uid) {
857                 debug collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid));
858                 return null;
859             }
860             if ((statbuf.st_mode & octal!777) != runtimeMode) {
861                 debug collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode));
862                 return null;
863             }
864             
865             return runtime;
866         } catch (Exception e) {
867             debug collectException(stderr.writeln("Error when getting runtime directory: %s", e.msg));
868             return null;
869         }
870     }
871     
872     string writablePath(StandardPath type) nothrow @safe
873     {
874         final switch(type) {
875             case StandardPath.Config:
876                 return xdgBaseDir("XDG_CONFIG_HOME", "/.config");
877             case StandardPath.Cache:
878                 return xdgBaseDir("XDG_CACHE_HOME", "/.cache");
879             case StandardPath.Data:
880                 return xdgBaseDir("XDG_DATA_HOME", "/.local/share");
881             case StandardPath.Desktop:
882                 return xdgUserDir("DESKTOP", "/Desktop");
883             case StandardPath.Documents:
884                 return xdgUserDir("DOCUMENTS");
885             case StandardPath.Pictures:
886                 return xdgUserDir("PICTURES");
887             case StandardPath.Music:
888                 return xdgUserDir("MUSIC");
889             case StandardPath.Videos:
890                 return xdgUserDir("VIDEOS");
891             case StandardPath.Download:
892                 return xdgUserDir("DOWNLOAD");
893             case StandardPath.Templates:
894                 return xdgUserDir("TEMPLATES", "/Templates");
895             case StandardPath.PublicShare:
896                 return xdgUserDir("PUBLICSHARE", "/Public");
897             case StandardPath.Fonts:
898                 return homeFontsPath();
899             case StandardPath.Applications:
900                 return maybeConcat(writablePath(StandardPath.Data), "/applications");
901         }
902     }
903     
904     string[] standardPaths(StandardPath type) nothrow @safe
905     {
906         string[] paths;
907         
908         switch(type) {
909             case StandardPath.Data:
910                 paths = xdgDataDirs();
911                 break;
912             case StandardPath.Config:
913                 paths = xdgConfigDirs();
914                 break;
915             case StandardPath.Applications:
916             {
917                 paths = xdgDataDirs();
918                 foreach(ref path; paths) {
919                     path ~= "/applications";
920                 }
921             }
922                 break;
923             case StandardPath.Fonts:
924                 return fontPaths();
925             default:
926                 break;
927         }
928         
929         string userPath = writablePath(type);
930         if (userPath.length) {
931             paths = userPath ~ paths;
932         }
933         return paths;
934     }
935 } else {
936     static assert(false, "Unsupported platform");
937 }
938 
939 private bool isExecutable(string filePath) nothrow @trusted {
940     try {
941         version(Posix) {
942             import core.sys.posix.unistd;
943             return access(toStringz(filePath), X_OK) == 0;
944         } else version(Windows) {
945             //Use GetEffectiveRightsFromAclW?
946             
947             string extension = filePath.extension;
948             const(string)[] exeExtensions = executableExtensions();
949             foreach(ext; exeExtensions) {
950                 if (sicmp(extension, ext) == 0)
951                     return true;
952             }
953             return false;
954             
955         } else {
956             static assert(false, "Unsupported platform");
957         }
958     } catch(Exception e) {
959         return false;
960     }
961 }
962 
963 private string checkExecutable(string filePath) nothrow @trusted {
964     try {
965         if (filePath.isFile && filePath.isExecutable) {
966             return buildNormalizedPath(filePath);
967         } else {
968             return null;
969         }
970     }
971     catch(Exception e) {
972         return null;
973     }
974 }
975 
976 /**
977  * Finds executable by $(B fileName) in the paths specified by $(B paths).
978  * Returns: absolute path to the existing executable file or an empty string if not found.
979  * Params:
980  *  fileName = name of executable to search
981  *  paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable
982  * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search.
983  */
984 string findExecutable(string fileName, in string[] paths = []) nothrow @safe
985 {
986     @trusted string[] getEnvPaths() { //trusted function for compatibility with older compilers
987         string pathVar = environment.get("PATH");
988         if (pathVar.length) {
989             return splitter(pathVar, pathVarSeparator).array;
990         } else {
991             return null;
992         }
993     }
994     
995     try {
996         if (fileName.isAbsolute()) {
997             return checkExecutable(fileName);
998         }
999         
1000         const(string)[] searchPaths = paths;
1001         if (searchPaths.empty) {
1002             searchPaths = getEnvPaths();
1003         }
1004         
1005         string toReturn;
1006         foreach(string path; searchPaths) {
1007             string candidate = buildPath(absolutePath(path), fileName);
1008             
1009             version(Windows) {
1010                 if (candidate.extension.empty) {
1011                     foreach(exeExtension; executableExtensions()) {
1012                         toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower()));
1013                         if (toReturn.length) {
1014                             return toReturn;
1015                         }
1016                     }
1017                 }
1018             }
1019             
1020             toReturn = checkExecutable(candidate);
1021             if (toReturn.length) {
1022                 return toReturn;
1023             }
1024         }
1025     } catch (Exception e) {
1026         
1027     }
1028     return null;
1029 }
1030 
1031