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