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, string newValue) {
132                 envVar = env;
133                 envValue = environment.get(env);
134                 environment[env] = newValue;
135             }
136 
137             ~this() {
138                 if (envValue is null) {
139                     environment.remove(envVar);
140                 } else {
141                     environment[envVar] = envValue;
142                 }
143             }
144 
145             string envVar;
146             string envValue;
147         }
148     }
149 
150     private string[] pathsFromEnvValue(string envValue, string subfolder = null) nothrow {
151         string[] result;
152         try {
153             foreach(path; splitter(envValue, ':').filter!(p => !p.empty).map!(p => buildPath(p, subfolder))) {
154                 if (path[$-1] == '/') {
155                     path = path[0..$-1];
156                 }
157                 if (!result.canFind(path)) {
158                     result ~= path;
159                 }
160             }
161         } catch(Exception e) {
162 
163         }
164         return result;
165     }
166 
167     unittest
168     {
169         assert(pathsFromEnvValue("") == (string[]).init);
170         assert(pathsFromEnvValue(":") == (string[]).init);
171         assert(pathsFromEnvValue("::") == (string[]).init);
172 
173         assert(pathsFromEnvValue("path1:path2") == ["path1", "path2"]);
174         assert(pathsFromEnvValue("path1:") == ["path1"]);
175         assert(pathsFromEnvValue("path1/") == ["path1"]);
176         assert(pathsFromEnvValue("path1/:path1") == ["path1"]);
177         assert(pathsFromEnvValue("path2:path1:path2") == ["path2", "path1"]);
178     }
179 
180     private string[] pathsFromEnv(string envVar, string subfolder = null) nothrow {
181         string envValue;
182         collectException(environment.get(envVar), envValue);
183         return pathsFromEnvValue(envValue, subfolder);
184     }
185 
186     private bool ensureExists(string dir) nothrow
187     {
188         bool ok;
189         try {
190             ok = dir.exists;
191             if (!ok) {
192                 mkdirRecurse(dir.dirName);
193                 ok = mkdir(dir.toStringz, privateMode) == 0;
194             } else {
195                 ok = dir.isDir;
196             }
197         } catch(Exception e) {
198             ok = false;
199         }
200         return ok;
201     }
202 
203     unittest
204     {
205         import std.file;
206         import std.stdio;
207 
208         string temp = tempDir();
209         if (temp.length) {
210             string testDir = buildPath(temp, "xdgpaths-unittest-tempdir");
211             string testFile = buildPath(testDir, "touched");
212             string testSubDir = buildPath(testDir, "subdir");
213             try {
214                 mkdir(testDir);
215                 File(testFile, "w");
216                 assert(!ensureExists(testFile));
217                 enforce(ensureExists(testSubDir));
218             } catch(Exception e) {
219 
220             } finally {
221                 collectException(rmdir(testSubDir));
222                 collectException(remove(testFile));
223                 collectException(rmdir(testDir));
224             }
225         }
226     }
227 
228     private string xdgBaseDir(string envvar, string fallback, string subfolder = null, bool shouldCreate = false) nothrow {
229         string dir;
230         collectException(environment.get(envvar), dir);
231         if (dir.length == 0) {
232             string home;
233             collectException(environment.get("HOME"), home);
234             dir = home.length ? buildPath(home, fallback) : null;
235         }
236 
237         if (dir.length == 0) {
238             return null;
239         }
240 
241         if (shouldCreate) {
242             if (ensureExists(dir)) {
243                 if (subfolder.length) {
244                     string path = buildPath(dir, subfolder);
245                     try {
246                         if (!path.exists) {
247                             mkdirRecurse(path);
248                         }
249                         return path;
250                     } catch(Exception e) {
251 
252                     }
253                 } else {
254                     return dir;
255                 }
256             }
257         } else {
258             return buildPath(dir, subfolder);
259         }
260         return null;
261     }
262 
263     version(unittest) {
264         void testXdgBaseDir(string envVar, string fallback) {
265             auto newDataHome = "/home/myuser/data";
266             auto dataHomeGuard = EnvGuard(envVar, newDataHome);
267             environment[envVar] = newDataHome;
268             assert(xdgBaseDir(envVar, fallback) == newDataHome);
269             assert(xdgBaseDir(envVar, fallback, "applications") == buildPath(newDataHome, "applications"));
270 
271             environment.remove(envVar);
272             auto newHome = "/home/myuser";
273             auto homeGuard = EnvGuard("HOME", newHome);
274             assert(xdgBaseDir(envVar, fallback) == buildPath(newHome, fallback));
275             assert(xdgBaseDir(envVar, fallback, "icons") == buildPath(newHome, fallback, "icons"));
276 
277             environment.remove("HOME");
278             assert(xdgBaseDir(envVar, fallback).empty);
279             assert(xdgBaseDir(envVar, fallback, "mime").empty);
280         }
281     }
282 
283     @trusted string[] xdgDataDirs(string subfolder = null) nothrow
284     {
285         auto result = pathsFromEnv("XDG_DATA_DIRS", subfolder);
286         if (result.length) {
287             return result;
288         } else {
289             return [buildPath("/usr/local/share", subfolder), buildPath("/usr/share", subfolder)];
290         }
291     }
292 
293     ///
294     unittest
295     {
296         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS", "/usr/local/data:/usr/data:/usr/local/data/:/usr/data/");
297         auto newDataDirs = ["/usr/local/data", "/usr/data"];
298 
299         assert(xdgDataDirs() == newDataDirs);
300         assert(equal(xdgDataDirs("applications"), newDataDirs.map!(p => buildPath(p, "applications"))));
301 
302         environment.remove("XDG_DATA_DIRS");
303         assert(xdgDataDirs() == ["/usr/local/share", "/usr/share"]);
304         assert(equal(xdgDataDirs("icons"), ["/usr/local/share", "/usr/share"].map!(p => buildPath(p, "icons"))));
305     }
306 
307     @trusted string[] xdgAllDataDirs(string subfolder = null) nothrow
308     {
309         string dataHome = xdgDataHome(subfolder);
310         string[] dataDirs = xdgDataDirs(subfolder);
311         if (dataHome.length) {
312             return dataHome ~ dataDirs;
313         } else {
314             return dataDirs;
315         }
316     }
317 
318     ///
319     unittest
320     {
321         auto newDataHome = "/home/myuser/data";
322         auto newDataDirs = ["/usr/local/data", "/usr/data"];
323 
324         auto homeGuard = EnvGuard("HOME", "");
325         auto dataHomeGuard = EnvGuard("XDG_DATA_HOME", newDataHome);
326         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS", "/usr/local/data:/usr/data");
327 
328         assert(xdgAllDataDirs() == newDataHome ~ newDataDirs);
329 
330         environment.remove("XDG_DATA_HOME");
331         environment.remove("HOME");
332 
333         assert(xdgAllDataDirs() == newDataDirs);
334     }
335 
336     @trusted string[] xdgConfigDirs(string subfolder = null) nothrow
337     {
338         auto result = pathsFromEnv("XDG_CONFIG_DIRS", subfolder);
339         if (result.length) {
340             return result;
341         } else {
342             return [buildPath("/etc/xdg", subfolder)];
343         }
344     }
345 
346     ///
347     unittest
348     {
349         auto dataConfigGuard = EnvGuard("XDG_CONFIG_DIRS", "/usr/local/config:/usr/config");
350         auto newConfigDirs = ["/usr/local/config", "/usr/config"];
351 
352         assert(xdgConfigDirs() == newConfigDirs);
353         assert(equal(xdgConfigDirs("menus"), newConfigDirs.map!(p => buildPath(p, "menus"))));
354 
355         environment.remove("XDG_CONFIG_DIRS");
356         assert(xdgConfigDirs() == ["/etc/xdg"]);
357         assert(equal(xdgConfigDirs("autostart"), ["/etc/xdg"].map!(p => buildPath(p, "autostart"))));
358     }
359 
360     @trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow
361     {
362         string configHome = xdgConfigHome(subfolder);
363         string[] configDirs = xdgConfigDirs(subfolder);
364         if (configHome.length) {
365             return configHome ~ configDirs;
366         } else {
367             return configDirs;
368         }
369     }
370 
371     ///
372     unittest
373     {
374         auto newConfigHome = "/home/myuser/data";
375         auto newConfigDirs = ["/usr/local/data", "/usr/data"];
376 
377         auto homeGuard = EnvGuard("HOME", "");
378         auto configHomeGuard = EnvGuard("XDG_CONFIG_HOME", newConfigHome);
379         auto configDirsGuard = EnvGuard("XDG_CONFIG_DIRS", "/usr/local/data:/usr/data");
380 
381         assert(xdgAllConfigDirs() == newConfigHome ~ newConfigDirs);
382 
383         environment.remove("XDG_CONFIG_HOME");
384         environment.remove("HOME");
385 
386         assert(xdgAllConfigDirs() == newConfigDirs);
387     }
388 
389     @trusted string xdgDataHome(string subfolder = null, bool shouldCreate = false) nothrow {
390         return xdgBaseDir("XDG_DATA_HOME", ".local/share", subfolder, shouldCreate);
391     }
392 
393     unittest
394     {
395         testXdgBaseDir("XDG_DATA_HOME", ".local/share");
396     }
397 
398     @trusted string xdgConfigHome(string subfolder = null, bool shouldCreate = false) nothrow {
399         return xdgBaseDir("XDG_CONFIG_HOME", ".config", subfolder, shouldCreate);
400     }
401 
402     unittest
403     {
404         testXdgBaseDir("XDG_CONFIG_HOME", ".config");
405     }
406 
407     @trusted string xdgCacheHome(string subfolder = null, bool shouldCreate = false) nothrow {
408         return xdgBaseDir("XDG_CACHE_HOME", ".cache", subfolder, shouldCreate);
409     }
410 
411     unittest
412     {
413         testXdgBaseDir("XDG_CACHE_HOME", ".cache");
414     }
415 
416     version(XdgPathsRuntimeDebug) {
417         private import std.stdio;
418     }
419 
420     @trusted string xdgRuntimeDir() nothrow // Do we need it on BSD systems?
421     {
422         import std.exception : assumeUnique;
423         import core.sys.posix.pwd;
424 
425         try { //one try to rule them all and for compatibility reasons
426             const uid_t uid = getuid();
427             string runtime;
428             collectException(environment.get("XDG_RUNTIME_DIR"), runtime);
429 
430             if (!runtime.length) {
431                 passwd* pw = getpwuid(uid);
432 
433                 try {
434                     if (pw && pw.pw_name) {
435                         runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
436 
437                         if (!(runtime.exists && runtime.isDir)) {
438                             if (mkdir(runtime.toStringz, privateMode) != 0) {
439                                 version(XdgPathsRuntimeDebug) stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
440                                 return null;
441                             }
442                         }
443                     } else {
444                         version(XdgPathsRuntimeDebug) stderr.writeln("Failed to get user name to create runtime directory");
445                         return null;
446                     }
447                 } catch(Exception e) {
448                     version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Error when creating runtime directory: %s", e.msg));
449                     return null;
450                 }
451             }
452             stat_t statbuf;
453             stat(runtime.toStringz, &statbuf);
454             if (statbuf.st_uid != uid) {
455                 version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid));
456                 return null;
457             }
458             if ((statbuf.st_mode & octal!777) != privateMode) {
459                 version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, privateMode));
460                 return null;
461             }
462 
463             return runtime;
464         } catch (Exception e) {
465             version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Error when getting runtime directory: %s", e.msg));
466             return null;
467         }
468     }
469 
470     version(xdgpathsFileTest) unittest
471     {
472         string runtimePath = buildPath(tempDir(), "xdgpaths-runtime-test");
473         try {
474             collectException(std.file.rmdir(runtimePath));
475 
476             if (mkdir(runtimePath.toStringz, privateMode) == 0) {
477                 auto runtimeGuard = EnvGuard("XDG_RUNTIME_DIR", runtimePath);
478                 assert(xdgRuntimeDir() == runtimePath);
479 
480                 if (chmod(runtimePath.toStringz, octal!777) == 0) {
481                     assert(xdgRuntimeDir() == string.init);
482                 }
483 
484                 std.file.rmdir(runtimePath);
485             } else {
486                 version(XdgPathsRuntimeDebug) stderr.writeln(fromStringz(strerror(errno)));
487             }
488         } catch(Exception e) {
489             version(XdgPathsRuntimeDebug) stderr.writeln(e.msg);
490         }
491     }
492 }