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 	private:
113 		HKEY key;
114 
115 		void readRaw(string name, void[] dest)
116 		{
117 			enforce(getSize(name) == dest.length, "Invalid registry value length for " ~ name);
118 			DWORD size = cast(DWORD)dest.length;
119 			enforce(RegQueryValueExW(key, toUTFz!LPCWSTR(name), null, null, cast(ubyte*)dest.ptr, &size) == ERROR_SUCCESS, "RegQueryValueEx failed");
120 			enforce(size == dest.length, "Not enough data read");
121 		}
122 
123 		void writeRaw(string name, const(void)[] dest, DWORD type)
124 		{
125 			enforce(RegSetValueExW(key, toUTFz!LPCWSTR(name), 0, type, cast(ubyte*)dest.ptr, cast(uint)dest.length) == ERROR_SUCCESS, "RegSetValueEx failed");
126 		}
127 
128 		uint getSize(string name)
129 		{
130 			DWORD size;
131 			enforce(RegQueryValueExW(key, toUTFz!LPCWSTR(name), null, null, null, &size) == ERROR_SUCCESS);
132 			return size;
133 		}
134 	}
135 	else // POSIX
136 	{
137 		import std.string;
138 		import std.stdio;
139 		import std.file;
140 		import std.path;
141 		import std.conv;
142 
143 		// Cache values from memory, and save them to disk when the program exits.
144 		this(string appName = null, string companyName = null)
145 		{
146 			fileName = getConfigDir(appName) ~ "/config";
147 			if (!exists(fileName))
148 				return;
149 			foreach (line; File(fileName, "rt").byLine())
150 				if (line.length>0 && line[0]!='#')
151 				{
152 					auto p = indexOf(line, '=');
153 					if (p>0)
154 						values[line[0..p].idup] = line[p+1..$].idup;
155 				}
156 			instances ~= this;
157 		}
158 
159 		~this()
160 		{
161 			assert(!dirty, "Dirty config destruction");
162 		}
163 
164 		T readImpl(T)(string name, T defaultValue = T.init)
165 		{
166 			auto pvalue = name in values;
167 			if (pvalue)
168 				return to!T(*pvalue);
169 			else
170 				return defaultValue;
171 		}
172 
173 		void writeImpl(T)(string name, T value)
174 			if (is(typeof(to!string(T.init))))
175 		{
176 			values[name] = to!string(value);
177 			dirty = true;
178 		}
179 
180 		void save()
181 		{
182 			if (!dirty)
183 				return;
184 			auto f = File(fileName, "wt");
185 			foreach (name, value; values)
186 				f.writefln("%s=%s", name, value);
187 			dirty = false;
188 		}
189 
190 	private:
191 		string[string] values;
192 		string fileName;
193 		bool dirty;
194 
195 		static Config[] instances;
196 
197 		static ~this()
198 		{
199 			foreach (instance; instances)
200 				instance.save();
201 		}
202 	}
203 
204 	T read(T)(string name, T defaultValue = T.init)
205 	{
206 		static if (is(T==struct))
207 		{
208 			T v = defaultValue;
209 			auto prefix = name ~ ".";
210 			foreach (i, field; v.tupleof)
211 				v.tupleof[i] = readImpl(prefix ~ Capitalize!(v.tupleof[i].stringof[2..$]), v.tupleof[i]);
212 			return v;
213 		}
214 		else
215 		static if (is(typeof(readImpl(name, defaultValue))))
216 			return readImpl(name, defaultValue);
217 		else
218 		static if (is(typeof(to!T(string.init))))
219 		{
220 			auto s = readImpl(name, string.init);
221 			return s is string.init ? defaultValue : to!T(s);
222 		}
223 		else
224 			static assert(0, "Can't read values of type " ~ T.stringof);
225 	}
226 
227 	void write(T)(string name, T v)
228 	{
229 		static if (is(T==struct))
230 		{
231 			auto prefix = name ~ ".";
232 			foreach (i, field; v.tupleof)
233 				writeImpl(prefix ~ Capitalize!(v.tupleof[i].stringof[2..$]), v.tupleof[i]);
234 		}
235 		else
236 		static if (is(typeof(writeImpl(name, v))))
237 			return writeImpl(name, v);
238 		else
239 		static if (is(typeof(to!string(v))))
240 			writeImpl(name, to!string(v));
241 		else
242 			static assert(0, "Can't write values of type " ~ T.stringof);
243 	}
244 }
245 
246 private template Capitalize(string s)
247 {
248 	enum Capitalize = s.length ? cast(char)toUpper(s[0]) ~ s[1..$] : s;
249 }