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