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