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.array : empty; 13 14 import my.path; 15 16 /** Returns the directory to use for program runtime data for the current 17 * user with a fallback for older OS:es. 18 * 19 * `$XDG_RUNTIME_DIR` isn't set on all OS such as older versions of CentOS. If 20 * such is the case a directory with equivalent properties when it comes to the 21 * permissions are created inside `falllback` and returned. This means that 22 * this function should in most cases work. When it fails it means something 23 * funky is happening such as someone is trying to hijack your data or 24 * `fallback` isn't writable. This is the only case when it will throw an 25 * exception. 26 * 27 * From the specification: 28 * 29 * $XDG_RUNTIME_DIR defines the base directory relative to which user-specific 30 * non-essential runtime files and other file objects (such as sockets, named 31 * pipes, ...) should be stored. The directory MUST be owned by the user, and 32 * he MUST be the only one having read and write access to it. Its Unix access 33 * mode MUST be 0700. 34 * 35 * The lifetime of the directory MUST be bound to the user being logged in. It 36 * MUST be created when the user first logs in and if the user fully logs out 37 * the directory MUST be removed. If the user logs in more than once he should 38 * get pointed to the same directory, and it is mandatory that the directory 39 * continues to exist from his first login to his last logout on the system, 40 * and not removed in between. Files in the directory MUST not survive reboot 41 * or a full logout/login cycle. 42 * 43 * The directory MUST be on a local file system and not shared with any other 44 * system. The directory MUST by fully-featured by the standards of the 45 * operating system. More specifically, on Unix-like operating systems AF_UNIX 46 * sockets, symbolic links, hard links, proper permissions, file locking, 47 * sparse files, memory mapping, file change notifications, a reliable hard 48 * link count must be supported, and no restrictions on the file name character 49 * set should be imposed. Files in this directory MAY be subjected to periodic 50 * clean-up. To ensure that your files are not removed, they should have their 51 * access time timestamp modified at least once every 6 hours of monotonic time 52 * or the 'sticky' bit should be set on the file. 53 * 54 * If $XDG_RUNTIME_DIR is not set applications should fall back to a 55 * replacement directory with similar capabilities and print a warning message. 56 * Applications should use this directory for communication and synchronization 57 * purposes and should not place larger files in it, since it might reside in 58 * runtime memory and cannot necessarily be swapped out to disk. 59 */ 60 Path xdgRuntimeDir(AbsolutePath fallback = AbsolutePath("/tmp")) @safe { 61 import std.process : environment; 62 63 auto xdg = environment.get("XDG_RUNTIME_DIR").Path; 64 if (xdg.empty) 65 xdg = makeXdgRuntimeDir(fallback); 66 return xdg; 67 } 68 69 @("shall return the XDG runtime directory") 70 unittest { 71 import std.process : environment; 72 73 auto xdg = xdgRuntimeDir; 74 auto hostEnv = environment.get("XDG_RUNTIME_DIR"); 75 if (!hostEnv.empty) 76 assert(xdg == hostEnv); 77 } 78 79 AbsolutePath makeXdgRuntimeDir(AbsolutePath rootDir = AbsolutePath("/tmp")) @trusted { 80 import core.stdc.stdio : perror; 81 import core.sys.posix.sys.stat : mkdir; 82 import core.sys.posix.sys.stat; 83 import core.sys.posix.unistd : getuid; 84 import std.file : exists; 85 import std.format : format; 86 import std..string : toStringz; 87 88 const uid = getuid(); 89 90 const base = rootDir ~ format!"user_%s"(uid); 91 string createdTmp; 92 93 foreach (i; 0 .. 1000) { 94 // create 95 createdTmp = format!"%s_%s"(base, i); 96 const cstr = createdTmp.toStringz; 97 98 if (!exists(createdTmp)) { 99 if (mkdir(cstr, S_IRWXU) != 0) { 100 createdTmp = null; 101 continue; 102 } 103 } 104 105 // validate 106 stat_t st; 107 stat(cstr, &st); 108 if (st.st_uid == uid && (st.st_mode & S_IFDIR) != 0 109 && ((st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == S_IRWXU)) { 110 break; 111 } 112 113 // try again 114 createdTmp = null; 115 } 116 117 if (createdTmp.empty) { 118 perror(null); 119 throw new Exception("Unable to create XDG_RUNTIME_DIR " ~ createdTmp); 120 } 121 return Path(createdTmp).AbsolutePath; 122 }