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