1 /**
2 * OS-specific configuration storage.
3 *
4 * License:
5 * This Source Code Form is subject to the terms of
6 * the Mozilla Public License, v. 2.0. If a copy of
7 * the MPL was not distributed with this file, You
8 * can obtain one at http://mozilla.org/MPL/2.0/.
9 *
10 * Authors:
11 * Vladimir Panteleev <ae@cy.md>
12 */
13
14 module ae.sys.config;
15
16 import std.ascii;
17
18 import ae.sys.file;
19 import ae.sys.paths;
20
21 /// Cross-platform API for storing and retrieving
22 /// the application's configuration.
23 /// On Windows, the Windows registry is used.
24 /// On POSIX, configuration is stored in the XDG
25 /// configuration directory (`"~/.config"`).
26 class Config
27 {
28 version (Windows)
29 {
30 import std.exception;
31 import std.utf;
32 import std.array;
33
34 import ae.sys.windows.imports;
35 mixin(importWin32!q{windef});
36 mixin(importWin32!q{winreg});
37
38 // On Windows, just keep the registry key open and read/write values directly.
39 this(string appName = null, string companyName = null)
40 {
41 if (!appName)
42 appName = getExecutableName();
43 if (companyName)
44 appName = companyName ~ `\` ~ appName;
45
46 enforce(RegCreateKeyExW(
47 HKEY_CURRENT_USER,
48 toUTFz!LPCWSTR(`Software\` ~ appName),
49 0,
50 null,
51 0,
52 KEY_READ | KEY_WRITE,
53 null,
54 &key,
55 null) == ERROR_SUCCESS, "RegCreateKeyEx failed");
56 } ///
57
58 ~this() @nogc
59 {
60 if (key)
61 RegCloseKey(key);
62 }
63
64 private T readImpl(T)(string name, T defaultValue)
65 {
66 try
67 {
68 static if (is(T : const(char[]))) // strings
69 {
70 uint bytes = getSize(name);
71 enforce(bytes % 2 == 0);
72 wchar[] ws = new wchar[bytes / 2];
73 readRaw(name, ws);
74 enforce(ws[$-1]==0); // should be null-terminated
75 return to!T(ws[0..$-1]);
76 }
77 else
78 static if (is(T == long) || is(T == ulong))
79 {
80 T value;
81 readRaw(name, (&value)[0..1]);
82 return value;
83 }
84 else
85 static if (is(T : uint) || is(T : bool))
86 {
87 uint value;
88 readRaw(name, (&value)[0..1]);
89 return cast(T)value;
90 }
91 else
92 static assert(0, "Can't read values of type " ~ T.stringof);
93 }
94 catch (Throwable e) // FIXME
95 return defaultValue;
96 }
97
98 private void writeImpl(T)(string name, T value)
99 {
100 static if (is(T : const(char[]))) // strings
101 {
102 wstring ws = to!wstring(value ~ '\0');
103 writeRaw(name, ws, REG_SZ);
104 }
105 else
106 static if (is(T == long) || is(T == ulong))
107 writeRaw(name, (&value)[0..1], REG_QWORD);
108 else
109 static if (is(T : uint) || is(T : bool))
110 {
111 uint dwordValue = cast(uint)value;
112 writeRaw(name, (&dwordValue)[0..1], REG_DWORD);
113 }
114 else
115 static assert(0, "Can't write values of type " ~ T.stringof);
116 }
117
118 void save() {} /// Flush unwritten changes.
119
120 private:
121 HKEY key;
122
123 void readRaw(string name, void[] dest)
124 {
125 enforce(getSize(name) == dest.length, "Invalid registry value length for " ~ name);
126 DWORD size = cast(DWORD)dest.length;
127 enforce(RegQueryValueExW(key, toUTFz!LPCWSTR(name), null, null, cast(ubyte*)dest.ptr, &size) == ERROR_SUCCESS, "RegQueryValueEx failed");
128 enforce(size == dest.length, "Not enough data read");
129 }
130
131 void writeRaw(string name, const(void)[] dest, DWORD type)
132 {
133 enforce(RegSetValueExW(key, toUTFz!LPCWSTR(name), 0, type, cast(ubyte*)dest.ptr, cast(uint)dest.length) == ERROR_SUCCESS, "RegSetValueEx failed");
134 }
135
136 uint getSize(string name)
137 {
138 DWORD size;
139 enforce(RegQueryValueExW(key, toUTFz!LPCWSTR(name), null, null, null, &size) == ERROR_SUCCESS);
140 return size;
141 }
142 }
143 else // POSIX
144 {
145 import std.string;
146 import std.stdio;
147 import std.file;
148 import std.path;
149 import std.conv;
150
151 // Cache values from memory, and save them to disk when the program exits.
152 this(string appName = null, string companyName = null)
153 {
154 fileName = getConfigDir(appName) ~ "/config";
155 if (!exists(fileName))
156 return;
157 foreach (line; File(fileName, "rt").byLine())
158 if (line.length>0 && line[0]!='#')
159 {
160 auto p = indexOf(line, '=');
161 if (p>0)
162 values[line[0..p].idup] = line[p+1..$].idup;
163 }
164 instances ~= this;
165 } ///
166
167 ~this() @nogc
168 {
169 assert(!dirty, "Dirty config destruction");
170 }
171
172 private T readImpl(T)(string name, T defaultValue = T.init)
173 {
174 auto pvalue = name in values;
175 if (pvalue)
176 return to!T(*pvalue);
177 else
178 return defaultValue;
179 }
180
181 private void writeImpl(T)(string name, T value)
182 if (is(typeof(to!string(T.init))))
183 {
184 values[name] = to!string(value);
185 dirty = true;
186 }
187
188 /// Flush unwritten changes.
189 void save()
190 {
191 if (!dirty)
192 return;
193 ensurePathExists(fileName);
194 auto f = File(fileName, "wt");
195 foreach (name, value; values)
196 f.writefln("%s=%s", name, value);
197 dirty = false;
198 }
199
200 private:
201 string[string] values;
202 string fileName;
203 bool dirty;
204
205 static Config[] instances;
206
207 static ~this()
208 {
209 foreach (instance; instances)
210 instance.save();
211 }
212 }
213
214 /// Read value.
215 T read(T)(string name, T defaultValue = T.init)
216 {
217 static if (is(T==struct))
218 {
219 T v = defaultValue;
220 auto prefix = name ~ ".";
221 foreach (i, field; v.tupleof)
222 v.tupleof[i] = readImpl(prefix ~ Capitalize!(v.tupleof[i].stringof[2..$]), v.tupleof[i]);
223 return v;
224 }
225 else
226 static if (is(typeof(readImpl(name, defaultValue))))
227 return readImpl(name, defaultValue);
228 else
229 static if (is(typeof(to!T(string.init))))
230 {
231 auto s = readImpl(name, string.init);
232 return s is string.init ? defaultValue : to!T(s);
233 }
234 else
235 static assert(0, "Can't read values of type " ~ T.stringof);
236 }
237
238 /// Write value.
239 void write(T)(string name, T v)
240 {
241 static if (is(T==struct))
242 {
243 auto prefix = name ~ ".";
244 foreach (i, field; v.tupleof)
245 writeImpl(prefix ~ Capitalize!(v.tupleof[i].stringof[2..$]), v.tupleof[i]);
246 }
247 else
248 static if (is(typeof(writeImpl(name, v))))
249 return writeImpl(name, v);
250 else
251 static if (is(typeof(to!string(v))))
252 writeImpl(name, to!string(v));
253 else
254 static assert(0, "Can't write values of type " ~ T.stringof);
255 }
256 }
257
258 private template Capitalize(string s)
259 {
260 enum Capitalize = s.length ? cast(char)toUpper(s[0]) ~ s[1..$] : s;
261 }