1 /**
2  * Getting XDG base directories.
3  * Note: These functions are defined only on freedesktop systems.
4  * Authors:
5  *  $(LINK2 https://github.com/FreeSlave, 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(D_Ddoc)
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      * See_Also: $(D xdgAllDataDirs), $(D xdgDataHome)
34      */
35     @trusted string[] xdgDataDirs(string subfolder = null) nothrow;
36 
37     /**
38      * The ordered set of non-empty base paths to search for data files, in descending order of preference.
39      * Params:
40      *  subfolder = Subfolder which is appended to every path if not null.
41      * Returns: Data directories, including user's one if could be evaluated.
42      * Note: This function does not check if paths actually exist and appear to be directories.
43      * See_Also: $(D xdgDataDirs), $(D xdgDataHome)
44      */
45     @trusted string[] xdgAllDataDirs(string subfolder = null) nothrow;
46 
47     /**
48      * The ordered set of non-empty base paths to search for configuration files, in descending order of preference.
49      * Params:
50      *  subfolder = Subfolder which is appended to every path if not null.
51      * Returns: Config directories, without user's one and with no duplicates.
52      * Note: This function does not check if paths actually exist and appear to be directories.
53      * See_Also: $(D xdgAllConfigDirs), $(D xdgConfigHome)
54      */
55     @trusted string[] xdgConfigDirs(string subfolder = null) nothrow;
56 
57     /**
58      * The ordered set of non-empty base paths to search for configuration files, in descending order of preference.
59      * Params:
60      *  subfolder = Subfolder which is appended to every path if not null.
61      * Returns: Config directories, including user's one if could be evaluated.
62      * Note: This function does not check if paths actually exist and appear to be directories.
63      * See_Also: $(D xdgConfigDirs), $(D xdgConfigHome)
64      */
65     @trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow;
66 
67     /**
68      * The base directory relative to which user-specific data files should be stored.
69      * Returns: Path to user-specific data directory or empty string on error.
70      * Params:
71      *  subfolder = Subfolder to append to determined path.
72      *  shouldCreate = If path does not exist, create directory using 700 permissions (i.e. allow access only for current user).
73      * See_Also: $(D xdgAllDataDirs), $(D xdgDataDirs)
74      */
75     @trusted string xdgDataHome(string subfolder = null, bool shouldCreate = false) nothrow;
76 
77     /**
78      * The base directory relative to which user-specific configuration files should be stored.
79      * Returns: Path to user-specific configuration directory or empty string on error.
80      * Params:
81      *  subfolder = Subfolder to append to determined path.
82      *  shouldCreate = If path does not exist, create directory using 700 permissions (i.e. allow access only for current user).
83      * See_Also: $(D xdgAllConfigDirs), $(D xdgConfigDirs)
84      */
85     @trusted string xdgConfigHome(string subfolder = null, bool shouldCreate = false) nothrow;
86 
87     /**
88      * The base directory relative to which user-specific non-essential files should be stored.
89      * Returns: Path to user-specific cache directory or empty string on error.
90      * Params:
91      *  subfolder = Subfolder to append to determined path.
92      *  shouldCreate = If path does not exist, create directory using 700 permissions (i.e. allow access only for current user).
93      */
94     @trusted string xdgCacheHome(string subfolder = null, bool shouldCreate = false) nothrow;
95 }
96 
97 static if (isFreedesktop)
98 {
99     private {
100         import std.algorithm : splitter, map, filter, canFind;
101         import std.array;
102         import std.conv : octal;
103         import std.exception : collectException, enforce;
104         import std.file;
105         import std.path : buildPath, dirName;
106         import std.process : environment;
107         import std.string : toStringz;
108 
109         import core.sys.posix.unistd;
110         import core.sys.posix.sys.stat;
111         import core.sys.posix.sys.types;
112         import core.stdc.string;
113         import core.stdc.errno;
114 
115         static if (is(typeof({import std.string : fromStringz;}))) {
116             import std.string : fromStringz;
117         } else { //own fromStringz implementation for compatibility reasons
118             @system static pure inout(char)[] fromStringz(inout(char)* cString) {
119                 return cString ? cString[0..strlen(cString)] : null;
120             }
121         }
122 
123         enum mode_t privateMode = octal!700;
124     }
125 
126     version(unittest) {
127         import std.algorithm : equal;
128 
129         private struct EnvGuard
130         {
131             this(string env) {
132                 envVar = env;
133                 envValue = environment.get(env);
134             }
135 
136             ~this() {
137                 if (envValue is null) {
138                     environment.remove(envVar);
139                 } else {
140                     environment[envVar] = envValue;
141                 }
142             }
143 
144             string envVar;
145             string envValue;
146         }
147     }
148 
149     private string[] pathsFromEnvValue(string envValue, string subfolder = null) nothrow {
150         string[] result;
151         try {
152             foreach(path; splitter(envValue, ':').filter!(p => !p.empty).map!(p => buildPath(p, subfolder))) {
153                 if (path[$-1] == '/') {
154                     path = path[0..$-1];
155                 }
156                 if (!result.canFind(path)) {
157                     result ~= path;
158                 }
159             }
160         } catch(Exception e) {
161 
162         }
163         return result;
164     }
165 
166     unittest
167     {
168         assert(pathsFromEnvValue("") == (string[]).init);
169         assert(pathsFromEnvValue(":") == (string[]).init);
170         assert(pathsFromEnvValue("::") == (string[]).init);
171 
172         assert(pathsFromEnvValue("path1:path2") == ["path1", "path2"]);
173         assert(pathsFromEnvValue("path1:") == ["path1"]);
174         assert(pathsFromEnvValue("path1/") == ["path1"]);
175         assert(pathsFromEnvValue("path1/:path1") == ["path1"]);
176         assert(pathsFromEnvValue("path2:path1:path2") == ["path2", "path1"]);
177     }
178 
179     private string[] pathsFromEnv(string envVar, string subfolder = null) nothrow {
180         string envValue;
181         collectException(environment.get(envVar), envValue);
182         return pathsFromEnvValue(envValue, subfolder);
183     }
184 
185     private bool ensureExists(string dir) nothrow
186     {
187         bool ok;
188         try {
189             ok = dir.exists;
190             if (!ok) {
191                 mkdirRecurse(dir.dirName);
192                 ok = mkdir(dir.toStringz, privateMode) == 0;
193             } else {
194                 ok = dir.isDir;
195             }
196         } catch(Exception e) {
197             ok = false;
198         }
199         return ok;
200     }
201 
202     unittest
203     {
204         import std.file;
205         import std.stdio;
206 
207         string temp = tempDir();
208         if (temp.length) {
209             string testDir = buildPath(temp, "xdgpaths-unittest-tempdir");
210             string testFile = buildPath(testDir, "touched");
211             string testSubDir = buildPath(testDir, "subdir");
212             try {
213                 mkdir(testDir);
214                 File(testFile, "w");
215                 assert(!ensureExists(testFile));
216                 enforce(ensureExists(testSubDir));
217             } catch(Exception e) {
218 
219             } finally {
220                 collectException(rmdir(testSubDir));
221                 collectException(remove(testFile));
222                 collectException(rmdir(testDir));
223             }
224         }
225     }
226 
227     private string xdgBaseDir(string envvar, string fallback, string subfolder = null, bool shouldCreate = false) nothrow {
228         string dir;
229         collectException(environment.get(envvar), dir);
230         if (dir.length == 0) {
231             string home;
232             collectException(environment.get("HOME"), home);
233             dir = home.length ? buildPath(home, fallback) : null;
234         }
235 
236         if (dir.length == 0) {
237             return null;
238         }
239 
240         if (shouldCreate) {
241             if (ensureExists(dir)) {
242                 if (subfolder.length) {
243                     string path = buildPath(dir, subfolder);
244                     try {
245                         if (!path.exists) {
246                             mkdirRecurse(path);
247                         }
248                         return path;
249                     } catch(Exception e) {
250 
251                     }
252                 } else {
253                     return dir;
254                 }
255             }
256         } else {
257             return buildPath(dir, subfolder);
258         }
259         return null;
260     }
261 
262     version(unittest) {
263         void testXdgBaseDir(string envVar, string fallback) {
264             auto homeGuard = EnvGuard("HOME");
265             auto dataHomeGuard = EnvGuard(envVar);
266 
267             auto newHome = "/home/myuser";
268             auto newDataHome = "/home/myuser/data";
269 
270             environment[envVar] = newDataHome;
271             assert(xdgBaseDir(envVar, fallback) == newDataHome);
272             assert(xdgBaseDir(envVar, fallback, "applications") == buildPath(newDataHome, "applications"));
273 
274             environment.remove(envVar);
275             environment["HOME"] = newHome;
276             assert(xdgBaseDir(envVar, fallback) == buildPath(newHome, fallback));
277             assert(xdgBaseDir(envVar, fallback, "icons") == buildPath(newHome, fallback, "icons"));
278 
279             environment.remove("HOME");
280             assert(xdgBaseDir(envVar, fallback).empty);
281             assert(xdgBaseDir(envVar, fallback, "mime").empty);
282         }
283     }
284 
285     @trusted string[] xdgDataDirs(string subfolder = null) nothrow
286     {
287         auto result = pathsFromEnv("XDG_DATA_DIRS", subfolder);
288         if (result.length) {
289             return result;
290         } else {
291             return [buildPath("/usr/local/share", subfolder), buildPath("/usr/share", subfolder)];
292         }
293     }
294 
295     ///
296     unittest
297     {
298         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
299 
300         auto newDataDirs = ["/usr/local/data", "/usr/data"];
301 
302         environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data:/usr/local/data/:/usr/data/";
303         assert(xdgDataDirs() == newDataDirs);
304         assert(equal(xdgDataDirs("applications"), newDataDirs.map!(p => buildPath(p, "applications"))));
305 
306         environment.remove("XDG_DATA_DIRS");
307         assert(xdgDataDirs() == ["/usr/local/share", "/usr/share"]);
308         assert(equal(xdgDataDirs("icons"), ["/usr/local/share", "/usr/share"].map!(p => buildPath(p, "icons"))));
309     }
310 
311     @trusted string[] xdgAllDataDirs(string subfolder = null) nothrow
312     {
313         string dataHome = xdgDataHome(subfolder);
314         string[] dataDirs = xdgDataDirs(subfolder);
315         if (dataHome.length) {
316             return dataHome ~ dataDirs;
317         } else {
318             return dataDirs;
319         }
320     }
321 
322     ///
323     unittest
324     {
325         auto homeGuard = EnvGuard("HOME");
326         auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
327         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
328 
329         auto newDataHome = "/home/myuser/data";
330         auto newDataDirs = ["/usr/local/data", "/usr/data"];
331         environment["XDG_DATA_HOME"] = newDataHome;
332         environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data";
333 
334         assert(xdgAllDataDirs() == newDataHome ~ newDataDirs);
335 
336         environment.remove("XDG_DATA_HOME");
337         environment.remove("HOME");
338 
339         assert(xdgAllDataDirs() == newDataDirs);
340     }
341 
342     @trusted string[] xdgConfigDirs(string subfolder = null) nothrow
343     {
344         auto result = pathsFromEnv("XDG_CONFIG_DIRS", subfolder);
345         if (result.length) {
346             return result;
347         } else {
348             return [buildPath("/etc/xdg", subfolder)];
349         }
350     }
351 
352     ///
353     unittest
354     {
355         auto dataConfigGuard = EnvGuard("XDG_CONFIG_DIRS");
356 
357         auto newConfigDirs = ["/usr/local/config", "/usr/config"];
358 
359         environment["XDG_CONFIG_DIRS"] = "/usr/local/config:/usr/config";
360         assert(xdgConfigDirs() == newConfigDirs);
361         assert(equal(xdgConfigDirs("menus"), newConfigDirs.map!(p => buildPath(p, "menus"))));
362 
363         environment.remove("XDG_CONFIG_DIRS");
364         assert(xdgConfigDirs() == ["/etc/xdg"]);
365         assert(equal(xdgConfigDirs("autostart"), ["/etc/xdg"].map!(p => buildPath(p, "autostart"))));
366     }
367 
368     @trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow
369     {
370         string configHome = xdgConfigHome(subfolder);
371         string[] configDirs = xdgConfigDirs(subfolder);
372         if (configHome.length) {
373             return configHome ~ configDirs;
374         } else {
375             return configDirs;
376         }
377     }
378 
379     ///
380     unittest
381     {
382         auto homeGuard = EnvGuard("HOME");
383         auto configHomeGuard = EnvGuard("XDG_CONFIG_HOME");
384         auto configDirsGuard = EnvGuard("XDG_CONFIG_DIRS");
385 
386         auto newConfigHome = "/home/myuser/data";
387         environment["XDG_CONFIG_HOME"] = newConfigHome;
388         auto newConfigDirs = ["/usr/local/data", "/usr/data"];
389         environment["XDG_CONFIG_DIRS"] = "/usr/local/data:/usr/data";
390 
391         assert(xdgAllConfigDirs() == newConfigHome ~ newConfigDirs);
392 
393         environment.remove("XDG_CONFIG_HOME");
394         environment.remove("HOME");
395 
396         assert(xdgAllConfigDirs() == newConfigDirs);
397     }
398 
399     @trusted string xdgDataHome(string subfolder = null, bool shouldCreate = false) nothrow {
400         return xdgBaseDir("XDG_DATA_HOME", ".local/share", subfolder, shouldCreate);
401     }
402 
403     unittest
404     {
405         testXdgBaseDir("XDG_DATA_HOME", ".local/share");
406     }
407 
408     @trusted string xdgConfigHome(string subfolder = null, bool shouldCreate = false) nothrow {
409         return xdgBaseDir("XDG_CONFIG_HOME", ".config", subfolder, shouldCreate);
410     }
411 
412     unittest
413     {
414         testXdgBaseDir("XDG_CONFIG_HOME", ".config");
415     }
416 
417     @trusted string xdgCacheHome(string subfolder = null, bool shouldCreate = false) nothrow {
418         return xdgBaseDir("XDG_CACHE_HOME", ".cache", subfolder, shouldCreate);
419     }
420 
421     unittest
422     {
423         testXdgBaseDir("XDG_CACHE_HOME", ".cache");
424     }
425 
426     version(XdgPathsRuntimeDebug) {
427         private import std.stdio;
428     }
429 
430     @trusted string xdgRuntimeDir() nothrow // Do we need it on BSD systems?
431     {
432         import std.exception : assumeUnique;
433         import core.sys.posix.pwd;
434 
435         try { //one try to rule them all and for compatibility reasons
436             const uid_t uid = getuid();
437             string runtime;
438             collectException(environment.get("XDG_RUNTIME_DIR"), runtime);
439 
440             if (!runtime.length) {
441                 passwd* pw = getpwuid(uid);
442 
443                 try {
444                     if (pw && pw.pw_name) {
445                         runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
446 
447                         if (!(runtime.exists && runtime.isDir)) {
448                             if (mkdir(runtime.toStringz, privateMode) != 0) {
449                                 version(XdgPathsRuntimeDebug) stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
450                                 return null;
451                             }
452                         }
453                     } else {
454                         version(XdgPathsRuntimeDebug) stderr.writeln("Failed to get user name to create runtime directory");
455                         return null;
456                     }
457                 } catch(Exception e) {
458                     version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Error when creating runtime directory: %s", e.msg));
459                     return null;
460                 }
461             }
462             stat_t statbuf;
463             stat(runtime.toStringz, &statbuf);
464             if (statbuf.st_uid != uid) {
465                 version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid));
466                 return null;
467             }
468             if ((statbuf.st_mode & octal!777) != privateMode) {
469                 version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, privateMode));
470                 return null;
471             }
472 
473             return runtime;
474         } catch (Exception e) {
475             version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Error when getting runtime directory: %s", e.msg));
476             return null;
477         }
478     }
479 
480     version(xdgpathsFileTest) unittest
481     {
482         string runtimePath = buildPath(tempDir(), "xdgpaths-runtime-test");
483         try {
484             collectException(std.file.rmdir(runtimePath));
485 
486             if (mkdir(runtimePath.toStringz, privateMode) == 0) {
487                 auto runtimeGuard = EnvGuard("XDG_RUNTIME_DIR");
488                 environment["XDG_RUNTIME_DIR"] = runtimePath;
489                 assert(xdgRuntimeDir() == runtimePath);
490 
491                 if (chmod(runtimePath.toStringz, octal!777) == 0) {
492                     assert(xdgRuntimeDir() == string.init);
493                 }
494 
495                 std.file.rmdir(runtimePath);
496             } else {
497                 version(XdgPathsRuntimeDebug) stderr.writeln(fromStringz(strerror(errno)));
498             }
499         } catch(Exception e) {
500             version(XdgPathsRuntimeDebug) stderr.writeln(e.msg);
501         }
502     }
503 }