1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This module contains functions to extract XDG variables to either what they are 7 configured or the fallback according to the standard at [XDG Base Directory 8 Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). 9 */ 10 module my.xdg; 11 12 import std.algorithm : map, splitter; 13 import std.array : empty, array; 14 import std.file : exists; 15 import std.process : environment; 16 17 import my.path; 18 19 /** Returns the directory to use for program runtime data for the current 20 * user with a fallback for older OS:es. 21 * 22 * `$XDG_RUNTIME_DIR` isn't set on all OS such as older versions of CentOS. If 23 * such is the case a directory with equivalent properties when it comes to the 24 * permissions are created inside `falllback` and returned. This means that 25 * this function should in most cases work. When it fails it means something 26 * funky is happening such as someone is trying to hijack your data or 27 * `fallback` isn't writable. This is the only case when it will throw an 28 * exception. 29 * 30 * From the specification: 31 * 32 * $XDG_RUNTIME_DIR defines the base directory relative to which user-specific 33 * non-essential runtime files and other file objects (such as sockets, named 34 * pipes, ...) should be stored. The directory MUST be owned by the user, and 35 * he MUST be the only one having read and write access to it. Its Unix access 36 * mode MUST be 0700. 37 * 38 * The lifetime of the directory MUST be bound to the user being logged in. It 39 * MUST be created when the user first logs in and if the user fully logs out 40 * the directory MUST be removed. If the user logs in more than once he should 41 * get pointed to the same directory, and it is mandatory that the directory 42 * continues to exist from his first login to his last logout on the system, 43 * and not removed in between. Files in the directory MUST not survive reboot 44 * or a full logout/login cycle. 45 * 46 * The directory MUST be on a local file system and not shared with any other 47 * system. The directory MUST by fully-featured by the standards of the 48 * operating system. More specifically, on Unix-like operating systems AF_UNIX 49 * sockets, symbolic links, hard links, proper permissions, file locking, 50 * sparse files, memory mapping, file change notifications, a reliable hard 51 * link count must be supported, and no restrictions on the file name character 52 * set should be imposed. Files in this directory MAY be subjected to periodic 53 * clean-up. To ensure that your files are not removed, they should have their 54 * access time timestamp modified at least once every 6 hours of monotonic time 55 * or the 'sticky' bit should be set on the file. 56 * 57 * If $XDG_RUNTIME_DIR is not set applications should fall back to a 58 * replacement directory with similar capabilities and print a warning message. 59 * Applications should use this directory for communication and synchronization 60 * purposes and should not place larger files in it, since it might reside in 61 * runtime memory and cannot necessarily be swapped out to disk. 62 */ 63 Path xdgRuntimeDir(AbsolutePath fallback = AbsolutePath("/tmp")) @safe { 64 import std.process : environment; 65 66 auto xdg = environment.get("XDG_RUNTIME_DIR").Path; 67 if (xdg.empty) 68 xdg = makeXdgRuntimeDir(fallback); 69 return xdg; 70 } 71 72 @("shall return the XDG runtime directory") 73 unittest { 74 import std.process : environment; 75 76 auto xdg = xdgRuntimeDir; 77 auto hostEnv = environment.get("XDG_RUNTIME_DIR"); 78 if (!hostEnv.empty) 79 assert(xdg == hostEnv); 80 } 81 82 AbsolutePath makeXdgRuntimeDir(AbsolutePath rootDir = AbsolutePath("/tmp")) @trusted { 83 import core.stdc.stdio : perror; 84 import core.sys.posix.sys.stat : mkdir; 85 import core.sys.posix.sys.stat; 86 import core.sys.posix.unistd : getuid; 87 import std.file : exists; 88 import std.format : format; 89 import std.string : toStringz; 90 91 const uid = getuid(); 92 93 const base = rootDir ~ format!"user_%s"(uid); 94 string createdTmp; 95 96 foreach (i; 0 .. 1000) { 97 // create 98 createdTmp = format!"%s_%s"(base, i); 99 const cstr = createdTmp.toStringz; 100 101 if (!exists(createdTmp)) { 102 if (mkdir(cstr, S_IRWXU) != 0) { 103 createdTmp = null; 104 continue; 105 } 106 } 107 108 // validate 109 stat_t st; 110 stat(cstr, &st); 111 if (st.st_uid == uid && (st.st_mode & S_IFDIR) != 0 112 && ((st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == S_IRWXU)) { 113 break; 114 } 115 116 // try again 117 createdTmp = null; 118 } 119 120 if (createdTmp.empty) { 121 perror(null); 122 throw new Exception("Unable to create XDG_RUNTIME_DIR " ~ createdTmp); 123 } 124 return Path(createdTmp).AbsolutePath; 125 } 126 127 @safe: 128 129 /// The XDG standard directory for data files. 130 AbsolutePath xdgDataHome() { 131 return AbsolutePath(environment.get("XDG_DATA_HOME", "~/.local/share")); 132 } 133 134 /// The XDG standard directory for config files. 135 AbsolutePath xdgConfigHome() { 136 return AbsolutePath(environment.get("XDG_CONFIG_HOME", "~/.config")); 137 } 138 139 /// The prefered search order for data files. 140 AbsolutePath[] xdgDataDirs() { 141 return environment.get("XDG_DATA_DIRS").splitter(':').map!(a => AbsolutePath(a)).array; 142 } 143 144 /// The prefered search order for config files. 145 AbsolutePath[] xdgConfigDirs() { 146 return environment.get("XDG_CONFIG_DIRS").splitter(':').map!(a => AbsolutePath(a)).array; 147 }