1 /**
2  * Functions for retrieving standard paths in cross-platform manner.
3  * 
4  */
5 
6 module standardpaths;
7 
8 private {
9     import std.process : environment;
10     import std.array;
11     import std.path;
12     import std.file;
13     import std.algorithm : splitter;
14 }
15 
16 version(Windows) {
17     private {
18         import std.c.windows.windows;
19         import std.utf;
20         import std.algorithm : canFind;
21     }
22 } else version(OSX) {
23     private {
24         //what to import?
25     }
26 } else version(Posix) {
27     private {
28         import std.stdio : File, StdioException;
29         import std.exception : assumeUnique;
30         import std.conv : octal;
31     }
32 } else {
33     static assert(false, "Unsupported platform");
34 }
35 
36 /** 
37  * Locations that can be passed to writablePath and standardPaths functions.
38  * See_Also:
39  *  writablePath, standardPaths
40  */
41 enum StandardPath {
42     Data,   ///Location of persisted application data
43     Config, ///Location of configuration files
44     Cache,  ///Location of cached data
45     Desktop, ///User's desktop directory
46     Documents, ///User's documents
47     Pictures, ///User's pictures
48     Music, ///User's music
49     Videos, ///User's videos (movies)
50     Download, ///Directory for user's downloaded files
51     Templates, ///Location of templates
52     PublicShare, ///Public share folder
53     Fonts, ///Location of fonts files
54     Applications, ///User's applications
55 }
56 
57 /**
58  * Returns: path to user home directory, or an empty string if could not determine home directory.
59  * Note: this function does not provide caching of its results.
60  */
61 string homeDir() nothrow
62 {
63     try { //environment.get may throw on Windows
64         version(Windows) {
65             //Use GetUserProfileDirectoryW from Userenv.dll?
66             string home = environment.get("USERPROFILE");
67             if (home.empty) {
68                 string homeDrive = environment.get("HOMEDRIVE");
69                 string homePath = environment.get("HOMEPATH");
70                 if (homeDrive.length && homePath.length) {
71                     home = homeDrive ~ homePath;
72                 }
73             }
74             return home;
75         } else {
76             string home = environment.get("HOME");
77             return home;
78         }
79     }
80     catch(Exception e) {
81         return null;
82     }
83 }
84 
85 /**
86  * Returns: path where files of $(U type) should be written to by current user, or an empty string if could not determine path.
87  * This function does not ensure if the returned path exists and appears to be accessible directory.
88  * Note: this function does not provide caching of its results.
89  */
90 string writablePath(StandardPath type);
91 
92 /**
93  * Returns: array of paths where file of $(U type) belong including one returned by $(B writablePath), or empty array if no paths for $(U type) are defined.
94  * This function does not ensure if all returned paths exist and appear to be accessible directories.
95  * Note: this function does not provide caching of its results. 
96  * It may cause performance impact to call this function often since retrieving some paths can be expensive operation.
97  * See_Also:
98  *  writablePath
99  */
100 string[] standardPaths(StandardPath type);
101 
102 version(Windows) {
103     private enum pathVarSeparator = ';';
104 } else version(Posix) {
105     private enum pathVarSeparator = ':';
106 }
107 
108 version(Windows) {
109     
110     private {
111         enum {
112             CSIDL_DESKTOP            =  0,
113             CSIDL_INTERNET,
114             CSIDL_PROGRAMS,
115             CSIDL_CONTROLS,
116             CSIDL_PRINTERS,
117             CSIDL_PERSONAL,
118             CSIDL_FAVORITES,
119             CSIDL_STARTUP,
120             CSIDL_RECENT,
121             CSIDL_SENDTO,
122             CSIDL_BITBUCKET,
123             CSIDL_STARTMENU,      // = 11
124             CSIDL_MYMUSIC            = 13,
125             CSIDL_MYVIDEO,        // = 14
126             CSIDL_DESKTOPDIRECTORY   = 16,
127             CSIDL_DRIVES,
128             CSIDL_NETWORK,
129             CSIDL_NETHOOD,
130             CSIDL_FONTS,
131             CSIDL_TEMPLATES,
132             CSIDL_COMMON_STARTMENU,
133             CSIDL_COMMON_PROGRAMS,
134             CSIDL_COMMON_STARTUP,
135             CSIDL_COMMON_DESKTOPDIRECTORY,
136             CSIDL_APPDATA,
137             CSIDL_PRINTHOOD,
138             CSIDL_LOCAL_APPDATA,
139             CSIDL_ALTSTARTUP,
140             CSIDL_COMMON_ALTSTARTUP,
141             CSIDL_COMMON_FAVORITES,
142             CSIDL_INTERNET_CACHE,
143             CSIDL_COOKIES,
144             CSIDL_HISTORY,
145             CSIDL_COMMON_APPDATA,
146             CSIDL_WINDOWS,
147             CSIDL_SYSTEM,
148             CSIDL_PROGRAM_FILES,
149             CSIDL_MYPICTURES,
150             CSIDL_PROFILE,
151             CSIDL_SYSTEMX86,
152             CSIDL_PROGRAM_FILESX86,
153             CSIDL_PROGRAM_FILES_COMMON,
154             CSIDL_PROGRAM_FILES_COMMONX86,
155             CSIDL_COMMON_TEMPLATES,
156             CSIDL_COMMON_DOCUMENTS,
157             CSIDL_COMMON_ADMINTOOLS,
158             CSIDL_ADMINTOOLS,
159             CSIDL_CONNECTIONS,  // = 49
160             CSIDL_COMMON_MUSIC     = 53,
161             CSIDL_COMMON_PICTURES,
162             CSIDL_COMMON_VIDEO,
163             CSIDL_RESOURCES,
164             CSIDL_RESOURCES_LOCALIZED,
165             CSIDL_COMMON_OEM_LINKS,
166             CSIDL_CDBURN_AREA,  // = 59
167             CSIDL_COMPUTERSNEARME  = 61,
168             CSIDL_FLAG_DONT_VERIFY = 0x4000,
169             CSIDL_FLAG_CREATE      = 0x8000,
170             CSIDL_FLAG_MASK        = 0xFF00
171         }
172     }
173     
174     private  {
175         alias GetSpecialFolderPath = extern(Windows) BOOL function (HWND, wchar*, int, BOOL);
176         
177         version(LinkedShell32) {
178             extern(Windows) BOOL SHGetSpecialFolderPathW(HWND, wchar*, int, BOOL);
179             GetSpecialFolderPath ptrSHGetSpecialFolderPath = &SHGetSpecialFolderPathW;
180         } else {
181             GetSpecialFolderPath ptrSHGetSpecialFolderPath = null;
182         }
183     }
184     
185     version(LinkedShell32) {} else {
186         shared static this() 
187         {
188             HMODULE lib = LoadLibraryA("Shell32");
189             if (lib) {
190                 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(lib, "SHGetSpecialFolderPathW");
191             }
192         }
193     }
194     
195     
196     private string getCSIDLFolder(wchar* path, int csidl)
197     {
198         import core.stdc.wchar_ : wcslen;
199         if (ptrSHGetSpecialFolderPath(null, path, csidl, FALSE)) {
200             size_t len = wcslen(path);
201             return toUTF8(path[0..len]);
202         }
203         return null;
204     }
205     
206     string writablePath(StandardPath type)
207     {
208         if (!ptrSHGetSpecialFolderPath) {
209             return null;
210         }
211         
212         wchar[MAX_PATH] buf;
213         wchar* path = buf.ptr;
214         
215         final switch(type) {
216             case StandardPath.Config:
217             case StandardPath.Data:
218                 return getCSIDLFolder(path, CSIDL_LOCAL_APPDATA);
219             case StandardPath.Cache:
220                 return buildPath(getCSIDLFolder(path, CSIDL_LOCAL_APPDATA), "cache");
221             case StandardPath.Desktop:
222                 return getCSIDLFolder(path, CSIDL_DESKTOPDIRECTORY);
223             case StandardPath.Documents:
224                 return getCSIDLFolder(path, CSIDL_PERSONAL);
225             case StandardPath.Pictures:
226                 return getCSIDLFolder(path, CSIDL_MYPICTURES);
227             case StandardPath.Music:
228                 return getCSIDLFolder(path, CSIDL_MYMUSIC);
229             case StandardPath.Videos:
230                 return getCSIDLFolder(path, CSIDL_MYVIDEO);
231             case StandardPath.Download:
232                 return null;
233             case StandardPath.Templates:
234                 return getCSIDLFolder(path, CSIDL_TEMPLATES);
235             case StandardPath.PublicShare:
236                 return null;
237             case StandardPath.Fonts:
238                 return null;
239             case StandardPath.Applications:
240                 return getCSIDLFolder(path, CSIDL_PROGRAMS);
241         }
242     }
243     
244     string[] standardPaths(StandardPath type)
245     {
246         if (!ptrSHGetSpecialFolderPath) {
247             return null;
248         }
249         
250         string commonPath;
251         wchar[MAX_PATH] buf;
252         wchar* path = buf.ptr;
253         
254         switch(type) {
255             case StandardPath.Config:
256             case StandardPath.Data:
257                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_APPDATA);
258                 break;
259             case StandardPath.Desktop:
260                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_DESKTOPDIRECTORY);
261                 break;
262             case StandardPath.Documents:
263                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_DOCUMENTS);
264                 break;
265             case StandardPath.Pictures:
266                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_PICTURES);
267                 break;
268             case StandardPath.Music:
269                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_MUSIC);
270                 break;
271             case StandardPath.Videos:
272                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_VIDEO);
273                 break;
274             case StandardPath.Templates:
275                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_TEMPLATES);
276                 break;
277             case StandardPath.Fonts:
278                 commonPath = getCSIDLFolder(path, CSIDL_FONTS);
279                 break;
280             case StandardPath.Applications:
281                 commonPath = getCSIDLFolder(path, CSIDL_COMMON_PROGRAMS);
282                 break;
283             default:
284                 break;
285         }
286         
287         string[] paths;
288         string userPath = writablePath(type);
289         if (userPath.length) 
290             paths ~= userPath;
291         if (commonPath.length)
292             paths ~= commonPath;
293         return paths;
294     }
295     
296     private string[] executableExtensions() 
297     {
298         static bool filenamesEqual(string first, string second) {
299             return filenameCmp(first, second) == 0;
300         }
301     
302         string[] extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array;
303         if (canFind!(filenamesEqual)(extensions, ".exe") == false) {
304             extensions = [".exe", ".com", ".bat", ".cmd"];
305         }
306         return extensions;
307     }
308 } else version(OSX) {
309     
310     string fsPath(short domain, OSType type) 
311     {
312         import std.string : fromStringz;
313         
314         FSRef fsref;
315         OSErr err = FSFindFolder(domain, type, false, &fsref);
316         if (err) {
317             return null;
318         } else {
319             ubyte[2048] buf;
320             ubyte* path = buf.ptr;
321             if (FSRefMakePath(&fsref, path, path.sizeof) == noErr) {
322                 const(char)* cpath = cast(const(char)*)path;
323                 return fromStringz(cpath).idup;
324             } else {
325                 return null;
326             }
327         }
328     }
329     
330     string writablePath(StandardPath type)
331     {
332         final switch(type) {
333             case StandardPath.Config:
334                 return fsPath(kUserDomain, kPreferencesFolderType);
335             case StandardPath.Cache:
336                 return fsPath(kUserDomain, kCachedDataFolderType);
337             case StandardPath.Data:
338                 return fsPath(kUserDomain, kApplicationSupportFolderType);
339             case StandardPath.Desktop:
340                 return fsPath(kUserDomain, kDesktopFolderType);
341             case StandardPath.Documents:
342                 return fsPath(kUserDomain, kDocumentsFolderType);
343             case StandardPath.Pictures:
344                 return fsPath(kUserDomain, kPictureDocumentsFolderType);
345             case StandardPath.Music:
346                 return fsPath(kUserDomain, kMusicDocumentsFolderType);
347             case StandardPath.Videos:
348                 return fsPath(kUserDomain, kMovieDocumentsFolderType);
349             case StandardPath.Download:
350                 return null;
351             case StandardPath.Templates:
352                 return null;
353             case StandardPath.PublicShare:
354                 return fsPath(kUserDomain, kPublicFolderType );
355             case StandardPath.Fonts:
356                 return fsPath(kUserDomain, kFontsFolderType);
357             case StandardPath.Applications:
358                 return fsPath(kUserDomain, kApplicationsFolderType);
359         }
360     }
361     
362     string[] standardPaths(StandardPath type)
363     {
364         string commonPath;
365         
366         switch(type) {
367             case StandardPath.Fonts:
368                 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType);
369             case StandardPath.Applications:
370                 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType);
371             case StandardPath.Data:
372                 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType);
373             case StandardPath.Cache:
374                 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType);
375         }
376         
377         string[] paths;
378         string userPath = writablePath(type);
379         if (userPath.length)
380             paths ~= userPath;
381         if (commonPath.length)
382             paths ~= commonPath;
383         return paths;
384     }
385     
386 } else {
387     
388     //Concat two paths, but if the first one is empty, then null string is returned.
389     private string homeConcat(string home, string path) nothrow
390     {
391         return home.empty ? null : home ~ path;
392     }
393     
394     private string xdgBaseDir(in char[] envvar, string fallback) {
395         string dir = environment.get(envvar);
396         if (!dir.length) {
397             dir = homeConcat(homeDir(), fallback);
398         }
399         return dir;
400     }
401     
402     private string xdgUserDir(in char[] key, string fallback) {
403         import std.algorithm : startsWith, countUntil;
404         
405         //Read /etc/xdg/user-dirs.defaults for fallbacks?
406         
407         string configDir = writablePath(StandardPath.Config);
408         string fileName = configDir ~ "/user-dirs.dirs";
409         string home = homeDir();
410         try {
411             auto f = File(fileName, "r");
412             
413             auto xdgdir = "XDG_" ~ key ~ "_DIR";
414             
415             char[] buf;
416             while(f.readln(buf)) {
417                 char[] line = buf[0..$-1]; //remove line terminator
418                 if (line.startsWith(xdgdir)) {
419                     ptrdiff_t index = line.countUntil('=');
420                     if (index != -1) {
421                         line = line[index+1..$];
422                         if (line.length > 2 && 
423                             line[0] == '"' && 
424                             line[$-1] == '"') {
425                             line = line[1..$-1];
426                         }
427                         
428                         if (line.startsWith("$HOME")) {
429                             return homeConcat(home, assumeUnique(line[5..$]));
430                         }
431                         if (line.length == 0 || line[0] != '/') {
432                             continue;
433                         }
434                         return assumeUnique(line);
435                     }
436                 }
437             }
438         }
439         catch(Exception e) {
440             
441         }
442         if (fallback.length)
443             return homeConcat(home, fallback);
444         return null;
445     }
446     
447     private string[] xdgConfigDirs() {
448         string configDirs = environment.get("XDG_CONFIG_DIRS");
449         if (configDirs.length) {
450             return splitter(configDirs, pathVarSeparator).array;
451         } else {
452             return ["/etc/xdg"];
453         }
454     }
455     
456     private string[] xdgDataDirs() {
457         string dataDirs = environment.get("XDG_DATA_DIRS");
458         if (dataDirs.length) {
459             return splitter(dataDirs, pathVarSeparator).array;
460         } else {
461             return ["/usr/local/share/", "/usr/share/"];
462         }
463     }
464     
465     private string[] readFontsConfig(string configFile) nothrow
466     {
467         //Should be changed in future since std.xml is deprecated
468         import std.xml;
469         
470         string[] paths;
471         try {
472             string contents = cast(string)read(configFile);
473             check(contents);
474             auto parser = new DocumentParser(contents);
475             parser.onEndTag["dir"] = (in Element xml)
476             {
477                 string path = xml.text;
478                 
479                 if (path.length && path[0] == '~') {
480                     path = homeConcat(homeDir(), path[1..$]);
481                 } else {
482                     const(string)* prefix = "prefix" in xml.tag.attr;
483                     if (prefix && *prefix == "xdg") {
484                         string dataPath = writablePath(StandardPath.Data);
485                         if (dataPath.length) {
486                             path = buildPath(dataPath, path);
487                         }
488                     }
489                 }
490                 if (path.length) {
491                     paths ~= path;
492                 }
493             };
494             parser.parse();
495         }
496         catch(Exception e) {
497             
498         }
499         return paths;
500     }
501     
502     private string[] fontPaths() nothrow
503     {
504         
505         string[] paths;
506         string[] configs = [homeConcat(homeDir(), "/.fonts"), "/etc/fonts/fonts.conf", "/usr/local/etc/fonts/fonts.conf"];
507         
508         foreach(config; configs) {
509             paths ~= readFontsConfig(config);
510         }
511         return paths;
512     }
513     
514     /**
515      * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 
516      * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string.
517      */
518     string runtimeDir() 
519     {
520         // Do we need it on BSD systems?
521         
522         import core.sys.posix.pwd;
523         import core.sys.posix.unistd;
524         import core.sys.posix.sys.stat;
525         import core.sys.posix.sys.types;
526         import core.stdc.errno;
527         import core.stdc.string;
528         
529         import std.string : fromStringz, toStringz;
530         import std.stdio : stderr;
531         
532         const uid_t uid = getuid();
533         string runtime = environment.get("XDG_RUNTIME_DIR");
534         
535         mode_t runtimeMode = octal!700;
536         
537         if (!runtime.length) {
538             setpwent();
539             passwd* pw = getpwuid(uid);
540             endpwent();
541             
542             if (pw && pw.pw_name) {
543                 runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
544                 
545                 if (!(runtime.exists && runtime.isDir)) {
546                     if (mkdir(runtime.toStringz, runtimeMode) != 0) {
547                         stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
548                         return null;
549                     }
550                 }
551             } else {
552                 stderr.writefln("Failed to get user name to create runtime directory");
553                 return null;
554             }
555         }
556         stat_t statbuf;
557         stat(runtime.toStringz, &statbuf);
558         if (statbuf.st_uid != uid) {
559             stderr.writefln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid);
560             return null;
561         }
562         if ((statbuf.st_mode & octal!777) != runtimeMode) {
563             stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode);
564             return null;
565         }
566         
567         return runtime;
568     }
569     
570     string writablePath(StandardPath type)
571     {
572         final switch(type) {
573             case StandardPath.Config:
574                 return xdgBaseDir("XDG_CONFIG_HOME", "/.config");
575             case StandardPath.Cache:
576                 return xdgBaseDir("XDG_CACHE_HOME", "/.cache");
577             case StandardPath.Data:
578                 return xdgBaseDir("XDG_DATA_HOME", "/.local/share");
579             case StandardPath.Desktop:
580                 return xdgUserDir("DESKTOP", "/Desktop");
581             case StandardPath.Documents:
582                 return xdgUserDir("DOCUMENTS", "/Documents");
583             case StandardPath.Pictures:
584                 return xdgUserDir("PICTURES", "/Pictures");
585             case StandardPath.Music:
586                 return xdgUserDir("MUSIC", "/Music");
587             case StandardPath.Videos:
588                 return xdgUserDir("VIDEOS", "/Videos");
589             case StandardPath.Download:
590                 return xdgUserDir("DOWNLOAD", "/Downloads");
591             case StandardPath.Templates:
592                 return xdgUserDir("TEMPLATES", "/Templates");
593             case StandardPath.PublicShare:
594                 return xdgUserDir("PUBLICSHARE", "/Public");
595             case StandardPath.Fonts:
596                 return homeConcat(homeDir(), "/.fonts");
597                 
598             case StandardPath.Applications:
599                 return null;
600         }
601     }
602     
603     string[] standardPaths(StandardPath type)
604     {
605         string[] paths;
606         
607         if (type == StandardPath.Data) {
608             paths = xdgDataDirs();
609         } else if (type == StandardPath.Config) {
610             paths = xdgConfigDirs();
611         } else if (type == StandardPath.Fonts) {
612             return fontPaths();
613         }
614         
615         string userPath = writablePath(type);
616         if (userPath.length) {
617             paths = userPath ~ paths;
618         }
619         return paths;
620     }
621 }
622 
623 private bool isExecutable(string filePath) {
624     version(Posix) {
625         return (getAttributes(filePath) & octal!100) != 0;
626     } else version(Windows) {
627         //Use GetEffectiveRightsFromAclW?
628         
629         const(string)[] exeExtensions = executableExtensions();
630         foreach(ext; exeExtensions) {
631             if (filePath.extension == ext)
632                 return true;
633         }
634         return false;
635         
636     } else {
637         static assert(false, "Unsupported platform");
638     }
639 }
640 
641 private string checkExecutable(string filePath) {
642     try {
643         if (filePath.isFile && filePath.isExecutable) {
644             return buildNormalizedPath(filePath);
645         } else {
646             return null;
647         }
648     }
649     catch(FileException e) {
650         return null;
651     }
652 }
653 
654 /**
655  * Finds executable by $(B fileName) in the paths specified by $(B paths).
656  * Returns: absolute path to the existing executable file or an empty string if not found.
657  * Params:
658  *  fileName = name of executable to search
659  *  paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable
660  * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search.
661  */
662 string findExecutable(string fileName, in string[] paths = [])
663 {
664     if (fileName.isAbsolute()) {
665         return checkExecutable(fileName);
666     }
667     
668     const(string)[] searchPaths = paths;
669     if (searchPaths.empty) {
670         string pathVar = environment.get("PATH");
671         if (pathVar.length) {
672             searchPaths = splitter(pathVar, pathVarSeparator).array;
673         }
674     }
675     
676     if (searchPaths.empty) {
677         return null;
678     }
679     
680     string toReturn;
681     foreach(string path; searchPaths) {
682         string candidate = buildPath(absolutePath(path), fileName);
683         
684         version(Windows) {
685             if (candidate.extension.empty) {
686                 foreach(exeExtension; executableExtensions()) {
687                     toReturn = checkExecutable(setExtension(candidate, exeExtension));
688                     if (toReturn.length) {
689                         return toReturn;
690                     }
691                 }
692             }
693         }
694         
695         toReturn = checkExecutable(candidate);
696         if (toReturn.length) {
697             return toReturn;
698         }
699     }
700     return null;
701 }
702 
703