1 /**
2  * Functions for retrieving standard paths in cross-platform manner.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * License:
6  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
7  * Copyright:
8  *  Roman Chistokhodov 2015-2016
9  */
10 
11 module standardpaths;
12 
13 private {
14     import std.process : environment;
15     import std.path;
16     import std.file;
17     import std.exception;
18     import std.range;
19 
20     import isfreedesktop;
21 
22     debug {
23         import std.stdio : stderr;
24     }
25 
26     string verifyIfNeeded(string path, bool shouldVerify) nothrow @trusted
27     {
28         if (path.length && shouldVerify) {
29             bool dirExists;
30             collectException(path.isDir, dirExists);
31             return dirExists ? path : null;
32         } else {
33             return path;
34         }
35     }
36 
37     string createIfNeeded(string path, bool shouldCreate) nothrow @trusted
38     {
39         if (path.length && shouldCreate) {
40             bool pathExist;
41             collectException(path.isDir, pathExist);
42             if (pathExist || collectException(mkdirRecurse(path)) is null) {
43                 return path;
44             } else {
45                 return null;
46             }
47         } else {
48             return path;
49         }
50     }
51 }
52 
53 version(Windows) {
54     private {
55         import core.sys.windows.windows;
56         import std.utf;
57     }
58 } else version(Posix) {
59     private {
60         //Concat two strings, but if the first one is empty, then null string is returned.
61         string maybeConcat(string start, string path) nothrow @safe
62         {
63             return start.empty ? null : start ~ path;
64         }
65 
66         string maybeBuild(string start, string path) nothrow @safe
67         {
68             return start.empty ? null : buildPath(start, path);
69         }
70     }
71 } else {
72     static assert(false, "Unsupported platform");
73 }
74 
75 /**
76  * Location types that can be passed to $(D writablePath) and $(D standardPaths) functions.
77  *
78  * Not all these paths are suggested for showing in file managers or file dialogs.
79  * Some of them are meant for internal application usage or should be treated in special way.
80  * On usual circumstances user wants to see Desktop, Documents, Downloads, Pictures, Music and Videos directories.
81  *
82  * See_Also:
83  *  $(D writablePath), $(D standardPaths)
84  */
85 enum StandardPath {
86     /**
87      * General location of persisted application data. Every application should have its own subdirectory here.
88      * Note: on Windows it's the same as $(D config) path.
89      */
90     data,
91     /**
92      * General location of configuration files. Every application should have its own subdirectory here.
93      * Note: on Windows it's the same as $(D data) path.
94      */
95     config,
96     /**
97      * Location of cached data.
98      * Note: Not available on Windows.
99      */
100     cache,
101     ///User's desktop directory.
102     desktop,
103     ///User's documents.
104     documents,
105     ///User's pictures.
106     pictures,
107 
108     ///User's music.
109     music,
110 
111     ///User's videos (movies).
112     videos,
113 
114     ///Directory for user's downloaded files.
115     downloads,
116 
117     /**
118      * Location of file templates (e.g. office suite document templates).
119      * Note: Not available on OS X.
120      */
121     templates,
122 
123     /**
124      * Public share folder.
125      * Note: Not available on Windows.
126      */
127     publicShare,
128     /**
129      * Location of fonts files.
130      * 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)
131      */
132     fonts,
133     /**
134      * User's applications. This has different meaning across platforms.
135      * On Windows it's directory where links (.lnk) to programs for Start menu are stored.
136      * On OS X it's folder where applications are typically put.
137      * On Freedesktop it's directory where .desktop files are put.
138      */
139     applications,
140 
141     /**
142      * Automatically started applications.
143      * On Windows it's directory where links (.lnk) to autostarted programs are stored.
144      * On OSX it's not available.
145      * On Freedesktop it's directory where autostarted .desktop files are stored.
146      */
147     startup,
148     /**
149      * Roaming directory that stores a user data which should be shared between user profiles on different machines. Windows-only.
150      */
151     roaming,
152     /**
153      * Common directory for game save files. Windows-only.
154      */
155     savedGames
156 }
157 
158 /**
159  * Control behavior of functions.
160  * See_Also: $(D writablePath)
161  */
162 enum FolderFlag
163 {
164     none = 0,   /// Don't verify that folder exist.
165     /**
166      * Create if folder does not exist.
167      * On Windows and OS X directory will be created using platform specific API, so it will have appropriate icon and other settings special for this kind of folder.
168      */
169     create = 1,
170     /**
171      * Verify that folder exists.
172      * On Windows directory is verified using platform specific API.
173      */
174     verify = 2
175 }
176 
177 /**
178  * Current user home directory.
179  * Returns: Path to user home directory, or an empty string if could not determine home directory.
180  * Relies on environment variables.
181  * Note: This function does not cache its result.
182  */
183 string homeDir() nothrow @safe
184 {
185     try {
186         version(Windows) {
187             //Use GetUserProfileDirectoryW from Userenv.dll?
188             string home = environment.get("USERPROFILE");
189             if (home.empty) {
190                 string homeDrive = environment.get("HOMEDRIVE");
191                 string homePath = environment.get("HOMEPATH");
192                 if (homeDrive.length && homePath.length) {
193                     home = homeDrive ~ homePath;
194                 }
195             }
196             return home;
197         } else {
198             string home = environment.get("HOME");
199             return home;
200         }
201     }
202     catch (Exception e) {
203         debug {
204             @trusted void writeException(Exception e) nothrow {
205                 collectException(stderr.writefln("Error when getting home directory %s", e.msg));
206             }
207             writeException(e);
208         }
209         return null;
210     }
211 }
212 
213 /**
214  * Get writable path for specific location.
215  * Returns: Path where files of $(U type) should be written to by current user, or an empty string if could not determine path.
216  * Params:
217  *  type = Location to lookup.
218  *  params = Union of $(D FolderFlag)s.
219  * Note: This function does not cache its results.
220  * Example:
221 --------------------
222 string downloadsDir = writablePath(StandardPath.downloads, FolderFlag.verify);
223 if (downloadsDir.length) {
224     //Open file dialog with this directory.
225 } else {
226     //Could not detect default downloads directory.
227     //Ask user to choose default downloads directory for this application.
228 }
229 --------------------
230  * See_Also: $(D StandardPath), $(D FolderFlag), $(D standardPaths)
231  */
232 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe;
233 
234 /**
235  * Get paths for various locations.
236  * Returns: Array of paths where files of $(U type) belong including one returned by $(D writablePath), or an empty array if no paths are defined for $(U type).
237  * This function does not ensure if all returned paths exist and appear to be accessible directories. Returned strings are not required to be unique.
238  * Note: This function does not cache its results.
239  * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation.
240  * Example:
241 --------------------
242 string[] templateDirs = standardPaths(StandardPath.templates);
243 //List all available file templates including system defined and user created ones.
244 --------------------
245  * See_Also: $(D StandardPath), $(D writablePath)
246  */
247 string[] standardPaths(StandardPath type) nothrow @safe;
248 
249 /**
250  * Evaluate writable path for specific location and append subfolder.
251  * This can be used with $(D StandardPath.config) and $(D StandardPath.data) to retrieve folder specific for this application instead of generic path.
252  * Returns: Path where files of $(U type) should be written to by current user concatenated with subfolder,
253  *  or an empty string if could not determine path.
254  * Params:
255  *  type = Location to lookup.
256  *  subfolder = Subfolder that will be appended to base writable path.
257  *  params = Union of $(D FolderFlag)s. This affects both base path and sub path.
258  * Note: This function does not cache its results.
259  * Example:
260 --------------------
261 enum organizationName = "MyLittleCompany";
262 enum applicationName = "MyLittleApplication";
263 
264 string configDir = writablePath(StandardPath.config, buildPath(organizationName, applicationName), FolderFlag.create);
265 if (configDir.length) {
266     string configFile = buildPath(configDir, "config.conf");
267     //read or write configuration file.
268 } else {
269     throw new Exception("Could not create application config directory");
270 }
271 --------------------
272  */
273 string writablePath(StandardPath type, string subfolder, FolderFlag params = FolderFlag.none) nothrow @safe
274 {
275     string baseWritablePath = writablePath(type, params);
276     if (baseWritablePath.length) {
277         string toReturn = buildPath(baseWritablePath, subfolder);
278         const bool shouldCreate = (params & FolderFlag.create) != 0;
279         const bool shouldVerify = (params & FolderFlag.verify) != 0;
280         return toReturn.createIfNeeded(shouldCreate).verifyIfNeeded(shouldVerify);
281     } else {
282         return null;
283     }
284 }
285 
286 /**
287  * Evaluate paths for various locations and append subfolder.
288  * Example:
289 --------------------
290 enum organizationName = "MyLittleCompany";
291 enum applicationName = "MyLittleApplication";
292 
293 string[] appDataDirs = standardPaths(StandardPath.data, buildPath(organizationName, applicationName));
294 //Gather data files for this application from each found directory.
295 --------------------
296  */
297 string[] standardPaths(StandardPath type, string subfolder) nothrow @safe
298 {
299     auto toReturn = standardPaths(type);
300     foreach(ref s; toReturn) {
301         s = buildPath(s, subfolder);
302     }
303     return toReturn;
304 }
305 
306 version(D_Ddoc)
307 {
308     /**
309      * Path to $(B Roaming) data directory. Windows only.
310      * Returns: User's Roaming directory. On fail returns an empty string.
311      * See_Also: $(D writablePath), $(D FolderFlag)
312      */
313     deprecated string roamingPath(FolderFlag params = FolderFlag.none) nothrow @safe;
314 
315     /**
316      * Location where games may store their saves. Windows only.
317      * Note: This is common path for games. One should use subfolder for their game saves.
318      * Returns: User's Saved Games directory. On fail returns an empty string.
319      * See_Also: $(D writablePath), $(D FolderFlag)
320      */
321     deprecated string savedGames(FolderFlag params = FolderFlag.none) nothrow @safe;
322 }
323 
324 version(Windows) {
325     private {
326         enum {
327             CSIDL_DESKTOP            =  0,
328             CSIDL_INTERNET,
329             CSIDL_PROGRAMS,
330             CSIDL_CONTROLS,
331             CSIDL_PRINTERS,
332             CSIDL_PERSONAL,
333             CSIDL_FAVORITES,
334             CSIDL_STARTUP,
335             CSIDL_RECENT,
336             CSIDL_SENDTO,
337             CSIDL_BITBUCKET,
338             CSIDL_STARTMENU,      // = 11
339             CSIDL_MYMUSIC            = 13,
340             CSIDL_MYVIDEO,        // = 14
341             CSIDL_DESKTOPDIRECTORY   = 16,
342             CSIDL_DRIVES,
343             CSIDL_NETWORK,
344             CSIDL_NETHOOD,
345             CSIDL_FONTS,
346             CSIDL_TEMPLATES,
347             CSIDL_COMMON_STARTMENU,
348             CSIDL_COMMON_PROGRAMS,
349             CSIDL_COMMON_STARTUP,
350             CSIDL_COMMON_DESKTOPDIRECTORY,
351             CSIDL_APPDATA,
352             CSIDL_PRINTHOOD,
353             CSIDL_LOCAL_APPDATA,
354             CSIDL_ALTSTARTUP,
355             CSIDL_COMMON_ALTSTARTUP,
356             CSIDL_COMMON_FAVORITES,
357             CSIDL_INTERNET_CACHE,
358             CSIDL_COOKIES,
359             CSIDL_HISTORY,
360             CSIDL_COMMON_APPDATA,
361             CSIDL_WINDOWS,
362             CSIDL_SYSTEM,
363             CSIDL_PROGRAM_FILES,
364             CSIDL_MYPICTURES,
365             CSIDL_PROFILE,
366             CSIDL_SYSTEMX86,
367             CSIDL_PROGRAM_FILESX86,
368             CSIDL_PROGRAM_FILES_COMMON,
369             CSIDL_PROGRAM_FILES_COMMONX86,
370             CSIDL_COMMON_TEMPLATES,
371             CSIDL_COMMON_DOCUMENTS,
372             CSIDL_COMMON_ADMINTOOLS,
373             CSIDL_ADMINTOOLS,
374             CSIDL_CONNECTIONS,  // = 49
375             CSIDL_COMMON_MUSIC     = 53,
376             CSIDL_COMMON_PICTURES,
377             CSIDL_COMMON_VIDEO,
378             CSIDL_RESOURCES,
379             CSIDL_RESOURCES_LOCALIZED,
380             CSIDL_COMMON_OEM_LINKS,
381             CSIDL_CDBURN_AREA,  // = 59
382             CSIDL_COMPUTERSNEARME  = 61,
383             CSIDL_FLAG_DONT_VERIFY = 0x4000,
384             CSIDL_FLAG_CREATE      = 0x8000,
385             CSIDL_FLAG_MASK        = 0xFF00
386         }
387 
388         enum {
389             KF_FLAG_SIMPLE_IDLIST                = 0x00000100,
390             KF_FLAG_NOT_PARENT_RELATIVE          = 0x00000200,
391             KF_FLAG_DEFAULT_PATH                 = 0x00000400,
392             KF_FLAG_INIT                         = 0x00000800,
393             KF_FLAG_NO_ALIAS                     = 0x00001000,
394             KF_FLAG_DONT_UNEXPAND                = 0x00002000,
395             KF_FLAG_DONT_VERIFY                  = 0x00004000,
396             KF_FLAG_CREATE                       = 0x00008000,
397             KF_FLAG_NO_APPCONTAINER_REDIRECTION  = 0x00010000,
398             KF_FLAG_ALIAS_ONLY                   = 0x80000000
399         };
400 
401         alias GUID KNOWNFOLDERID;
402 
403         enum KNOWNFOLDERID FOLDERID_LocalAppData = {0xf1b32785, 0x6fba, 0x4fcf, [0x9d,0x55,0x7b,0x8e,0x7f,0x15,0x70,0x91]};
404         enum KNOWNFOLDERID FOLDERID_RoamingAppData = {0x3eb685db, 0x65f9, 0x4cf6, [0xa0,0x3a,0xe3,0xef,0x65,0x72,0x9f,0x3d]};
405 
406         enum KNOWNFOLDERID FOLDERID_Desktop = {0xb4bfcc3a, 0xdb2c, 0x424c, [0xb0,0x29,0x7f,0xe9,0x9a,0x87,0xc6,0x41]};
407         enum KNOWNFOLDERID FOLDERID_Documents = {0xfdd39ad0, 0x238f, 0x46af, [0xad,0xb4,0x6c,0x85,0x48,0x3,0x69,0xc7]};
408         enum KNOWNFOLDERID FOLDERID_Downloads = {0x374de290, 0x123f, 0x4565, [0x91,0x64,0x39,0xc4,0x92,0x5e,0x46,0x7b]};
409         enum KNOWNFOLDERID FOLDERID_Favorites = {0x1777f761, 0x68ad, 0x4d8a, [0x87,0xbd,0x30,0xb7,0x59,0xfa,0x33,0xdd]};
410         enum KNOWNFOLDERID FOLDERID_Links = {0xbfb9d5e0, 0xc6a9, 0x404c, [0xb2,0xb2,0xae,0x6d,0xb6,0xaf,0x49,0x68]};
411         enum KNOWNFOLDERID FOLDERID_Music = {0x4bd8d571, 0x6d19, 0x48d3, [0xbe,0x97,0x42,0x22,0x20,0x8,0xe,0x43]};
412         enum KNOWNFOLDERID FOLDERID_Pictures = {0x33e28130, 0x4e1e, 0x4676, [0x83,0x5a,0x98,0x39,0x5c,0x3b,0xc3,0xbb]};
413         enum KNOWNFOLDERID FOLDERID_Programs = {0xa77f5d77, 0x2e2b, 0x44c3, [0xa6,0xa2,0xab,0xa6,0x1,0x5,0x4a,0x51]};
414         enum KNOWNFOLDERID FOLDERID_SavedGames = {0x4c5c32ff, 0xbb9d, 0x43b0, [0xb5,0xb4,0x2d,0x72,0xe5,0x4e,0xaa,0xa4]};
415         enum KNOWNFOLDERID FOLDERID_Startup = {0xb97d20bb, 0xf46a, 0x4c97, [0xba,0x10,0x5e,0x36,0x8,0x43,0x8,0x54]};
416         enum KNOWNFOLDERID FOLDERID_Templates = {0xa63293e8, 0x664e, 0x48db, [0xa0,0x79,0xdf,0x75,0x9e,0x5,0x9,0xf7]};
417         enum KNOWNFOLDERID FOLDERID_Videos = {0x18989b1d, 0x99b5, 0x455b, [0x84,0x1c,0xab,0x7c,0x74,0xe4,0xdd,0xfc]};
418 
419         enum KNOWNFOLDERID FOLDERID_Fonts = {0xfd228cb7, 0xae11, 0x4ae3, [0x86,0x4c,0x16,0xf3,0x91,0xa,0xb8,0xfe]};
420         enum KNOWNFOLDERID FOLDERID_ProgramData = {0x62ab5d82, 0xfdc1, 0x4dc3, [0xa9,0xdd,0x7,0xd,0x1d,0x49,0x5d,0x97]};
421         enum KNOWNFOLDERID FOLDERID_CommonPrograms = {0x139d44e, 0x6afe, 0x49f2, [0x86,0x90,0x3d,0xaf,0xca,0xe6,0xff,0xb8]};
422         enum KNOWNFOLDERID FOLDERID_CommonStartup = {0x82a5ea35, 0xd9cd, 0x47c5, [0x96,0x29,0xe1,0x5d,0x2f,0x71,0x4e,0x6e]};
423         enum KNOWNFOLDERID FOLDERID_CommonTemplates = {0xb94237e7, 0x57ac, 0x4347, [0x91,0x51,0xb0,0x8c,0x6c,0x32,0xd1,0xf7]};
424 
425         enum KNOWNFOLDERID FOLDERID_PublicDesktop = {0xc4aa340d, 0xf20f, 0x4863, [0xaf,0xef,0xf8,0x7e,0xf2,0xe6,0xba,0x25]};
426         enum KNOWNFOLDERID FOLDERID_PublicDocuments = {0xed4824af, 0xdce4, 0x45a8, [0x81,0xe2,0xfc,0x79,0x65,0x8,0x36,0x34]};
427         enum KNOWNFOLDERID FOLDERID_PublicDownloads = {0x3d644c9b, 0x1fb8, 0x4f30, [0x9b,0x45,0xf6,0x70,0x23,0x5f,0x79,0xc0]};
428         enum KNOWNFOLDERID FOLDERID_PublicMusic = {0x3214fab5, 0x9757, 0x4298, [0xbb,0x61,0x92,0xa9,0xde,0xaa,0x44,0xff]};
429         enum KNOWNFOLDERID FOLDERID_PublicPictures = {0xb6ebfb86, 0x6907, 0x413c, [0x9a,0xf7,0x4f,0xc2,0xab,0xf0,0x7c,0xc5]};
430         enum KNOWNFOLDERID FOLDERID_PublicVideos = {0x2400183a, 0x6185, 0x49fb, [0xa2,0xd8,0x4a,0x39,0x2a,0x60,0x2b,0xa3]};
431     }
432 
433     private  {
434         extern(Windows) @nogc @system BOOL _dummy_SHGetSpecialFolderPath(HWND, wchar*, int, BOOL) nothrow { return 0; }
435         extern(Windows) @nogc @system HRESULT _dummy_SHGetKnownFolderPath(const(KNOWNFOLDERID)* rfid, DWORD dwFlags, HANDLE hToken, wchar** ppszPath) nothrow { return 0; }
436         extern(Windows) @nogc @system void _dummy_CoTaskMemFree(void* pv) nothrow {return;}
437 
438         __gshared typeof(&_dummy_SHGetSpecialFolderPath) ptrSHGetSpecialFolderPath = null;
439         __gshared typeof(&_dummy_SHGetKnownFolderPath) ptrSHGetKnownFolderPath = null;
440         __gshared typeof(&_dummy_CoTaskMemFree) ptrCoTaskMemFree = null;
441 
442         @nogc @trusted bool hasSHGetSpecialFolderPath() nothrow {
443             return ptrSHGetSpecialFolderPath !is null;
444         }
445 
446         @nogc @trusted bool hasSHGetKnownFolderPath() nothrow {
447             return ptrSHGetKnownFolderPath !is null && ptrCoTaskMemFree !is null;
448         }
449     }
450 
451     shared static this()
452     {
453         HMODULE shellLib = LoadLibraryA("Shell32");
454         if (shellLib !is null) {
455             ptrSHGetKnownFolderPath = cast(typeof(ptrSHGetKnownFolderPath))enforce(GetProcAddress(shellLib, "SHGetKnownFolderPath"));
456             if (ptrSHGetKnownFolderPath) {
457                 HMODULE ole = LoadLibraryA("Ole32");
458                 if (ole !is null) {
459                     ptrCoTaskMemFree = cast(typeof(ptrCoTaskMemFree))enforce(GetProcAddress(ole, "CoTaskMemFree"));
460                     if (!ptrCoTaskMemFree) {
461                         FreeLibrary(ole);
462                     }
463                 }
464             }
465 
466             if (!hasSHGetKnownFolderPath()) {
467                 ptrSHGetSpecialFolderPath = cast(typeof(ptrSHGetSpecialFolderPath))GetProcAddress(shellLib, "SHGetSpecialFolderPathW");
468             }
469         }
470     }
471 
472     private string getCSIDLFolder(int csidl, FolderFlag params = FolderFlag.none) nothrow @trusted {
473         import core.stdc.wchar_ : wcslen;
474 
475         if (params & FolderFlag.create) {
476             csidl |= CSIDL_FLAG_CREATE;
477         }
478         if (!(params & FolderFlag.verify)) {
479             csidl |= CSIDL_FLAG_DONT_VERIFY;
480         }
481         wchar[MAX_PATH] path = void;
482         if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) {
483             size_t len = wcslen(path.ptr);
484             try {
485                 return toUTF8(path[0..len]);
486             } catch(Exception e) {
487 
488             }
489         }
490         return null;
491     }
492 
493     private string getKnownFolder(const(KNOWNFOLDERID) folder, FolderFlag params = FolderFlag.none) nothrow @trusted {
494         import core.stdc.wchar_ : wcslen;
495 
496         wchar* str;
497 
498         DWORD flags = 0;
499         if (params & FolderFlag.create) {
500             flags |= KF_FLAG_CREATE;
501         }
502         if (!(params & FolderFlag.verify)) {
503             flags |= KF_FLAG_DONT_VERIFY;
504         }
505 
506         if (hasSHGetKnownFolderPath() && ptrSHGetKnownFolderPath(&folder, flags, null, &str) == S_OK) {
507             scope(exit) ptrCoTaskMemFree(str);
508             try {
509                 return str[0..wcslen(str)].toUTF8;
510             } catch(Exception e) {
511 
512             }
513         }
514         return null;
515     }
516 
517     deprecated("use writablePath(StandardPath.roaming)") string roamingPath(FolderFlag params = FolderFlag.none) nothrow @safe
518     {
519         return writablePath(StandardPath.roaming, params);
520     }
521 
522     deprecated("use writablePath(StandardPath.savedGames)") string savedGames(FolderFlag params = FolderFlag.none) nothrow @safe
523     {
524         return writablePath(StandardPath.savedGames, params);
525     }
526 
527     string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe
528     {
529         if (hasSHGetKnownFolderPath()) {
530             final switch(type) {
531                 case StandardPath.config:
532                 case StandardPath.data:
533                     return getKnownFolder(FOLDERID_LocalAppData, params);
534                 case StandardPath.cache:
535                     return null;
536                 case StandardPath.desktop:
537                     return getKnownFolder(FOLDERID_Desktop, params);
538                 case StandardPath.documents:
539                     return getKnownFolder(FOLDERID_Documents, params);
540                 case StandardPath.pictures:
541                     return getKnownFolder(FOLDERID_Pictures, params);
542                 case StandardPath.music:
543                     return getKnownFolder(FOLDERID_Music, params);
544                 case StandardPath.videos:
545                     return getKnownFolder(FOLDERID_Videos, params);
546                 case StandardPath.downloads:
547                     return getKnownFolder(FOLDERID_Downloads, params);
548                 case StandardPath.templates:
549                     return getKnownFolder(FOLDERID_Templates, params);
550                 case StandardPath.publicShare:
551                     return null;
552                 case StandardPath.fonts:
553                     return null;
554                 case StandardPath.applications:
555                     return getKnownFolder(FOLDERID_Programs, params);
556                 case StandardPath.startup:
557                     return getKnownFolder(FOLDERID_Startup, params);
558                 case StandardPath.roaming:
559                     return getKnownFolder(FOLDERID_RoamingAppData, params);
560                 case StandardPath.savedGames:
561                     return getKnownFolder(FOLDERID_SavedGames, params);
562             }
563         } else if (hasSHGetSpecialFolderPath()) {
564             final switch(type) {
565                 case StandardPath.config:
566                 case StandardPath.data:
567                     return getCSIDLFolder(CSIDL_LOCAL_APPDATA, params);
568                 case StandardPath.cache:
569                     return null;
570                 case StandardPath.desktop:
571                     return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY, params);
572                 case StandardPath.documents:
573                     return getCSIDLFolder(CSIDL_PERSONAL, params);
574                 case StandardPath.pictures:
575                     return getCSIDLFolder(CSIDL_MYPICTURES, params);
576                 case StandardPath.music:
577                     return getCSIDLFolder(CSIDL_MYMUSIC, params);
578                 case StandardPath.videos:
579                     return getCSIDLFolder(CSIDL_MYVIDEO, params);
580                 case StandardPath.downloads:
581                     return null;
582                 case StandardPath.templates:
583                     return getCSIDLFolder(CSIDL_TEMPLATES, params);
584                 case StandardPath.publicShare:
585                     return null;
586                 case StandardPath.fonts:
587                     return null;
588                 case StandardPath.applications:
589                     return getCSIDLFolder(CSIDL_PROGRAMS, params);
590                 case StandardPath.startup:
591                     return getCSIDLFolder(CSIDL_STARTUP, params);
592                 case StandardPath.roaming:
593                     return getCSIDLFolder(CSIDL_APPDATA, params);
594                 case StandardPath.savedGames:
595                     return null;
596             }
597         } else {
598             return null;
599         }
600     }
601 
602     string[] standardPaths(StandardPath type) nothrow @safe
603     {
604         string commonPath;
605 
606         if (hasSHGetKnownFolderPath()) {
607             switch(type) {
608                 case StandardPath.config:
609                 case StandardPath.data:
610                     commonPath = getKnownFolder(FOLDERID_ProgramData);
611                     break;
612                 case StandardPath.desktop:
613                     commonPath = getKnownFolder(FOLDERID_PublicDesktop);
614                     break;
615                 case StandardPath.documents:
616                     commonPath = getKnownFolder(FOLDERID_PublicDocuments);
617                     break;
618                 case StandardPath.pictures:
619                     commonPath = getKnownFolder(FOLDERID_PublicPictures);
620                     break;
621                 case StandardPath.music:
622                     commonPath = getKnownFolder(FOLDERID_PublicMusic);
623                     break;
624                 case StandardPath.videos:
625                     commonPath = getKnownFolder(FOLDERID_PublicVideos);
626                     break;
627                 case StandardPath.downloads:
628                     commonPath = getKnownFolder(FOLDERID_PublicDownloads);
629                     break;
630                 case StandardPath.templates:
631                     commonPath = getKnownFolder(FOLDERID_CommonTemplates);
632                     break;
633                 case StandardPath.fonts:
634                     commonPath = getKnownFolder(FOLDERID_Fonts);
635                     break;
636                 case StandardPath.applications:
637                     commonPath = getKnownFolder(FOLDERID_CommonPrograms);
638                     break;
639                 case StandardPath.startup:
640                     commonPath = getKnownFolder(FOLDERID_CommonStartup);
641                     break;
642                 default:
643                     break;
644             }
645         } else if (hasSHGetSpecialFolderPath()) {
646             switch(type) {
647                 case StandardPath.config:
648                 case StandardPath.data:
649                     commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA);
650                     break;
651                 case StandardPath.desktop:
652                     commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY);
653                     break;
654                 case StandardPath.documents:
655                     commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS);
656                     break;
657                 case StandardPath.pictures:
658                     commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES);
659                     break;
660                 case StandardPath.music:
661                     commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC);
662                     break;
663                 case StandardPath.videos:
664                     commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO);
665                     break;
666                 case StandardPath.templates:
667                     commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES);
668                     break;
669                 case StandardPath.fonts:
670                     commonPath = getCSIDLFolder(CSIDL_FONTS);
671                     break;
672                 case StandardPath.applications:
673                     commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS);
674                     break;
675                 case StandardPath.startup:
676                     commonPath = getCSIDLFolder(CSIDL_COMMON_STARTUP);
677                     break;
678                 default:
679                     break;
680             }
681         }
682 
683         string[] paths;
684         string userPath = writablePath(type);
685         if (userPath.length)
686             paths ~= userPath;
687         if (commonPath.length)
688             paths ~= commonPath;
689         return paths;
690     }
691 } else version(OSX) {
692     private {
693         import std.string : fromStringz;
694         version(StandardPathsCocoa) {
695             import core.attribute : selector;
696 
697             alias size_t NSUInteger;
698 
699             enum objectiveC_declarations = q{
700                 extern (Objective-C)
701                 interface NSString
702                 {
703                     NSString initWithUTF8String(in char* str) @selector("initWithUTF8String:");
704                     const(char)* UTF8String() @selector("UTF8String");
705                     void release() @selector("release");
706                 }
707 
708                 extern(Objective-C)
709                 interface NSArray
710                 {
711                     NSString objectAtIndex(size_t) @selector("objectAtIndex:");
712                     NSString firstObject() @selector("firstObject");
713                     NSUInteger count() @selector("count");
714                     void release() @selector("release");
715                 }
716 
717                 extern(Objective-C)
718                 interface NSURL
719                 {
720                     NSString absoluteString() @selector("absoluteString");
721                     void release() @selector("release");
722                 }
723 
724                 extern(Objective-C)
725                 interface NSError
726                 {
727 
728                 }
729 
730                 extern (C) NSFileManager objc_lookUpClass(in char* name);
731 
732                 extern(Objective-C)
733                 interface NSFileManager
734                 {
735                     NSFileManager defaultManager() @selector("defaultManager");
736                     NSURL URLForDirectory(NSSearchPathDirectory, NSSearchPathDomainMask domain, NSURL url, int shouldCreate, NSError* error) @selector("URLForDirectory:inDomain:appropriateForURL:create:error:");
737                 }
738             };
739 
740             mixin(objectiveC_declarations);
741 
742             enum : NSUInteger {
743                NSApplicationDirectory = 1,
744                NSDemoApplicationDirectory,
745                NSDeveloperApplicationDirectory,
746                NSAdminApplicationDirectory,
747                NSLibraryDirectory,
748                NSDeveloperDirectory,
749                NSUserDirectory,
750                NSDocumentationDirectory,
751                NSDocumentDirectory,
752                NSCoreServiceDirectory,
753                NSAutosavedInformationDirectory = 11,
754                NSDesktopDirectory = 12,
755                NSCachesDirectory = 13,
756                NSApplicationSupportDirectory = 14,
757                NSDownloadsDirectory = 15,
758                NSInputMethodsDirectory = 16,
759                NSMoviesDirectory = 17,
760                NSMusicDirectory = 18,
761                NSPicturesDirectory = 19,
762                NSPrinterDescriptionDirectory = 20,
763                NSSharedPublicDirectory = 21,
764                NSPreferencePanesDirectory = 22,
765                NSItemReplacementDirectory = 99,
766                NSAllApplicationsDirectory = 100,
767                NSAllLibrariesDirectory = 101,
768             };
769 
770             alias NSUInteger NSSearchPathDirectory;
771 
772             enum : NSUInteger {
773                NSUserDomainMask = 1,
774                NSLocalDomainMask = 2,
775                NSNetworkDomainMask = 4,
776                NSSystemDomainMask = 8,
777                NSAllDomainsMask = 0x0ffff,
778             };
779 
780             alias NSUInteger NSSearchPathDomainMask;
781 
782             string domainDir(NSSearchPathDirectory dir, NSSearchPathDomainMask domain, bool shouldCreate = false) nothrow @trusted
783             {
784                 import std.uri;
785                 import std.algorithm : startsWith;
786 
787                 try {
788                     auto managerInterface = objc_lookUpClass("NSFileManager");
789                     if (!managerInterface) {
790                         return null;
791                     }
792 
793                     auto manager = managerInterface.defaultManager();
794                     if (!manager) {
795                         return null;
796                     }
797 
798                     NSURL url = manager.URLForDirectory(dir, domain, null, shouldCreate, null);
799                     if (!url) {
800                         return null;
801                     }
802                     scope(exit) url.release();
803                     NSString nsstr = url.absoluteString();
804                     scope(exit) nsstr.release();
805 
806                     string str = fromStringz(nsstr.UTF8String()).idup;
807 
808                     enum fileProtocol = "file://";
809                     if (str.startsWith(fileProtocol)) {
810                         str = str.decode()[fileProtocol.length..$];
811                         if (str.length > 1 && str[$-1] == '/') {
812                             return str[0..$-1];
813                         } else {
814                             return str;
815                         }
816                     }
817                 } catch(Exception e) {
818 
819                 }
820                 return null;
821             }
822         } else {
823             private enum : short {
824                 kOnSystemDisk                 = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/
825                 kOnAppropriateDisk            = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/
826                                                         /* Folder Domains - Carbon only.  The constants above can continue to be used, but the folder/volume returned will*/
827                                                         /* be from one of the domains below.*/
828                 kSystemDomain                 = -32766, /* Read-only system hierarchy.*/
829                 kLocalDomain                  = -32765, /* All users of a single machine have access to these resources.*/
830                 kNetworkDomain                = -32764, /* All users configured to use a common network server has access to these resources.*/
831                 kUserDomain                   = -32763, /* Read/write. Resources that are private to the user.*/
832                 kClassicDomain                = -32762, /* Domain referring to the currently configured Classic System Folder.  Not supported in Mac OS X Leopard and later.*/
833                 kFolderManagerLastDomain      = -32760
834             }
835 
836             private @nogc int k(string s) nothrow {
837                 return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3];
838             }
839 
840             private enum {
841                 kDesktopFolderType            = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */
842                 kTrashFolderType              = k("trsh"), /* the trash folder; objects in this folder show up in the trash */
843                 kWhereToEmptyTrashFolderType  = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */
844                 kFontsFolderType              = k("font"), /* Fonts go here */
845                 kPreferencesFolderType        = k("pref"), /* preferences for applications go here */
846                 kSystemPreferencesFolderType  = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */
847                 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*/
848                                                         /*    folders such that other users can not access the data inside.  On Mac OS X 10.4 and later the data inside the temporary*/
849                                                         /*    items folder is deleted at logout and at boot, but not otherwise.  Earlier version of Mac OS X would delete items inside*/
850                                                         /*    the temporary items folder after a period of inaccess.  You can ask for a temporary item in a specific domain or on a */
851                                                         /*    particular volume by FSVolumeRefNum.  If you want a location for temporary items for a short time, then use either*/
852                                                         /*    ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ).  The kUserDomain varient will always be*/
853                                                         /*    on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/
854                                                         /*    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*/
855                                                         /*    file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/
856                                                         /*    ask for the temporary items folder on the same volume as the file you are safe saving.*/
857                                                         /*    However, be prepared for a failure to find a temporary folder in any domain or on any volume.  Some volumes may not have*/
858                                                         /*    a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/
859                                                         /*    a temporary folder for the volume.*/
860                                                         /*    If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/
861                                                         /*    and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/
862                                                         /*    restarts.  Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/
863                                                         /*    when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/
864                 kChewableItemsFolderType      = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */
865                 kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/
866                 kApplicationsFolderType       = k("apps"), /*    Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/
867                 kVolumeRootFolderType         = k("root"), /* root folder of a volume or domain */
868                 kDomainTopLevelFolderType     = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/
869                 kDomainLibraryFolderType      = k("dlib"), /* the Library subfolder of a particular domain*/
870                 kUsersFolderType              = k("usrs"), /* "Users" folder, usually contains one folder for each user. */
871                 kCurrentUserFolderType        = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */
872                 kSharedUserDataFolderType     = k("sdat"), /* A Shared folder, readable & writeable by all users */
873                 kCachedDataFolderType         = k("cach"), /* Contains various cache files for different clients*/
874                 kDownloadsFolderType          = k("down"), /* Refers to the ~/Downloads folder*/
875                 kApplicationSupportFolderType = k("asup"), /* third-party items and folders */
876 
877 
878                 kDocumentsFolderType          = k("docs"), /*    User documents are typically put in this folder ( or a subfolder ).*/
879                 kPictureDocumentsFolderType   = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/
880                 kMovieDocumentsFolderType     = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/
881                 kMusicDocumentsFolderType     = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/
882                 kInternetSitesFolderType      = k("site"), /* Refers to the "Sites" folder in a users home directory*/
883                 kPublicFolderType             = k("pubb"), /* Refers to the "Public" folder in a users home directory*/
884 
885                 kDropBoxFolderType            = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/
886             };
887 
888             struct FSRef {
889               char[80] hidden;    /* private to File Manager*/
890             };
891 
892             alias ubyte Boolean;
893             alias int OSType;
894             alias short OSErr;
895             alias int OSStatus;
896 
897             extern(C) @nogc @system OSErr _dummy_FSFindFolder(short, OSType, Boolean, FSRef*) nothrow { return 0; }
898             extern(C) @nogc @system OSStatus _dummy_FSRefMakePath(const(FSRef)*, char*, uint) nothrow { return 0; }
899 
900             __gshared typeof(&_dummy_FSFindFolder) ptrFSFindFolder = null;
901             __gshared typeof(&_dummy_FSRefMakePath) ptrFSRefMakePath = null;
902         }
903     }
904 
905     version(StandardPathsCocoa) {
906 
907     } else {
908         shared static this()
909         {
910             enum carbonPath = "CoreServices.framework/Versions/A/CoreServices\0";
911 
912             import core.sys.posix.dlfcn;
913 
914             void* handle = dlopen(carbonPath.ptr, RTLD_NOW | RTLD_LOCAL);
915             if (handle) {
916                 ptrFSFindFolder = cast(typeof(ptrFSFindFolder))dlsym(handle, "FSFindFolder");
917                 ptrFSRefMakePath = cast(typeof(ptrFSRefMakePath))dlsym(handle, "FSRefMakePath");
918             }
919             if (ptrFSFindFolder == null || ptrFSRefMakePath == null) {
920                 debug collectException(stderr.writeln("Could not load carbon functions"));
921                 if (handle) dlclose(handle);
922             }
923         }
924 
925         private @nogc @trusted bool isCarbonLoaded() nothrow
926         {
927             return ptrFSFindFolder != null && ptrFSRefMakePath != null;
928         }
929 
930         private enum OSErr noErr = 0;
931 
932         private string fsPath(short domain, OSType type, bool shouldCreate = false) nothrow @trusted
933         {
934             import std.stdio;
935             FSRef fsref;
936             if (isCarbonLoaded() && ptrFSFindFolder(domain, type, shouldCreate, &fsref) == noErr) {
937 
938                 char[2048] buf;
939                 char* path = buf.ptr;
940                 if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) {
941                     try {
942                         return fromStringz(path).idup;
943                     }
944                     catch(Exception e) {
945 
946                     }
947                 }
948             }
949             return null;
950         }
951     }
952 
953     private string writablePathImpl(StandardPath type, bool shouldCreate = false) nothrow @safe
954     {
955         version(StandardPathsCocoa) {
956             final switch(type) {
957                 case StandardPath.config:
958                     return domainDir(NSLibraryDirectory, NSUserDomainMask, shouldCreate).maybeBuild("Preferences").createIfNeeded(shouldCreate);
959                 case StandardPath.cache:
960                     return domainDir(NSCachesDirectory, NSUserDomainMask, shouldCreate);
961                 case StandardPath.data:
962                     return domainDir(NSApplicationSupportDirectory, NSUserDomainMask, shouldCreate);
963                 case StandardPath.desktop:
964                     return domainDir(NSDesktopDirectory, NSUserDomainMask, shouldCreate);
965                 case StandardPath.documents:
966                     return domainDir(NSDocumentDirectory, NSUserDomainMask, shouldCreate);
967                 case StandardPath.pictures:
968                     return domainDir(NSPicturesDirectory, NSUserDomainMask, shouldCreate);
969                 case StandardPath.music:
970                     return domainDir(NSMusicDirectory, NSUserDomainMask, shouldCreate);
971                 case StandardPath.videos:
972                     return domainDir(NSMoviesDirectory, NSUserDomainMask, shouldCreate);
973                 case StandardPath.downloads:
974                     return domainDir(NSDownloadsDirectory, NSUserDomainMask, shouldCreate);
975                 case StandardPath.templates:
976                     return null;
977                 case StandardPath.publicShare:
978                     return domainDir(NSSharedPublicDirectory, NSUserDomainMask, shouldCreate);
979                 case StandardPath.fonts:
980                     return domainDir(NSLibraryDirectory, NSUserDomainMask, shouldCreate).maybeBuild("Fonts").createIfNeeded(shouldCreate);
981                 case StandardPath.applications:
982                     return domainDir(NSApplicationDirectory, NSUserDomainMask, shouldCreate);
983                 case StandardPath.startup:
984                     return null;
985                 case StandardPath.roaming:
986                     return null;
987                 case StandardPath.savedGames:
988                     return null;
989             }
990         } else {
991             final switch(type) {
992                 case StandardPath.config:
993                     return fsPath(kUserDomain, kPreferencesFolderType, shouldCreate);
994                 case StandardPath.cache:
995                     return fsPath(kUserDomain, kCachedDataFolderType, shouldCreate);
996                 case StandardPath.data:
997                     return fsPath(kUserDomain, kApplicationSupportFolderType, shouldCreate);
998                 case StandardPath.desktop:
999                     return fsPath(kUserDomain, kDesktopFolderType, shouldCreate);
1000                 case StandardPath.documents:
1001                     return fsPath(kUserDomain, kDocumentsFolderType, shouldCreate);
1002                 case StandardPath.pictures:
1003                     return fsPath(kUserDomain, kPictureDocumentsFolderType, shouldCreate);
1004                 case StandardPath.music:
1005                     return fsPath(kUserDomain, kMusicDocumentsFolderType, shouldCreate);
1006                 case StandardPath.videos:
1007                     return fsPath(kUserDomain, kMovieDocumentsFolderType, shouldCreate);
1008                 case StandardPath.downloads:
1009                     return fsPath(kUserDomain, kDownloadsFolderType, shouldCreate);
1010                 case StandardPath.templates:
1011                     return null;
1012                 case StandardPath.publicShare:
1013                     return fsPath(kUserDomain, kPublicFolderType, shouldCreate);
1014                 case StandardPath.fonts:
1015                     return fsPath(kUserDomain, kFontsFolderType, shouldCreate);
1016                 case StandardPath.applications:
1017                     return fsPath(kUserDomain, kApplicationsFolderType, shouldCreate);
1018                 case StandardPath.startup:
1019                     return null;
1020                 case StandardPath.roaming:
1021                     return null;
1022                 case StandardPath.savedGames:
1023                     return null;
1024             }
1025         }
1026     }
1027 
1028     string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe
1029     {
1030         const bool shouldCreate = (params & FolderFlag.create) != 0;
1031         const bool shouldVerify = (params & FolderFlag.verify) != 0;
1032         return writablePathImpl(type, shouldCreate).verifyIfNeeded(shouldVerify);
1033     }
1034 
1035     string[] standardPaths(StandardPath type) nothrow @safe
1036     {
1037         string commonPath;
1038 
1039         version(StandardPathsCocoa) {
1040             switch(type) {
1041                 case StandardPath.fonts:
1042                     commonPath = domainDir(NSLibraryDirectory, NSSystemDomainMask).maybeBuild("Fonts");
1043                     break;
1044                 case StandardPath.applications:
1045                     commonPath = domainDir(NSApplicationDirectory, NSSystemDomainMask);
1046                     break;
1047                 case StandardPath.data:
1048                     commonPath = domainDir(NSApplicationSupportDirectory, NSSystemDomainMask);
1049                     break;
1050                 case StandardPath.cache:
1051                     commonPath = domainDir(NSCachesDirectory, NSSystemDomainMask);
1052                     break;
1053                 default:
1054                     break;
1055             }
1056         } else {
1057             switch(type) {
1058                 case StandardPath.fonts:
1059                     commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType);
1060                     break;
1061                 case StandardPath.applications:
1062                     commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType);
1063                     break;
1064                 case StandardPath.data:
1065                     commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType);
1066                     break;
1067                 case StandardPath.cache:
1068                     commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType);
1069                     break;
1070                 default:
1071                     break;
1072             }
1073         }
1074 
1075         string[] paths;
1076         string userPath = writablePath(type);
1077         if (userPath.length)
1078             paths ~= userPath;
1079         if (commonPath.length)
1080             paths ~= commonPath;
1081         return paths;
1082     }
1083 
1084 } else {
1085 
1086     static if (!isFreedesktop) {
1087         static assert(false, "Unsupported platform");
1088     } else {
1089         public import xdgpaths;
1090 
1091         private {
1092             import std.stdio : File;
1093             import std.algorithm : startsWith;
1094             import std.string;
1095             import std.traits;
1096         }
1097 
1098         unittest
1099         {
1100             assert(maybeConcat(null, "path") == string.init);
1101             assert(maybeConcat("path", "/file") == "path/file");
1102         }
1103 
1104         private @trusted string getFromUserDirs(Range)(string xdgdir, string home, Range range) if (isInputRange!Range && isSomeString!(ElementType!Range))
1105         {
1106             foreach(line; range) {
1107                 line = strip(line);
1108                 auto index = xdgdir.length;
1109                 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') {
1110                     line = line[index+1..$];
1111                     if (line.length > 2 && line[0] == '"' && line[$-1] == '"')
1112                     {
1113                         line = line[1..$-1];
1114 
1115                         if (line.startsWith("$HOME")) {
1116                             return maybeConcat(home, assumeUnique(line[5..$]));
1117                         }
1118                         if (line.length == 0 || line[0] != '/') {
1119                             continue;
1120                         }
1121                         return assumeUnique(line);
1122                     }
1123                 }
1124             }
1125             return null;
1126         }
1127 
1128 
1129         unittest
1130         {
1131             string content =
1132 `# Comment
1133 
1134 XDG_DOCUMENTS_DIR="$HOME/My Documents"
1135 XDG_MUSIC_DIR="/data/Music"
1136 XDG_VIDEOS_DIR="data/Video"
1137 `;
1138             string home = "/home/user";
1139 
1140             assert(getFromUserDirs("XDG_DOCUMENTS_DIR", home, content.splitLines) == "/home/user/My Documents");
1141             assert(getFromUserDirs("XDG_MUSIC_DIR", home, content.splitLines) == "/data/Music");
1142             assert(getFromUserDirs("XDG_DOWNLOAD_DIR", home, content.splitLines).empty);
1143             assert(getFromUserDirs("XDG_VIDEOS_DIR", home, content.splitLines).empty);
1144         }
1145 
1146         private @trusted string getFromDefaultDirs(Range)(string key, string home, Range range) if (isInputRange!Range && isSomeString!(ElementType!Range))
1147         {
1148             foreach(line; range) {
1149                 line = strip(line);
1150                 auto index = key.length;
1151                 if (line.startsWith(key) && line.length > index && line[index] == '=')
1152                 {
1153                     line = line[index+1..$];
1154                     return home ~ "/" ~ assumeUnique(line);
1155                 }
1156             }
1157             return null;
1158         }
1159 
1160         unittest
1161         {
1162             string content =
1163 `# Comment
1164 
1165 DOCUMENTS=MyDocuments
1166 PICTURES=Images
1167 `;
1168             string home = "/home/user";
1169             assert(getFromDefaultDirs("DOCUMENTS", home, content.splitLines) == "/home/user/MyDocuments");
1170             assert(getFromDefaultDirs("PICTURES", home, content.splitLines) == "/home/user/Images");
1171             assert(getFromDefaultDirs("VIDEOS", home, content.splitLines).empty);
1172         }
1173 
1174         private string xdgUserDir(string key, string fallback = null) nothrow @trusted {
1175             string fileName = maybeConcat(writablePath(StandardPath.config), "/user-dirs.dirs");
1176             string home = homeDir();
1177             try {
1178                 auto f = File(fileName, "r");
1179                 auto xdgdir = "XDG_" ~ key ~ "_DIR";
1180                 auto path = getFromUserDirs(xdgdir, home, f.byLine());
1181                 if (path.length) {
1182                     return path;
1183                 }
1184             } catch(Exception e) {
1185 
1186             }
1187 
1188             if (home.length) {
1189                 try {
1190                     auto f = File("/etc/xdg/user-dirs.defaults", "r");
1191                     auto path = getFromDefaultDirs(key, home, f.byLine());
1192                     if (path.length) {
1193                         return path;
1194                     }
1195                 } catch (Exception e) {
1196 
1197                 }
1198                 if (fallback.length) {
1199                     return home ~ fallback;
1200                 }
1201             }
1202             return null;
1203         }
1204 
1205         private string homeFontsPath() nothrow @trusted {
1206             return maybeConcat(homeDir(), "/.fonts");
1207         }
1208 
1209         private string[] fontPaths() nothrow @trusted
1210         {
1211             enum localShare = "/usr/local/share/fonts";
1212             enum share = "/usr/share/fonts";
1213 
1214             string homeFonts = homeFontsPath();
1215             if (homeFonts.length) {
1216                 return [homeFonts, localShare, share];
1217             } else {
1218                 return [localShare, share];
1219             }
1220         }
1221 
1222         private string writablePathImpl(StandardPath type, bool shouldCreate) nothrow @safe
1223         {
1224             final switch(type) {
1225                 case StandardPath.config:
1226                     return xdgConfigHome(null, shouldCreate);
1227                 case StandardPath.cache:
1228                     return xdgCacheHome(null, shouldCreate);
1229                 case StandardPath.data:
1230                     return xdgDataHome(null, shouldCreate);
1231                 case StandardPath.desktop:
1232                     return xdgUserDir("DESKTOP", "/Desktop").createIfNeeded(shouldCreate);
1233                 case StandardPath.documents:
1234                     return xdgUserDir("DOCUMENTS").createIfNeeded(shouldCreate);
1235                 case StandardPath.pictures:
1236                     return xdgUserDir("PICTURES").createIfNeeded(shouldCreate);
1237                 case StandardPath.music:
1238                     return xdgUserDir("MUSIC").createIfNeeded(shouldCreate);
1239                 case StandardPath.videos:
1240                     return xdgUserDir("VIDEOS").createIfNeeded(shouldCreate);
1241                 case StandardPath.downloads:
1242                     return xdgUserDir("DOWNLOAD").createIfNeeded(shouldCreate);
1243                 case StandardPath.templates:
1244                     return xdgUserDir("TEMPLATES", "/Templates").createIfNeeded(shouldCreate);
1245                 case StandardPath.publicShare:
1246                     return xdgUserDir("PUBLICSHARE", "/Public").createIfNeeded(shouldCreate);
1247                 case StandardPath.fonts:
1248                     return homeFontsPath().createIfNeeded(shouldCreate);
1249                 case StandardPath.applications:
1250                     return xdgDataHome("applications", shouldCreate);
1251                 case StandardPath.startup:
1252                     return xdgConfigHome("autostart", shouldCreate);
1253                 case StandardPath.roaming:
1254                     return null;
1255                 case StandardPath.savedGames:
1256                     return null;
1257             }
1258         }
1259 
1260         string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe
1261         {
1262             const bool shouldCreate = (params & FolderFlag.create) != 0;
1263             const bool shouldVerify = (params & FolderFlag.verify) != 0;
1264             return writablePathImpl(type, shouldCreate).verifyIfNeeded(shouldVerify);
1265         }
1266 
1267         string[] standardPaths(StandardPath type) nothrow @safe
1268         {
1269             string[] paths;
1270 
1271             switch(type) {
1272                 case StandardPath.data:
1273                     return xdgAllDataDirs();
1274                 case StandardPath.config:
1275                     return xdgAllConfigDirs();
1276                 case StandardPath.applications:
1277                     return xdgAllDataDirs("applications");
1278                 case StandardPath.startup:
1279                     return xdgAllConfigDirs("autostart");
1280                 case StandardPath.fonts:
1281                     return fontPaths();
1282                 default:
1283                     break;
1284             }
1285 
1286             string userPath = writablePath(type);
1287             if (userPath.length) {
1288                 return [userPath];
1289             }
1290             return null;
1291         }
1292     }
1293 }