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 }