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