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