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 }