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