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