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 }