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 }