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 }