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(Path fallback = Path("/tmp")) @safe {
61     import std.process : environment;
62 
63     Path backup() @trusted {
64         import core.stdc.stdio : perror;
65         import core.sys.posix.sys.stat : mkdir;
66         import core.sys.posix.sys.stat;
67         import core.sys.posix.unistd : getuid;
68         import std.file : exists;
69         import std.format : format;
70         import std..string : toStringz;
71 
72         const base = fallback ~ format!"user_%s"(getuid);
73         string rval;
74 
75         foreach (i; 0 .. 1000) {
76             // create
77             rval = format!"%s_%s"(base, i);
78             const cstr = rval.toStringz;
79 
80             if (!exists(rval)) {
81                 if (mkdir(cstr, S_IRWXU) != 0) {
82                     continue;
83                 }
84             }
85 
86             // validate
87             stat_t st;
88             stat(cstr, &st);
89             if (st.st_uid == getuid && (st.st_mode & S_IFDIR) != 0
90                     && ((st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == S_IRWXU)) {
91                 break;
92             }
93 
94             // try again
95             rval = null;
96         }
97 
98         if (rval.empty) {
99             perror(null);
100             throw new Exception("Unable to create XDG_RUNTIME_DIR " ~ rval);
101         }
102         return Path(rval);
103     }
104 
105     auto xdg = environment.get("XDG_RUNTIME_DIR").Path;
106     if (xdg.empty)
107         xdg = backup;
108     return xdg;
109 }
110 
111 @("shall return the XDG runtime directory")
112 unittest {
113     import std.process : environment;
114 
115     auto xdg = xdgRuntimeDir;
116     auto hostEnv = environment.get("XDG_RUNTIME_DIR");
117     if (!hostEnv.empty)
118         assert(xdg == hostEnv);
119 }