1 /**
2  * Structured INI
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.utils.sini;
15 
16 import std.algorithm;
17 import std.conv;
18 import std.exception;
19 import std.range;
20 import std.string;
21 import std.traits;
22 
23 import ae.utils.exception;
24 
25 alias std..string.indexOf indexOf;
26 
27 /// Represents the user-defined behavior for handling a node in a
28 /// structured INI file's hierarchy.
29 struct IniHandler(S)
30 {
31 	/// User callback for parsing a value at this node.
32 	void delegate(S name, S value) leafHandler;
33 
34 	/// User callback for obtaining a child node from this node.
35 	IniHandler delegate(S name) nodeHandler;
36 }
37 
38 struct IniLine(S)
39 {
40 	enum Type
41 	{
42 		empty,
43 		section,
44 		value
45 	}
46 
47 	Type type;
48 	S name; // section or value
49 	S value;
50 }
51 
52 IniLine!S lexIniLine(S)(S line)
53 if (isSomeString!S)
54 {
55 	IniLine!S result;
56 
57 	line = line.chomp().stripLeft();
58 	if (line.empty)
59 		return result;
60 	if (line[0] == '#' || line[0] == ';')
61 		return result;
62 
63 	if (line[0] == '[')
64 	{
65 		line = line.stripRight();
66 		enforce(line[$-1] == ']', "Malformed section line (no ']')");
67 		result.type = result.Type.section;
68 		result.name = line[1..$-1];
69 	}
70 	else
71 	{
72 		auto pos = line.indexOf('=');
73 		enforce(pos > 0, "Malformed value line (no '=')");
74 		result.type = result.Type.value;
75 		result.name = line[0..pos].strip;
76 		result.value = line[pos+1..$].strip;
77 	}
78 	return result;
79 }
80 
81 /// Parse a structured INI from a range of lines, through the given handler.
82 void parseIni(R, H)(R r, H rootHandler)
83 	if (isInputRange!R && isSomeString!(ElementType!R))
84 {
85 	auto currentHandler = rootHandler;
86 
87 	size_t lineNumber;
88 	while (!r.empty)
89 	{
90 		lineNumber++;
91 		mixin(exceptionContext(q{"Error while parsing INI line %s:".format(lineNumber)}));
92 
93 		scope(success) r.popFront();
94 		auto line = lexIniLine(r.front);
95 		final switch (line.type)
96 		{
97 			case line.Type.empty:
98 				break;
99 			case line.Type.section:
100 				currentHandler = rootHandler;
101 				foreach (segment; line.name.split("."))
102 					currentHandler = currentHandler.nodeHandler
103 						.enforce("This group may not have any nodes.")
104 						(segment);
105 				break;
106 			case line.Type.value:
107 			{
108 				auto handler = currentHandler;
109 				auto segments = line.name.split(".");
110 				enforce(segments.length, "Malformed value line (empty name)");
111 				foreach (segment; segments[0..$-1])
112 					handler = handler.nodeHandler
113 						.enforce("This group may not have any nodes.")
114 						(segment);
115 				handler.leafHandler
116 					.enforce("This group may not have any values.")
117 					(segments[$-1], line.value);
118 				break;
119 			}
120 		}
121 	}
122 }
123 
124 /// Helper which creates an INI handler out of delegates.
125 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniHandler!S delegate(S) nodeHandler = null)
126 {
127 	return IniHandler!S(leafHandler, nodeHandler);
128 }
129 
130 unittest
131 {
132 	int count;
133 
134 	parseIni
135 	(
136 		q"<
137 			s.n1=v1
138 			[s]
139 			n2=v2
140 		>".splitLines(),
141 		iniHandler
142 		(
143 			null,
144 			(in char[] name)
145 			{
146 				assert(name == "s");
147 				return iniHandler
148 				(
149 					(in char[] name, in char[] value)
150 					{
151 						assert(name .length==2 && name [0] == 'n'
152 						    && value.length==2 && value[0] == 'v'
153 						    && name[1] == value[1]);
154 						count++;
155 					}
156 				);
157 			}
158 		)
159 	);
160 
161 	assert(count==2);
162 }
163 
164 /// Alternative API for IniHandler, where each leaf is a node
165 struct IniTraversingHandler(S)
166 {
167 	/// User callback for parsing a value at this node.
168 	void delegate(S value) leafHandler;
169 
170 	/// User callback for obtaining a child node from this node.
171 	IniTraversingHandler delegate(S name) nodeHandler;
172 
173 	private IniHandler!S conv()
174 	{
175 		// Don't reference "this" from a lambda,
176 		// as it can be a temporary on the stack
177 		IniTraversingHandler thisCopy = this;
178 		return IniHandler!S
179 		(
180 			(S name, S value)
181 			{
182 				thisCopy
183 					.nodeHandler
184 					.enforce("This group may not have any nodes.")
185 					(name)
186 					.leafHandler
187 					.enforce("This group may not have a value.")
188 					(value);
189 			},
190 			(S name)
191 			{
192 				return thisCopy
193 					.nodeHandler
194 					.enforce("This group may not have any nodes.")
195 					(name)
196 					.conv();
197 			}
198 		);
199 	}
200 }
201 
202 enum isNestingType(T) = isAssociativeArray!T || is(T == struct);
203 
204 IniTraversingHandler!S makeIniHandler(S = string, U)(ref U v)
205 {
206 	static if (!is(U == Unqual!U))
207 		return makeIniHandler!S(*cast(Unqual!U*)&v);
208 	else
209 	static if (is(typeof((ref U v){alias V=typeof(v[S.init]); v[S.init] = V.init; *(S.init in v) = V.init;})))
210 		return IniTraversingHandler!S
211 		(
212 			null,
213 			(S name)
214 			{
215 				auto pField = name in v;
216 				if (!pField)
217 				{
218 					v[name] = typeof(v[name]).init;
219 					pField = name in v;
220 				}
221 				else
222 				static if (!isNestingType!U)
223 					throw new Exception("Duplicate value: " ~ to!string(name));
224 				return makeIniHandler!S(*pField);
225 			}
226 		);
227 	else
228 	static if (is(U == struct))
229 		return IniTraversingHandler!S
230 		(
231 			null,
232 			delegate IniTraversingHandler!S (S name)
233 			{
234 				bool found;
235 				foreach (i, ref field; v.tupleof)
236 				{
237 					enum fieldName = to!S(v.tupleof[i].stringof[2..$]);
238 					if (name == fieldName)
239 					{
240 						static if (is(typeof(makeIniHandler!S(v.tupleof[i]))))
241 							return makeIniHandler!S(v.tupleof[i]);
242 						else
243 							throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof);
244 					}
245 				}
246 				static if (is(ReturnType!(v.parseSection)))
247 					return v.parseSection(name);
248 				else
249 					throw new Exception("Unknown field " ~ to!string(name));
250 			}
251 		);
252 	else
253 	static if (isAssociativeArray!U)
254 		return IniTraversingHandler!S
255 		(
256 			null,
257 			(S name)
258 			{
259 				alias K = typeof(v.keys[0]);
260 				auto key = to!K(name);
261 				auto pField = key in v;
262 				if (!pField)
263 				{
264 					v[key] = typeof(v[key]).init;
265 					pField = key in v;
266 				}
267 				else
268 				static if (!isNestingType!U)
269 					throw new Exception("Duplicate value: " ~ to!string(name));
270 				return makeIniHandler!S(*pField);
271 			}
272 		);
273 	else
274 	static if (is(typeof(to!U(string.init))))
275 		return IniTraversingHandler!S
276 		(
277 			(S value)
278 			{
279 				v = to!U(value);
280 			}
281 		);
282 	else
283 	static if (is(U V : V*))
284 	{
285 		static if (is(typeof(v = new V)))
286 			if (!v)
287 				v = new V;
288 		return makeIniHandler!S(*v);
289 	}
290 	else
291 		static assert(false, "Can't parse " ~ U.stringof);
292 }
293 
294 /// Parse structured INI lines from a range of strings, into a user-defined struct.
295 T parseIni(T, R)(R r)
296 	if (isInputRange!R && isSomeString!(ElementType!R))
297 {
298 	T result;
299 	r.parseIniInto(result);
300 	return result;
301 }
302 
303 /// ditto
304 void parseIniInto(R, T)(R r, ref T result)
305 	if (isInputRange!R && isSomeString!(ElementType!R))
306 {
307 	parseIni(r, makeIniHandler!(ElementType!R)(result).conv());
308 }
309 
310 unittest
311 {
312 	static struct File
313 	{
314 		struct S
315 		{
316 			string n1, n2;
317 			int[string] a;
318 		}
319 		S s;
320 	}
321 
322 	auto f = parseIni!File
323 	(
324 		q"<
325 			s.n1=v1
326 			s.a.foo=1
327 			[s]
328 			n2=v2
329 			a.bar=2
330 		>".dup.splitLines()
331 	);
332 
333 	assert(f.s.n1=="v1");
334 	assert(f.s.n2=="v2");
335 	assert(f.s.a==["foo":1, "bar":2]);
336 }
337 
338 unittest
339 {
340 	static struct Custom
341 	{
342 		struct Section
343 		{
344 			string name;
345 			string[string] values;
346 		}
347 		Section[] sections;
348 
349 		auto parseSection(wstring name)
350 		{
351 			sections.length++;
352 			auto p = &sections[$-1];
353 			p.name = to!string(name);
354 			return makeIniHandler!wstring(p.values);
355 		}
356 	}
357 
358 	auto c = parseIni!Custom
359 	(
360 		q"<
361 			[one]
362 			a=a
363 			[two]
364 			b=b
365 		>"w.splitLines()
366 	);
367 
368 	assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])]));
369 }
370 
371 version(unittest) static import ae.utils.aa;
372 
373 unittest
374 {
375 	import ae.utils.aa;
376 
377 	auto o = parseIni!(OrderedMap!(string, string))
378 	(
379 		q"<
380 			b=b
381 			a=a
382 		>".splitLines()
383 	);
384 
385 	assert(o["a"]=="a" && o["b"] == "b");
386 }
387 
388 unittest
389 {
390 	import ae.utils.aa;
391 
392 	static struct S { string x; }
393 
394 	auto o = parseIni!(OrderedMap!(string, S))
395 	(
396 		q"<
397 			b.x=b
398 			[a]
399 			x=a
400 		>".splitLines()
401 	);
402 
403 	assert(o["a"].x == "a" && o["b"].x == "b");
404 }
405 
406 unittest
407 {
408 	static struct S { string x, y; }
409 
410 	auto r = parseIni!(S[string])
411 	(
412 		q"<
413 			a.x=x
414 			[a]
415 			y=y
416 		>".splitLines()
417 	);
418 
419 	assert(r["a"].x == "x" && r["a"].y == "y");
420 }
421 
422 unittest
423 {
424 	static struct S { string x, y; }
425 	static struct T { S* s; }
426 
427 	{
428 		T t;
429 		parseIniInto(["s.x=v"], t);
430 		assert(t.s.x == "v");
431 	}
432 
433 	{
434 		S s = {"x"}; T t = {&s};
435 		parseIniInto(["s.y=v"], t);
436 		assert(s.x == "x");
437 		assert(s.y == "v");
438 	}
439 }
440 
441 // ***************************************************************************
442 
443 deprecated alias StructuredIniHandler = IniHandler;
444 deprecated alias parseStructuredIni = parseIni;
445 deprecated alias StructuredIniTraversingHandler = IniTraversingHandler;
446 deprecated alias makeStructuredIniHandler = makeIniHandler;
447 
448 // ***************************************************************************
449 
450 /// Convenience function to load a struct from an INI file.
451 /// Returns .init if the file does not exist.
452 S loadIni(S)(string fileName)
453 {
454 	S s;
455 
456 	import std.file;
457 	if (fileName.exists)
458 		s = fileName
459 			.readText()
460 			.splitLines()
461 			.parseIni!S();
462 
463 	return s;
464 }
465 
466 /// As above, though loads several INI files
467 /// (duplicate values appearing in later INI files
468 /// override any values from earlier files).
469 S loadInis(S)(in char[][] fileNames)
470 {
471 	S s;
472 
473 	import std.file;
474 	s = fileNames
475 		.map!(fileName =>
476 			fileName.exists ?
477 				fileName
478 				.readText()
479 				.splitLines()
480 			:
481 				null
482 		)
483 		.joiner(["[]"])
484 		.parseIni!S();
485 
486 	return s;
487 }
488 
489 // ***************************************************************************
490 
491 /// Simple convenience formatter for writing INI files.
492 struct IniWriter(O)
493 {
494 	O writer;
495 
496 	void startSection(string name)
497 	{
498 		writer.put('[', name, "]\n");
499 	}
500 
501 	void writeValue(string name, string value)
502 	{
503 		writer.put(name, '=', value, '\n');
504 	}
505 }
506 
507 /// Insert a blank line before each section
508 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); }
509 
510 // ***************************************************************************
511 
512 /**
513    Adds or updates a value in an INI file.
514 
515    If the value is already in the INI file, then it is updated
516    in-place; otherwise, a new one is added to the matching section.
517 
518    Whitespace and comments on other lines are preserved.
519 
520    Params:
521      lines = INI file lines (as in parseIni)
522      name = fully-qualified name of the value to update
523             (use `.` to specify section path)
524      value = new value to write
525 */
526 
527 void updateIni(S)(ref S[] lines, S name, S value)
528 {
529 	size_t valueLine = size_t.max;
530 	S valueLineSection;
531 
532 	S currentSection = null;
533 	auto pathPrefix() { return chain(currentSection, repeat(typeof(name[0])('.'), currentSection is null ? 0 : 1)); }
534 
535 	size_t bestSectionEnd;
536 	S bestSection;
537 	bool inBestSection = true;
538 
539 	foreach (i, line; lines)
540 	{
541 		auto lex = lexIniLine(line);
542 		final switch (lex.type)
543 		{
544 			case lex.Type.empty:
545 				break;
546 			case lex.Type.value:
547 				if (equal(chain(pathPrefix, lex.name), name))
548 				{
549 					valueLine = i;
550 					valueLineSection = currentSection;
551 				}
552 				break;
553 			case lex.type.section:
554 				if (inBestSection)
555 					bestSectionEnd = i;
556 				inBestSection = false;
557 
558 				currentSection = lex.name;
559 				if (name.startsWith(pathPrefix) && currentSection.length > bestSection.length)
560 				{
561 					bestSection = currentSection;
562 					inBestSection = true;
563 				}
564 				break;
565 		}
566 	}
567 
568 	if (inBestSection)
569 		bestSectionEnd = lines.length;
570 
571 	S genLine(S section) { return name[section.length ? section.length + 1 : 0 .. $] ~ '=' ~ value; }
572 
573 	if (valueLine != size_t.max)
574 		lines[valueLine] = genLine(valueLineSection);
575 	else
576 		lines = lines[0..bestSectionEnd] ~ genLine(bestSection) ~ lines[bestSectionEnd..$];
577 }
578 
579 unittest
580 {
581 	auto ini = q"<
582 		a=1
583 		a=2
584 	>".splitLines();
585 	updateIni(ini, "a", "3");
586 	struct S { int a; }
587 	assert(parseIni!S(ini).a == 3);
588 }
589 
590 unittest
591 {
592 	auto ini = q"<
593 		a=1
594 		[s]
595 		a=2
596 		[t]
597 		a=3
598 	>".strip.splitLines.map!strip.array;
599 	updateIni(ini, "a", "4");
600 	updateIni(ini, "s.a", "5");
601 	updateIni(ini, "t.a", "6");
602 	assert(equal(ini, q"<
603 		a=4
604 		[s]
605 		a=5
606 		[t]
607 		a=6
608 	>".strip.splitLines.map!strip), text(ini));
609 }
610 
611 unittest
612 {
613 	auto ini = q"<
614 		[s]
615 		[t]
616 	>".strip.splitLines.map!strip.array;
617 	updateIni(ini, "a", "1");
618 	updateIni(ini, "s.a", "2");
619 	updateIni(ini, "t.a", "3");
620 	assert(equal(ini, q"<
621 		a=1
622 		[s]
623 		a=2
624 		[t]
625 		a=3
626 	>".strip.splitLines.map!strip));
627 }
628 
629 void updateIniFile(S)(string fileName, S name, S value)
630 {
631 	import std.file, std.stdio, std.utf;
632 	auto lines = fileName.exists ? fileName.readText.splitLines : null;
633 	updateIni(lines, name, value);
634 	lines.map!(l => chain(l.byCodeUnit, only(typeof(S.init[0])('\n')))).joiner.toFile(fileName);
635 }
636 
637 unittest
638 {
639 	import std.file;
640 	enum fn = "temp.ini";
641 	std.file.write(fn, "a=b\n");
642 	scope(exit) remove(fn);
643 	updateIniFile(fn, "a", "c");
644 	assert(read(fn) == "a=c\n");
645 }