1 /**
2  * Getting XDG base directories.
3  * Note: these functions are defined only on freedesktop systems.
4  * Authors: 
5  *  $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov)
6  * Copyright:
7  *  Roman Chistokhodov, 2016
8  * License: 
9  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
10  * See_Also:
11  *  $(LINK2 https://specifications.freedesktop.org/basedir-spec/latest/index.html, XDG Base Directory Specification)
12  */
13 
14 module xdgpaths;
15 
16 import isfreedesktop;
17 
18 version(XdgPathsDocs)
19 {
20     /**
21      * Path to runtime user directory.
22      * Returns: User's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 
23      * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string.
24      */
25     @trusted string xdgRuntimeDir() nothrow;
26     
27     /**
28      * The ordered set of non-empty base paths to search for data files, in descending order of preference.
29      * Params:
30      *  subfolder = Subfolder which is appended to every path if not null.
31      * Returns: Data directories, without user's one and with no duplicates.
32      * Note: This function does not check if paths actually exist and appear to be directories.
33      */
34     @trusted string[] xdgDataDirs(string subfolder = null) nothrow;
35     
36     /**
37      * The ordered set of non-empty base paths to search for data files, in descending order of preference.
38      * Params:
39      *  subfolder = Subfolder which is appended to every path if not null.
40      * Returns: data directories, including user's one if could be evaluated.
41      * Note: This function does not check if paths actually exist and appear to be directories.
42      */
43     @trusted string[] xdgAllDataDirs(string subfolder = null) nothrow;
44     
45     /**
46      * The ordered set of non-empty base paths to search for configuration files, in descending order of preference.
47      * Params:
48      *  subfolder = Subfolder which is appended to every path if not null.
49      * Returns: Config directories, without user's one and with no duplicates.
50      * Note: This function does not check if paths actually exist and appear to be directories.
51      */
52     @trusted string[] xdgConfigDirs(string subfolder = null) nothrow;
53     
54     /**
55      * The ordered set of non-empty base paths to search for configuration files, in descending order of preference.
56      * Params:
57      *  subfolder = Subfolder which is appended to every path if not null.
58      * Returns: config directories, including user's one if could be evaluated.
59      * Note: This function does not check if paths actually exist and appear to be directories.
60      */
61     @trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow;
62     
63     /**
64      * The base directory relative to which user-specific data files should be stored.
65      * Returns: Path to user-specific data directory or empty string if could not be evaluated.
66      * Note: This function does not check if returned path actually exists and appears to be directory.
67      * If such directory does not exist, it's recommended to create it using 0700 permissions restricting any access to user data for anyone else.
68      */
69     @trusted string xdgDataHome(string subfolder = null) nothrow;
70     
71     /**
72      * The base directory relative to which user-specific configuration files should be stored.
73      * Returns: Path to user-specific configuration directory or empty string if could not be evaluated.
74      * Note: This function does not check if returned path actually exists and appears to be directory.
75      * If such directory does not exist, it's recommended to create it using 0700 permissions restricting any access to user preferences for anyone else.
76      */
77     @trusted string xdgConfigHome(string subfolder = null) nothrow;
78     
79     /**
80      * The base directory relative to which user-specific non-essential files should be stored.
81      * Returns: Path to user-specific cache directory or empty string if could not be evaluated.
82      * Note: This function does not check if returned path actually exists and appears to be directory.
83      */
84     @trusted string xdgCacheHome(string subfolder = null) nothrow {
85         return xdgBaseDir("XDG_CACHE_HOME", ".cache", subfolder);
86     }
87 }
88 
89 static if (isFreedesktop) 
90 {
91     private {
92         import std.algorithm : splitter, map, filter, canFind;
93         import std.array;
94         import std.path : buildPath;
95         import std.process : environment;
96         import std.exception : collectException;
97     }
98     
99     version(unittest) {
100         import std.stdio;
101         import std.algorithm : joiner, equal;
102         
103         struct EnvGuard
104         {
105             this(string env) {
106                 envVar = env;
107                 envValue = environment.get(env);
108             }
109             
110             ~this() {
111                 if (envValue is null) {
112                     environment.remove(envVar);
113                 } else {
114                     environment[envVar] = envValue;
115                 }
116             }
117             
118             string envVar;
119             string envValue;
120         }
121     }
122     
123     private string[] pathsFromEnvValue(string envValue, string subfolder = null) nothrow {
124         string[] result;
125         try {
126             foreach(path; splitter(envValue, ':').filter!(p => !p.empty).map!(p => buildPath(p, subfolder))) {
127                 if (!result.canFind(path)) {
128                     result ~= path;
129                 }
130             }
131         } catch(Exception e) {
132             
133         }
134         return result;
135     }
136     
137     unittest
138     {
139         assert(pathsFromEnvValue("") == (string[]).init);
140         assert(pathsFromEnvValue(":") == (string[]).init);
141         assert(pathsFromEnvValue("::") == (string[]).init);
142         
143         assert(pathsFromEnvValue("path1:path2") == ["path1", "path2"]);
144         assert(pathsFromEnvValue("path1:") == ["path1"]);
145         assert(pathsFromEnvValue("path2:path1:path2") == ["path2", "path1"]);
146     }
147     
148     private string[] pathsFromEnv(string envVar, string subfolder = null) nothrow {   
149         string envValue;
150         collectException(environment.get(envVar), envValue);
151         return pathsFromEnvValue(envValue, subfolder);
152     }
153 
154     private string xdgBaseDir(string envvar, string fallback, string subfolder = null) nothrow {
155         string dir;
156         collectException(environment.get(envvar), dir);
157         if (dir.length) {
158             return buildPath(dir, subfolder);
159         } else {
160             string home;
161             collectException(environment.get("HOME"), home);
162             return home.length ? buildPath(home, fallback, subfolder) : null;
163         }
164     }
165     
166     version(unittest) {
167         void testXdgBaseDir(string envVar, string fallback) {
168             auto homeGuard = EnvGuard("HOME");
169             auto dataHomeGuard = EnvGuard(envVar);
170             
171             auto newHome = "/home/myuser";
172             auto newDataHome = "/home/myuser/data";
173             
174             environment[envVar] = newDataHome;
175             assert(xdgBaseDir(envVar, fallback) == newDataHome);
176             assert(xdgBaseDir(envVar, fallback, "applications") == buildPath(newDataHome, "applications"));
177             
178             environment.remove(envVar);
179             environment["HOME"] = newHome;
180             assert(xdgBaseDir(envVar, fallback) == buildPath(newHome, fallback));
181             assert(xdgBaseDir(envVar, fallback, "icons") == buildPath(newHome, fallback, "icons"));
182             
183             environment.remove("HOME");
184             assert(xdgBaseDir(envVar, fallback).empty);
185             assert(xdgBaseDir(envVar, fallback, "mime").empty);
186         }
187     }
188     
189     @trusted string[] xdgDataDirs(string subfolder = null) nothrow
190     {
191         auto result = pathsFromEnv("XDG_DATA_DIRS", subfolder);
192         if (result.length) {
193             return result;
194         } else {
195             return [buildPath("/usr/local/share", subfolder), buildPath("/usr/share", subfolder)];
196         }
197     }
198     
199     unittest
200     {
201         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
202         
203         auto newDataDirs = ["/usr/local/data", "/usr/data"];
204         
205         environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data";
206         assert(xdgDataDirs() == newDataDirs);
207         assert(equal(xdgDataDirs("applications"), newDataDirs.map!(p => buildPath(p, "applications"))));
208         
209         environment.remove("XDG_DATA_DIRS");
210         assert(xdgDataDirs() == ["/usr/local/share", "/usr/share"]);
211         assert(equal(xdgDataDirs("icons"), ["/usr/local/share", "/usr/share"].map!(p => buildPath(p, "icons"))));
212     }
213     
214     @trusted string[] xdgAllDataDirs(string subfolder = null) nothrow
215     {
216         string dataHome = xdgDataHome(subfolder);
217         string[] dataDirs = xdgDataDirs(subfolder);
218         if (dataHome.length) {
219             return dataHome ~ dataDirs;
220         } else {
221             return dataDirs;
222         }
223     }
224     
225     unittest
226     {
227         auto homeGuard = EnvGuard("HOME");
228         auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
229         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
230         
231         auto newDataHome = "/home/myuser/data";
232         auto newDataDirs = ["/usr/local/data", "/usr/data"];
233         environment["XDG_DATA_HOME"] = newDataHome;
234         environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data";
235         
236         assert(xdgAllDataDirs() == newDataHome ~ newDataDirs);
237         
238         environment.remove("XDG_DATA_HOME");
239         environment.remove("HOME");
240         
241         assert(xdgAllDataDirs() == newDataDirs);
242     }
243     
244     @trusted string[] xdgConfigDirs(string subfolder = null) nothrow
245     {
246         auto result = pathsFromEnv("XDG_CONFIG_DIRS", subfolder);
247         if (result.length) {
248             return result;
249         } else {
250             return [buildPath("/etc/xdg", subfolder)];
251         }
252     }
253     
254     unittest
255     {
256         auto dataConfigGuard = EnvGuard("XDG_CONFIG_DIRS");
257         
258         auto newConfigDirs = ["/usr/local/config", "/usr/config"];
259         
260         environment["XDG_CONFIG_DIRS"] = "/usr/local/config:/usr/config";
261         assert(xdgConfigDirs() == newConfigDirs);
262         assert(equal(xdgConfigDirs("menus"), newConfigDirs.map!(p => buildPath(p, "menus"))));
263         
264         environment.remove("XDG_CONFIG_DIRS");
265         assert(xdgConfigDirs() == ["/etc/xdg"]);
266         assert(equal(xdgConfigDirs("autostart"), ["/etc/xdg"].map!(p => buildPath(p, "autostart"))));
267     }
268     
269     @trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow
270     {
271         string configHome = xdgConfigHome(subfolder);
272         string[] configDirs = xdgConfigDirs(subfolder);
273         if (configHome.length) {
274             return configHome ~ configDirs;
275         } else {
276             return configDirs;
277         }
278     }
279     
280     unittest
281     {
282         auto homeGuard = EnvGuard("HOME");
283         auto configHomeGuard = EnvGuard("XDG_CONFIG_HOME");
284         auto configDirsGuard = EnvGuard("XDG_CONFIG_DIRS");
285         
286         auto newConfigHome = "/home/myuser/data";
287         environment["XDG_CONFIG_HOME"] = newConfigHome;
288         auto newConfigDirs = ["/usr/local/data", "/usr/data"];
289         environment["XDG_CONFIG_DIRS"] = "/usr/local/data:/usr/data";
290         
291         assert(xdgAllConfigDirs() == newConfigHome ~ newConfigDirs);
292         
293         environment.remove("XDG_CONFIG_HOME");
294         environment.remove("HOME");
295         
296         assert(xdgAllConfigDirs() == newConfigDirs);
297     }
298     
299     @trusted string xdgDataHome(string subfolder = null) nothrow {
300         return xdgBaseDir("XDG_DATA_HOME", ".local/share", subfolder);
301     }
302     
303     unittest
304     {
305         testXdgBaseDir("XDG_DATA_HOME", ".local/share");
306     }
307     
308     @trusted string xdgConfigHome(string subfolder = null) nothrow {
309         return xdgBaseDir("XDG_CONFIG_HOME", ".config", subfolder);
310     }
311     
312     unittest
313     {
314         testXdgBaseDir("XDG_CONFIG_HOME", ".config");
315     }
316     
317     @trusted string xdgCacheHome(string subfolder = null) nothrow {
318         return xdgBaseDir("XDG_CACHE_HOME", ".cache", subfolder);
319     }
320     
321     unittest
322     {
323         testXdgBaseDir("XDG_CACHE_HOME", ".cache");
324     }
325     
326     private {
327         import std.conv : octal;
328         import std.string : toStringz;
329         import std.file : isDir, exists, tempDir;
330         import std.stdio;
331         
332         import core.sys.posix.unistd;
333         import core.sys.posix.sys.stat;
334         import core.sys.posix.sys.types;
335         import core.stdc.string;
336         import core.stdc.errno;
337         
338         static if (is(typeof({import std.string : fromStringz;}))) {
339             import std.string : fromStringz;
340         } else { //own fromStringz implementation for compatibility reasons
341             @system static pure inout(char)[] fromStringz(inout(char)* cString) {
342                 return cString ? cString[0..strlen(cString)] : null;
343             }
344         }
345     }
346     
347     private enum mode_t runtimeMode = octal!700;
348     
349     @trusted string xdgRuntimeDir() nothrow // Do we need it on BSD systems?
350     {   
351         import std.exception : assumeUnique;
352         import core.sys.posix.pwd;
353         
354         try { //one try to rule them all and for compatibility reasons
355             const uid_t uid = getuid();
356             string runtime;
357             collectException(environment.get("XDG_RUNTIME_DIR"), runtime);
358             
359             if (!runtime.length) {
360                 setpwent();
361                 passwd* pw = getpwuid(uid);
362                 endpwent();
363                 
364                 try {
365                     if (pw && pw.pw_name) {
366                         runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
367                         
368                         if (!(runtime.exists && runtime.isDir)) {
369                             if (mkdir(runtime.toStringz, runtimeMode) != 0) {
370                                 version(XdgPathsRuntimeDebug) stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
371                                 return null;
372                             }
373                         }
374                     } else {
375                         version(XdgPathsRuntimeDebug) stderr.writeln("Failed to get user name to create runtime directory");
376                         return null;
377                     }
378                 } catch(Exception e) {
379                     version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Error when creating runtime directory: %s", e.msg));
380                     return null;
381                 }
382             }
383             stat_t statbuf;
384             stat(runtime.toStringz, &statbuf);
385             if (statbuf.st_uid != uid) {
386                 version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid));
387                 return null;
388             }
389             if ((statbuf.st_mode & octal!777) != runtimeMode) {
390                 version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode));
391                 return null;
392             }
393             
394             return runtime;
395         } catch (Exception e) {
396             version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Error when getting runtime directory: %s", e.msg));
397             return null;
398         }
399     }
400     
401     unittest
402     {
403         string runtimePath = buildPath(tempDir(), "xdgpaths-runtime-test");
404         try {
405             collectException(std.file.rmdir(runtimePath));
406             
407             if (mkdir(runtimePath.toStringz, runtimeMode) == 0) {
408                 auto runtimeGuard = EnvGuard("XDG_RUNTIME_DIR");
409                 environment["XDG_RUNTIME_DIR"] = runtimePath;
410                 assert(xdgRuntimeDir() == runtimePath);
411                 
412                 if (chmod(runtimePath.toStringz, octal!777) == 0) {
413                     assert(xdgRuntimeDir() == string.init);
414                 }
415                 
416                 std.file.rmdir(runtimePath);
417             } else {
418                 version(XdgPathsRuntimeDebug) stderr.writeln(fromStringz(strerror(errno)));
419             }
420         } catch(Exception e) {
421             version(XdgPathsRuntimeDebug) stderr.writeln(e.msg);
422         }
423     }
424 }