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 }