1 /**
2  * Class representation of ini-like file.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * Copyright:
6  *  Roman Chistokhodov, 2015-2017
7  * License:
8  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
9  * See_Also:
10  *  $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
11  */
12 
13 module inilike.file;
14 
15 private {
16     import std.exception;
17     import inilike.common;
18 }
19 
20 import std.algorithm : map;
21 public import inilike.range;
22 public import inilike.exception;
23 import inilike.read;
24 
25 private @trusted string makeComment(string line) pure nothrow
26 {
27     if (line.length && line[$-1] == '\n') {
28         line = line[0..$-1];
29     }
30     if (!line.isComment && line.length) {
31         line = '#' ~ line;
32     }
33     line = line.replace("\n", " ");
34     return line;
35 }
36 
37 private @trusted IniLikeLine makeCommentLine(string line) pure nothrow
38 {
39     return IniLikeLine.fromComment(makeComment(line));
40 }
41 
42 /**
43  * Container used internally by $(D IniLikeFile) and $(D IniLikeGroup).
44  * Technically this is list with optional value access by key.
45  */
46 struct ListMap(K,V, size_t chunkSize = 32)
47 {
48     ///
49     @disable this(this);
50 
51     /**
52      * Insert key-value pair to the front of list.
53      * Returns: Inserted node.
54      */
55     Node* insertFront(K key, V value) {
56         Node* newNode = givePlace(key, value);
57         putToFront(newNode);
58         return newNode;
59     }
60 
61     /**
62      * Insert key-value pair to the back of list.
63      * Returns: Inserted node.
64      */
65     Node* insertBack(K key, V value) {
66         Node* newNode = givePlace(key, value);
67         putToBack(newNode);
68         return newNode;
69     }
70 
71     /**
72      * Insert key-value pair before some node in the list.
73      * Returns: Inserted node.
74      */
75     Node* insertBefore(Node* node, K key, V value) {
76         Node* newNode = givePlace(key, value);
77         putBefore(node, newNode);
78         return newNode;
79     }
80 
81     /**
82      * Insert key-value pair after some node in the list.
83      * Returns: Inserted node.
84      */
85     Node* insertAfter(Node* node, K key, V value) {
86         Node* newNode = givePlace(key, value);
87         putAfter(node, newNode);
88         return newNode;
89     }
90 
91     /**
92      * Add value at the start of list.
93      * Returns: Inserted node.
94      */
95     Node* prepend(V value) {
96         Node* newNode = givePlace(value);
97         putToFront(newNode);
98         return newNode;
99     }
100 
101     /**
102      * Add value at the end of list.
103      * Returns: Inserted node.
104      */
105     Node* append(V value) {
106         Node* newNode = givePlace(value);
107         putToBack(newNode);
108         return newNode;
109     }
110 
111     /**
112      * Add value before some node in the list.
113      * Returns: Inserted node.
114      */
115     Node* addBefore(Node* node, V value) {
116         Node* newNode = givePlace(value);
117         putBefore(node, newNode);
118         return newNode;
119     }
120 
121     /**
122      * Add value after some node in the list.
123      * Returns: Inserted node.
124      */
125     Node* addAfter(Node* node, V value) {
126         Node* newNode = givePlace(value);
127         putAfter(node, newNode);
128         return newNode;
129     }
130 
131     /**
132      * Move node to the front of list.
133      */
134     void moveToFront(Node* toMove)
135     {
136         if (_head is toMove) {
137             return;
138         }
139 
140         pullOut(toMove);
141         putToFront(toMove);
142     }
143 
144     /**
145      * Move node to the back of list.
146      */
147     void moveToBack(Node* toMove)
148     {
149         if (_tail is toMove) {
150             return;
151         }
152 
153         pullOut(toMove);
154         putToBack(toMove);
155     }
156 
157     /**
158      * Move node to the location before other node.
159      */
160     void moveBefore(Node* other, Node* toMove) {
161         if (other is toMove) {
162             return;
163         }
164 
165         pullOut(toMove);
166         putBefore(other, toMove);
167     }
168 
169     /**
170      * Move node to the location after other node.
171      */
172     void moveAfter(Node* other, Node* toMove) {
173         if (other is toMove) {
174             return;
175         }
176 
177         pullOut(toMove);
178         putAfter(other, toMove);
179     }
180 
181     /**
182      * Remove node from list. It also becomes unaccessible via key lookup.
183      */
184     void remove(Node* toRemove)
185     {
186         pullOut(toRemove);
187 
188         if (toRemove.hasKey()) {
189             _dict.remove(toRemove.key);
190         }
191 
192         if (_lastEmpty) {
193             _lastEmpty.next = toRemove;
194         }
195         toRemove.prev = _lastEmpty;
196         _lastEmpty = toRemove;
197     }
198 
199     /**
200      * Remove value by key.
201      * Returns: true if node with such key was found and removed. False otherwise.
202      */
203     bool remove(K key) {
204         Node** toRemove = key in _dict;
205         if (toRemove) {
206             remove(*toRemove);
207             return true;
208         }
209         return false;
210     }
211 
212     /**
213      * Remove the first node.
214      */
215     void removeFront() {
216         remove(_head);
217     }
218 
219     /**
220      * Remove the last node.
221      */
222     void removeBack() {
223         remove(_tail);
224     }
225 
226     /**
227      * Get list node by key.
228      * Returns: Found Node or null if container does not have node associated with key.
229      */
230     inout(Node)* getNode(K key) inout {
231         auto toReturn = key in _dict;
232         if (toReturn) {
233             return *toReturn;
234         }
235         return null;
236     }
237 
238     private static struct ByNode(NodeType)
239     {
240     private:
241         NodeType* _begin;
242         NodeType* _end;
243 
244     public:
245         bool empty() const {
246             return _begin is null || _end is null || _begin.prev is _end || _end.next is _begin;
247         }
248 
249         auto front() {
250             return _begin;
251         }
252 
253         auto back() {
254             return _end;
255         }
256 
257         void popFront() {
258             _begin = _begin.next;
259         }
260 
261         void popBack() {
262             _end = _end.prev;
263         }
264 
265         @property auto save() {
266             return this;
267         }
268     }
269 
270     /**
271      * Iterate over list nodes.
272      * See_Also: $(D byEntry)
273      */
274     auto byNode()
275     {
276         return ByNode!Node(_head, _tail);
277     }
278 
279     ///ditto
280     auto byNode() const
281     {
282         return ByNode!(const(Node))(_head, _tail);
283     }
284 
285     /**
286      * Iterate over nodes mapped to Entry elements (useful for testing).
287      */
288     auto byEntry() const {
289         return byNode().map!(node => node.toEntry());
290     }
291 
292     /**
293      * Represenation of list node.
294      */
295     static struct Node {
296     private:
297         K _key;
298         V _value;
299         bool _hasKey;
300         Node* _prev;
301         Node* _next;
302 
303         @trusted this(K key, V value) pure nothrow {
304             _key = key;
305             _value = value;
306             _hasKey = true;
307         }
308 
309         @trusted this(V value) pure nothrow {
310             _value = value;
311             _hasKey = false;
312         }
313 
314         @trusted void prev(Node* newPrev) pure nothrow {
315             _prev = newPrev;
316         }
317 
318         @trusted void next(Node* newNext) pure nothrow {
319             _next = newNext;
320         }
321 
322     public:
323         /**
324          * Get stored value.
325          */
326         @trusted inout(V) value() inout pure nothrow {
327             return _value;
328         }
329 
330         /**
331          * Set stored value.
332          */
333         @trusted void value(V newValue) pure nothrow {
334             _value = newValue;
335         }
336 
337         /**
338          * Tell whether this node is a key-value node.
339          */
340         @trusted bool hasKey() const pure nothrow {
341             return _hasKey;
342         }
343 
344         /**
345          * Key in key-value node.
346          */
347         @trusted auto key() const pure nothrow {
348             return _key;
349         }
350 
351         /**
352          * Access previous node in the list.
353          */
354         @trusted inout(Node)* prev() inout pure nothrow {
355             return _prev;
356         }
357 
358         /**
359          * Access next node in the list.
360          */
361         @trusted inout(Node)* next() inout pure nothrow {
362             return _next;
363         }
364 
365         ///
366         auto toEntry() const {
367             static if (is(V == class)) {
368                 alias Rebindable!(const(V)) T;
369                 if (hasKey()) {
370                     return Entry!T(_key, rebindable(_value));
371                 } else {
372                     return Entry!T(rebindable(_value));
373                 }
374 
375             } else {
376                 alias V T;
377 
378                 if (hasKey()) {
379                     return Entry!T(_key, _value);
380                 } else {
381                     return Entry!T(_value);
382                 }
383             }
384         }
385     }
386 
387     /// Mapping of Node to structure.
388     static struct Entry(T = V)
389     {
390     private:
391         K _key;
392         T _value;
393         bool _hasKey;
394 
395     public:
396         ///
397         this(T value) {
398             _value = value;
399             _hasKey = false;
400         }
401 
402         ///
403         this(K key, T value) {
404             _key = key;
405             _value = value;
406             _hasKey = true;
407         }
408 
409         ///
410         auto value() inout {
411             return _value;
412         }
413 
414         ///
415         auto key() const {
416             return _key;
417         }
418 
419         ///
420         bool hasKey() const {
421             return _hasKey;
422         }
423     }
424 
425 private:
426     void putToFront(Node* toPut)
427     in {
428         assert(toPut !is null);
429     }
430     body {
431         if (_head) {
432             _head.prev = toPut;
433             toPut.next = _head;
434             _head = toPut;
435         } else {
436             _head = toPut;
437             _tail = toPut;
438         }
439     }
440 
441     void putToBack(Node* toPut)
442     in {
443         assert(toPut !is null);
444     }
445     body {
446         if (_tail) {
447             _tail.next = toPut;
448             toPut.prev = _tail;
449             _tail = toPut;
450         } else {
451             _tail = toPut;
452             _head = toPut;
453         }
454     }
455 
456     void putBefore(Node* node, Node* toPut)
457     in {
458         assert(toPut !is null);
459         assert(node !is null);
460     }
461     body {
462         toPut.prev = node.prev;
463         if (toPut.prev) {
464             toPut.prev.next = toPut;
465         }
466         toPut.next = node;
467         node.prev = toPut;
468 
469         if (node is _head) {
470             _head = toPut;
471         }
472     }
473 
474     void putAfter(Node* node, Node* toPut)
475     in {
476         assert(toPut !is null);
477         assert(node !is null);
478     }
479     body {
480         toPut.next = node.next;
481         if (toPut.next) {
482             toPut.next.prev = toPut;
483         }
484         toPut.prev = node;
485         node.next = toPut;
486 
487         if (node is _tail) {
488             _tail = toPut;
489         }
490     }
491 
492     void pullOut(Node* node)
493     in {
494         assert(node !is null);
495     }
496     body {
497         if (node.next) {
498             node.next.prev = node.prev;
499         }
500         if (node.prev) {
501             node.prev.next = node.next;
502         }
503 
504         if (node is _head) {
505             _head = node.next;
506         }
507         if (node is _tail) {
508             _tail = node.prev;
509         }
510 
511         node.next = null;
512         node.prev = null;
513     }
514 
515     Node* givePlace(K key, V value) {
516         auto newNode = Node(key, value);
517         return givePlace(newNode);
518     }
519 
520     Node* givePlace(V value) {
521         auto newNode = Node(value);
522         return givePlace(newNode);
523     }
524 
525     Node* givePlace(ref Node node) {
526         Node* toReturn;
527         if (_lastEmpty is null) {
528             if (_storageSize < _storage.length) {
529                 toReturn = &_storage[_storageSize];
530             } else {
531                 size_t storageIndex = (_storageSize - chunkSize) / chunkSize;
532                 if (storageIndex >= _additonalStorages.length) {
533                     _additonalStorages ~= (Node[chunkSize]).init;
534                 }
535 
536                 size_t index = (_storageSize - chunkSize) % chunkSize;
537                 toReturn = &_additonalStorages[storageIndex][index];
538             }
539 
540             _storageSize++;
541         } else {
542             toReturn = _lastEmpty;
543             _lastEmpty = _lastEmpty.prev;
544             if (_lastEmpty) {
545                 _lastEmpty.next = null;
546             }
547             toReturn.next = null;
548             toReturn.prev = null;
549         }
550 
551         toReturn._hasKey = node._hasKey;
552         toReturn._key = node._key;
553         toReturn._value = node._value;
554 
555         if (toReturn.hasKey()) {
556             _dict[toReturn.key] = toReturn;
557         }
558         return toReturn;
559     }
560 
561     Node[chunkSize] _storage;
562     Node[chunkSize][] _additonalStorages;
563     size_t _storageSize;
564 
565     Node* _tail;
566     Node* _head;
567     Node* _lastEmpty;
568     Node*[K] _dict;
569 }
570 
571 unittest
572 {
573     import std.range : isBidirectionalRange;
574     ListMap!(string, string) listMap;
575     static assert(isBidirectionalRange!(typeof(listMap.byNode())));
576 }
577 
578 unittest
579 {
580     import std.algorithm : equal;
581     import std.range : ElementType;
582 
583     alias ListMap!(string, string, 2) TestListMap;
584 
585     TestListMap listMap;
586     alias typeof(listMap).Node Node;
587     alias ElementType!(typeof(listMap.byEntry())) Entry;
588 
589     assert(listMap.byEntry().empty);
590     assert(listMap.getNode("Nonexistent") is null);
591 
592     listMap.insertFront("Start", "Fast");
593     assert(listMap.getNode("Start") !is null);
594     assert(listMap.getNode("Start").key() == "Start");
595     assert(listMap.getNode("Start").value() == "Fast");
596     assert(listMap.getNode("Start").hasKey());
597     assert(listMap.byEntry().equal([Entry("Start", "Fast")]));
598     assert(listMap.remove("Start"));
599     assert(listMap.byEntry().empty);
600     assert(listMap.getNode("Start") is null);
601 
602     listMap.insertBack("Finish", "Bad");
603     assert(listMap.byEntry().equal([Entry("Finish", "Bad")]));
604     assert(listMap.getNode("Finish").value() == "Bad");
605 
606     listMap.insertFront("Begin", "Good");
607     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Finish", "Bad")]));
608     assert(listMap.getNode("Begin").value() == "Good");
609 
610     listMap.insertFront("Start", "Slow");
611     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")]));
612 
613     listMap.insertAfter(listMap.getNode("Begin"), "Middle", "Person");
614     assert(listMap.getNode("Middle").value() == "Person");
615     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Middle", "Person"), Entry("Finish", "Bad")]));
616 
617     listMap.insertBefore(listMap.getNode("Middle"), "Mean", "Man");
618     assert(listMap.getNode("Mean").value() == "Man");
619     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Mean", "Man"), Entry("Middle", "Person"), Entry("Finish", "Bad")]));
620 
621     assert(listMap.remove("Mean"));
622     assert(listMap.remove("Middle"));
623 
624     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")]));
625 
626     listMap.insertFront("New", "Era");
627     assert(listMap.byEntry().equal([Entry("New", "Era"), Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")]));
628 
629     listMap.insertBack("Old", "Epoch");
630     assert(listMap.byEntry().equal([Entry("New", "Era"), Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Old", "Epoch")]));
631 
632     listMap.moveToBack(listMap.getNode("New"));
633     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Old", "Epoch"), Entry("New", "Era")]));
634 
635     listMap.moveToFront(listMap.getNode("Begin"));
636     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Start", "Slow"), Entry("Finish", "Bad"), Entry("Old", "Epoch"), Entry("New", "Era")]));
637 
638     listMap.moveAfter(listMap.getNode("Finish"), listMap.getNode("Start"));
639     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Start", "Slow"), Entry("Old", "Epoch"), Entry("New", "Era")]));
640 
641     listMap.moveBefore(listMap.getNode("Finish"), listMap.getNode("Old"));
642     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("Finish", "Bad"), Entry("Start", "Slow"), Entry("New", "Era")]));
643 
644     listMap.moveBefore(listMap.getNode("Begin"), listMap.getNode("Start"));
645     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("Finish", "Bad"), Entry("New", "Era")]));
646 
647     listMap.moveAfter(listMap.getNode("New"), listMap.getNode("Finish"));
648     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("New", "Era"), Entry("Finish", "Bad")]));
649 
650     listMap.getNode("Begin").value = "Evil";
651     assert(listMap.getNode("Begin").value() == "Evil");
652 
653     listMap.remove("Begin");
654     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Old", "Epoch"), Entry("New", "Era"), Entry("Finish", "Bad")]));
655     listMap.remove("Old");
656     listMap.remove("New");
657     assert(!listMap.remove("Begin"));
658 
659     Node* shebang = listMap.prepend("Shebang");
660     Node* endOfStory = listMap.append("End of story");
661 
662     assert(listMap.byEntry().equal([Entry("Shebang"), Entry("Start", "Slow"), Entry("Finish", "Bad"), Entry("End of story")]));
663 
664     Node* mid = listMap.addAfter(listMap.getNode("Start"), "Mid");
665     Node* average = listMap.addBefore(listMap.getNode("Finish"), "Average");
666     assert(listMap.byEntry().equal([Entry("Shebang"), Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad"), Entry("End of story")]));
667 
668     listMap.remove(shebang);
669     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad"), Entry("End of story")]));
670 
671     listMap.remove(endOfStory);
672     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
673 
674     listMap.moveToFront(listMap.getNode("Start"));
675     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
676     listMap.moveToBack(listMap.getNode("Finish"));
677     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
678 
679     listMap.moveBefore(listMap.getNode("Start"), listMap.getNode("Start"));
680     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
681     listMap.moveAfter(listMap.getNode("Finish"), listMap.getNode("Finish"));
682     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
683 
684     listMap.insertAfter(mid, "Center", "Universe");
685     listMap.insertBefore(average, "Focus", "Cosmos");
686     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average"), Entry("Finish", "Bad")]));
687 
688     listMap.removeFront();
689     assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average"), Entry("Finish", "Bad")]));
690     listMap.removeBack();
691 
692     assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")]));
693 
694     assert(listMap.byEntry().retro.equal([Entry("Average"), Entry("Focus", "Cosmos"), Entry("Center", "Universe"), Entry("Mid")]));
695 
696     auto byEntry = listMap.byEntry();
697     Entry entry = byEntry.front;
698     assert(entry.value == "Mid");
699     assert(!entry.hasKey());
700 
701     byEntry.popFront();
702     assert(byEntry.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")]));
703     byEntry.popBack();
704     assert(byEntry.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos")]));
705 
706     entry = byEntry.back;
707     assert(entry.key == "Focus");
708     assert(entry.value == "Cosmos");
709     assert(entry.hasKey());
710 
711     auto saved = byEntry.save;
712 
713     byEntry.popFront();
714     assert(byEntry.equal([Entry("Focus", "Cosmos")]));
715     byEntry.popBack();
716     assert(byEntry.empty);
717 
718     assert(saved.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos")]));
719     saved.popBack();
720     assert(saved.equal([Entry("Center", "Universe")]));
721     saved.popFront();
722     assert(saved.empty);
723 
724     static void checkConst(ref const TestListMap listMap)
725     {
726         assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")]));
727     }
728     checkConst(listMap);
729 
730     static class Class
731     {
732         this(string name) {
733             _name = name;
734         }
735 
736         string name() const {
737             return _name;
738         }
739     private:
740         string _name;
741     }
742 
743     alias ListMap!(string, Class) TestClassListMap;
744     TestClassListMap classListMap;
745     classListMap.insertFront("name", new Class("Name"));
746     classListMap.append(new Class("Value"));
747     auto byClass = classListMap.byEntry();
748     assert(byClass.front.value.name == "Name");
749     assert(byClass.front.key == "name");
750     assert(byClass.back.value.name == "Value");
751 }
752 
753 /**
754  * Line in group.
755  */
756 struct IniLikeLine
757 {
758     /**
759      * Type of line.
760      */
761     enum Type
762     {
763         None = 0,   /// deleted or invalid line
764         Comment = 1, /// a comment or empty line
765         KeyValue = 2 /// key-value pair
766     }
767 
768     /**
769      * Contruct from comment.
770      */
771     @nogc @safe static IniLikeLine fromComment(string comment) nothrow pure {
772         return IniLikeLine(comment, null, Type.Comment);
773     }
774 
775     /**
776      * Construct from key and value. Value must be provided as it's written in a file, i.e in the escaped form.
777      */
778     @nogc @safe static IniLikeLine fromKeyValue(string key, string value) nothrow pure {
779         return IniLikeLine(key, value, Type.KeyValue);
780     }
781 
782     /**
783      * Get comment.
784      * Returns: Comment or empty string if type is not Type.Comment.
785      */
786     @nogc @safe string comment() const nothrow pure {
787         return _type == Type.Comment ? _first : null;
788     }
789 
790     /**
791      * Get key.
792      * Returns: Key or empty string if type is not Type.KeyValue
793      */
794     @nogc @safe string key() const nothrow pure {
795         return _type == Type.KeyValue ? _first : null;
796     }
797 
798     /**
799      * Get value.
800      * Returns: Value in the escaped form or empty string if type is not Type.KeyValue
801      */
802     @nogc @safe string value() const nothrow pure {
803         return _type == Type.KeyValue ? _second : null;
804     }
805 
806     /**
807      * Get type of line.
808      */
809     @nogc @safe Type type() const nothrow pure {
810         return _type;
811     }
812 private:
813     string _first;
814     string _second;
815     Type _type = Type.None;
816 }
817 
818 
819 /**
820  * This class represents the group (section) of key-value entries in the ini-like file.
821  * Instances of this class can be created only in the context of $(D IniLikeFile) or its derivatives.
822  * Values are stored in the escaped form, but the interface allows to set and get values in both escaped and unescaped forms.
823  * Note: Keys are case-sensitive.
824  */
825 class IniLikeGroup
826 {
827 private:
828     alias ListMap!(string, IniLikeLine) LineListMap;
829 
830 public:
831     ///
832     enum InvalidKeyPolicy : ubyte {
833         ///Throw error on invalid key
834         throwError,
835         ///Skip invalid key
836         skip,
837         ///Save entry with invalid key.
838         save
839     }
840 
841     /**
842      * Create $(D IniLikeGroup) instance with given name.
843      */
844     protected @nogc @safe this(string groupName) nothrow {
845         _name = groupName;
846     }
847 
848     deprecated("use escapedValue") @nogc @safe final string opIndex(string key) const nothrow pure {
849         return _listMap.getNode(key).value.value;
850     }
851 
852     private @safe final string setKeyValueImpl(string key, string value)
853     in {
854         assert(!value.needEscaping);
855     }
856     body {
857         import std.stdio;
858         auto node = _listMap.getNode(key);
859         if (node) {
860             node.value = IniLikeLine.fromKeyValue(key, value);
861         } else {
862             _listMap.insertBack(key, IniLikeLine.fromKeyValue(key, value));
863         }
864         return value;
865     }
866 
867     deprecated("use setEscapedValue") @safe final string opIndexAssign(string value, string key) {
868         return setEscapedValue(key, value);
869     }
870 
871     deprecated("use setEscapedValue") @safe final string opIndexAssign(string value, string key, string locale) {
872         return setEscapedValue(key, locale, value);
873     }
874 
875     /**
876      * Check if the group contains a value associated with the key.
877      */
878     @nogc @safe final bool contains(string key) const nothrow pure {
879         return _listMap.getNode(key) !is null;
880     }
881 
882     /**
883      * Get value by key in escaped form.
884      * Returns: The escaped value associated with the key, or empty string if group does not contain such item.
885      * See_Also: $(D setEscapedValue), $(D unescapedValue)
886      */
887     @nogc @safe final string escapedValue(string key) const nothrow pure {
888         auto node = _listMap.getNode(key);
889         if (node) {
890             return node.value.value;
891         } else {
892             return null;
893         }
894     }
895 
896     /**
897      * Perform locale matching lookup as described in $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html, Localized values for keys).
898      * Params:
899      *  key = Non-localized key.
900      *  locale = Locale in intereset.
901      *  nonLocaleFallback = Allow fallback to non-localized version.
902      * Returns:
903      *  The escaped localized value associated with key and locale,
904      *  or the value associated with non-localized key if group does not contain localized value and nonLocaleFallback is true.
905      * See_Also: $(D setEscapedValue), $(D unescapedValue)
906      */
907     @safe final string escapedValue(string key, string locale, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) const nothrow pure {
908         //Any ideas how to get rid of this boilerplate and make less allocations?
909         const t = parseLocaleName(locale);
910         auto lang = t.lang;
911         auto country = t.country;
912         auto modifier = t.modifier;
913 
914         if (lang.length) {
915             string pick;
916             if (country.length && modifier.length) {
917                 pick = escapedValue(localizedKey(key, locale));
918                 if (pick !is null) {
919                     return pick;
920                 }
921             }
922             if (country.length) {
923                 pick = escapedValue(localizedKey(key, lang, country));
924                 if (pick !is null) {
925                     return pick;
926                 }
927             }
928             if (modifier.length) {
929                 pick = escapedValue(localizedKey(key, lang, string.init, modifier));
930                 if (pick !is null) {
931                     return pick;
932                 }
933             }
934             pick = escapedValue(localizedKey(key, lang, string.init));
935             if (pick !is null) {
936                 return pick;
937             }
938         }
939 
940         if (nonLocaleFallback) {
941             return escapedValue(key);
942         } else {
943             return null;
944         }
945     }
946 
947     ///
948     unittest
949     {
950         auto lilf = new IniLikeFile;
951         lilf.addGenericGroup("Entry");
952         auto group = lilf.group("Entry");
953         assert(group.groupName == "Entry");
954         group.setEscapedValue("Name", "Programmer");
955         group.setEscapedValue("Name[ru_RU]", "Разработчик");
956         group.setEscapedValue("Name[ru@jargon]", "Кодер");
957         group.setEscapedValue("Name[ru]", "Программист");
958         group.setEscapedValue("Name[de_DE@dialect]", "Programmierer"); //just example
959         group.setEscapedValue("Name[fr_FR]", "Programmeur");
960         group.setEscapedValue("GenericName", "Program");
961         group.setEscapedValue("GenericName[ru]", "Программа");
962         assert(group.escapedValue("Name") == "Programmer");
963         assert(group.escapedValue("Name", "ru@jargon") == "Кодер");
964         assert(group.escapedValue("Name", "ru_RU@jargon") == "Разработчик");
965         assert(group.escapedValue("Name", "ru") == "Программист");
966         assert(group.escapedValue("Name", "ru_RU.UTF-8") == "Разработчик");
967         assert(group.escapedValue("Name", "nonexistent locale") == "Programmer");
968         assert(group.escapedValue("Name", "de_DE@dialect") == "Programmierer");
969         assert(group.escapedValue("Name", "fr_FR.UTF-8") == "Programmeur");
970         assert(group.escapedValue("GenericName", "ru_RU") == "Программа");
971         assert(group.escapedValue("GenericName", "fr_FR") == "Program");
972         assert(group.escapedValue("GenericName", "fr_FR", No.nonLocaleFallback) is null);
973     }
974 
975     deprecated("use escapedValue") alias escapedValue value;
976 
977     private @trusted final bool validateKeyValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy)
978     {
979         validateValue(key, value);
980 
981         try {
982             validateKey(key, value);
983             return true;
984         } catch(IniLikeEntryException e) {
985             final switch(invalidKeyPolicy) {
986                 case InvalidKeyPolicy.throwError:
987                     throw e;
988                 case InvalidKeyPolicy.save:
989                     validateKeyImpl(key, value, _name);
990                     return true;
991                 case InvalidKeyPolicy.skip:
992                     validateKeyImpl(key, value, _name);
993                     return false;
994             }
995         }
996     }
997 
998     /**
999      * Set value associated with key.
1000      * Params:
1001      *  key = Key to associate value with.
1002      *  value = Value to set. Must be in escaped form.
1003      *  invalidKeyPolicy = Policy about invalid keys.
1004      * Throws: $(D inilike.exception.IniLikeEntryException) if key or value is not valid or value needs to be escaped.
1005      * See_Also: $(D escapedValue), $(D setUnescapedValue)
1006      */
1007     @safe final string setEscapedValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError)
1008     {
1009         if (validateKeyValue(key, value, invalidKeyPolicy)) {
1010             return setKeyValueImpl(key, value);
1011         }
1012         return null;
1013     }
1014 
1015     deprecated("use setEscapedValue") alias setEscapedValue setValue;
1016 
1017     /**
1018      * Set value associated with key and locale.
1019      * Throws: $(D inilike.exception.IniLikeEntryException) if key or value is not valid or value needs to be escaped.
1020      * See_Also: $(D escapedValue), $(D setUnescapedValue)
1021      */
1022     @safe final string setEscapedValue(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1023         return setEscapedValue(localizedKey(key, locale), value, invalidKeyPolicy);
1024     }
1025 
1026     /**
1027      * Get value by key.
1028      * Params:
1029      *  key = Key of value.
1030      *  locale = Optional locale to use in localized lookup if not empty.
1031      *  nonLocaleFallback = Allow fallback to non-localized version.
1032      * Returns: The unescaped value associated with key or null if not found.
1033      * See_Also: $(D escapedValue), $(D setUnescapedValue)
1034      */
1035     @safe final string unescapedValue(string key, string locale = null, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) const nothrow pure {
1036         if (locale.length) {
1037             return escapedValue(key, locale, nonLocaleFallback).unescapeValue();
1038         } else {
1039             return escapedValue(key).unescapeValue();
1040         }
1041     }
1042 
1043     deprecated("use unescapedValue") alias unescapedValue readEntry;
1044 
1045     /**
1046      * Set value by key. The value is considered to be in the unescaped form.
1047      * Throws: $(D inilike.exception.IniLikeEntryException) if key or value is not valid.
1048      * See_Also: $(D unescapedValue), $(D setEscapedValue)
1049      */
1050     @safe final string setUnescapedValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1051         value = value.escapeValue();
1052         return setEscapedValue(key, value, invalidKeyPolicy);
1053     }
1054 
1055     ///ditto, localized version
1056     @safe final string setUnescapedValue(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1057         value = value.escapeValue();
1058         return setEscapedValue(key, locale, value, invalidKeyPolicy);
1059     }
1060 
1061     deprecated("use setUnescapedValue") alias setUnescapedValue writeEntry;
1062 
1063     deprecated("use escapedValue") @safe final string localizedValue(string key, string locale, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) const nothrow pure {
1064         return escapedValue(key, locale, nonLocaleFallback);
1065     }
1066 
1067     deprecated("use setEscapedValue") @safe final string setLocalizedValue(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1068         return setEscapedValue(key, locale, value, invalidKeyPolicy);
1069     }
1070 
1071     /**
1072      * Removes entry by key. Do nothing if no value associated with key found.
1073      * Returns: true if entry was removed, false otherwise.
1074      */
1075     @safe final bool removeEntry(string key) nothrow pure {
1076         return _listMap.remove(key);
1077     }
1078 
1079     ///ditto, but remove entry by localized key.
1080     @safe final bool removeEntry(string key, string locale) nothrow pure {
1081         return removeEntry(localizedKey(key, locale));
1082     }
1083 
1084     ///ditto, but remove entry by node.
1085     @safe final void removeEntry(LineNode node) nothrow pure {
1086         _listMap.remove(node.node);
1087     }
1088 
1089     private @nogc @safe static auto staticByKeyValue(Range)(Range nodes) nothrow {
1090         return nodes.map!(node => node.value).filter!(v => v.type == IniLikeLine.Type.KeyValue).map!(v => keyValueTuple(v.key, v.value));
1091     }
1092 
1093     /**
1094      * Iterate by Key-Value pairs. Values are left in escaped form.
1095      * Returns: Range of Tuple!(string, "key", string, "value").
1096      * See_Also: $(D escapedValue), $(D localizedValue), $(D byIniLine)
1097      */
1098     @nogc @safe final auto byKeyValue() const nothrow {
1099         return staticByKeyValue(_listMap.byNode);
1100     }
1101 
1102     /**
1103      * Empty range of the same type as $(D byKeyValue). Can be used in derived classes if it's needed to have an empty range.
1104      * Returns: Empty range of Tuple!(string, "key", string, "value").
1105      */
1106     @nogc @safe static auto emptyByKeyValue() nothrow {
1107         const ListMap!(string, IniLikeLine) listMap;
1108         return staticByKeyValue(listMap.byNode);
1109     }
1110 
1111     ///
1112     unittest
1113     {
1114         assert(emptyByKeyValue().empty);
1115         auto group = new IniLikeGroup("Group name");
1116         static assert(is(typeof(emptyByKeyValue()) == typeof(group.byKeyValue()) ));
1117     }
1118 
1119     /**
1120      * Get name of this group.
1121      * Returns: The name of this group.
1122      */
1123     @nogc @safe final string groupName() const nothrow pure {
1124         return _name;
1125     }
1126 
1127     /**
1128      * Returns: Range of $(D IniLikeLine)s included in this group.
1129      * See_Also: $(D byNode), $(D byKeyValue)
1130      */
1131     @trusted final auto byIniLine() const {
1132         return _listMap.byNode.map!(node => node.value);
1133     }
1134 
1135     /**
1136      * Wrapper for internal ListMap node.
1137      */
1138     static struct LineNode
1139     {
1140     private:
1141         LineListMap.Node* node;
1142         string groupName;
1143     public:
1144         /**
1145          * Get key of node.
1146          */
1147         @nogc @trusted string key() const pure nothrow {
1148             if (node) {
1149                 return node.key;
1150             } else {
1151                 return null;
1152             }
1153         }
1154 
1155         /**
1156          * Get $(D IniLikeLine) pointed by node.
1157          */
1158         @nogc @trusted IniLikeLine line() const pure nothrow {
1159             if (node) {
1160                 return node.value;
1161             } else {
1162                 return IniLikeLine.init;
1163             }
1164         }
1165 
1166         /**
1167          * Set value for line. If underline line is comment, than newValue is set as comment.
1168          * Prerequisites: Node must be non-null.
1169          */
1170         @trusted void setEscapedValue(string newValue) pure {
1171             auto type = node.value.type;
1172             if (type == IniLikeLine.Type.KeyValue) {
1173                 node.value = IniLikeLine.fromKeyValue(node.value.key, newValue);
1174             } else if (type == IniLikeLine.Type.Comment) {
1175                 node.value = makeCommentLine(newValue);
1176             }
1177         }
1178 
1179         /**
1180          * Check if underlined node is null.
1181          */
1182         @nogc @safe bool isNull() const pure nothrow {
1183             return node is null;
1184         }
1185     }
1186 
1187     private @trusted auto lineNode(LineListMap.Node* node) pure nothrow {
1188         return LineNode(node, groupName());
1189     }
1190 
1191     /**
1192      * Iterate over nodes of internal list.
1193      * See_Also: $(D getNode), $(D byIniLine)
1194      */
1195     @trusted auto byNode() {
1196         return _listMap.byNode().map!(node => lineNode(node));
1197     }
1198 
1199     /**
1200      * Get internal list node for key.
1201      * See_Also: $(D byNode)
1202      */
1203     @trusted final auto getNode(string key) {
1204         return lineNode(_listMap.getNode(key));
1205     }
1206 
1207     /**
1208      * Add key-value entry without diret association of the value with the key. Can be used to add duplicates.
1209      */
1210     final auto appendValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1211         if (validateKeyValue(key, value, invalidKeyPolicy)) {
1212             return lineNode(_listMap.append(IniLikeLine.fromKeyValue(key, value)));
1213         } else {
1214             return lineNode(null);
1215         }
1216     }
1217 
1218     /**
1219      * Add comment line into the group.
1220      * Returns: Added $(D LineNode).
1221      * See_Also: $(D byIniLine), $(D prependComment), $(D addCommentBefore), $(D addCommentAfter)
1222      */
1223     @safe final auto appendComment(string comment) nothrow pure {
1224         return lineNode(_listMap.append(makeCommentLine(comment)));
1225     }
1226 
1227     /**
1228      * Add comment line at the start of group (after group header, before any key-value pairs).
1229      * Returns: Added $(D LineNode).
1230      * See_Also: $(D byIniLine), $(D appendComment), $(D addCommentBefore), $(D addCommentAfter)
1231      */
1232     @safe final auto prependComment(string comment) nothrow pure {
1233         return lineNode(_listMap.prepend(makeCommentLine(comment)));
1234     }
1235 
1236     /**
1237      * Add comment before some node.
1238      * Returns: Added $(D LineNode).
1239      * See_Also: $(D byIniLine), $(D appendComment), $(D prependComment), $(D getNode), $(D addCommentAfter)
1240      */
1241     @trusted final auto addCommentBefore(LineNode node, string comment) nothrow pure
1242     in {
1243         assert(!node.isNull());
1244     }
1245     body {
1246         return _listMap.addBefore(node.node, makeCommentLine(comment));
1247     }
1248 
1249     /**
1250      * Add comment after some node.
1251      * Returns: Added $(D LineNode).
1252      * See_Also: $(D byIniLine), $(D appendComment), $(D prependComment), $(D getNode), $(D addCommentBefore)
1253      */
1254     @trusted final auto addCommentAfter(LineNode node, string comment) nothrow pure
1255     in {
1256         assert(!node.isNull());
1257     }
1258     body {
1259         return _listMap.addAfter(node.node, makeCommentLine(comment));
1260     }
1261 
1262     /**
1263      * Move line to the start of group.
1264      * Prerequisites: $(D toMove) is not null and belongs to this group.
1265      * See_Also: $(D getNode)
1266      */
1267     @trusted final void moveLineToFront(LineNode toMove) nothrow pure {
1268         _listMap.moveToFront(toMove.node);
1269     }
1270 
1271     /**
1272      * Move line to the end of group.
1273      * Prerequisites: $(D toMove) is not null and belongs to this group.
1274      * See_Also: $(D getNode)
1275      */
1276     @trusted final void moveLineToBack(LineNode toMove) nothrow pure {
1277         _listMap.moveToBack(toMove.node);
1278     }
1279 
1280     /**
1281      * Move line before other line in the group.
1282      * Prerequisites: $(D toMove) and $(D other) are not null and belong to this group.
1283      * See_Also: $(D getNode)
1284      */
1285     @trusted final void moveLineBefore(LineNode other, LineNode toMove) nothrow pure {
1286         _listMap.moveBefore(other.node, toMove.node);
1287     }
1288 
1289     /**
1290      * Move line after other line in the group.
1291      * Prerequisites: $(D toMove) and $(D other) are not null and belong to this group.
1292      * See_Also: $(D getNode)
1293      */
1294     @trusted final void moveLineAfter(LineNode other, LineNode toMove) nothrow pure {
1295         _listMap.moveAfter(other.node, toMove.node);
1296     }
1297 
1298 private:
1299     @trusted static void validateKeyImpl(string key, string value, string groupName)
1300     {
1301         if (key.empty || key.strip.empty) {
1302             throw new IniLikeEntryException("key must not be empty", groupName, key, value);
1303         }
1304         if (key.isComment()) {
1305             throw new IniLikeEntryException("key must not start with #", groupName, key, value);
1306         }
1307         if (key.canFind('=')) {
1308             throw new IniLikeEntryException("key must not have '=' character in it", groupName, key, value);
1309         }
1310         if (key.needEscaping()) {
1311             throw new IniLikeEntryException("key must not contain new line characters", groupName, key, value);
1312         }
1313     }
1314 
1315 protected:
1316     /**
1317      * Validate key before setting value to key for this group and throw exception if not valid.
1318      * Can be reimplemented in derived classes.
1319      *
1320      * Default implementation checks if key is not empty string, does not look like comment and does not contain new line or carriage return characters.
1321      * Params:
1322      *  key = key to validate.
1323      *  value = value that is being set to key.
1324      * Throws: $(D inilike.exception.IniLikeEntryException) if either key is invalid.
1325      * See_Also: $(D validateValue)
1326      * Note:
1327      *  Implementer should ensure that their implementation still validates key for format consistency (i.e. no new line characters, etc.).
1328      *  If not sure, just call super.validateKey(key, value) in your implementation.
1329      */
1330     @trusted void validateKey(string key, string value) const {
1331         validateKeyImpl(key, value, _name);
1332     }
1333 
1334     ///
1335     unittest
1336     {
1337         auto ilf = new IniLikeFile();
1338         ilf.addGenericGroup("Group");
1339 
1340         auto entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("", "Value1"));
1341         assert(entryException !is null);
1342         assert(entryException.groupName == "Group");
1343         assert(entryException.key == "");
1344         assert(entryException.value == "Value1");
1345 
1346         entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("    ", "Value2"));
1347         assert(entryException !is null);
1348         assert(entryException.key == "    ");
1349         assert(entryException.value == "Value2");
1350 
1351         entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("New\nLine", "Value3"));
1352         assert(entryException !is null);
1353         assert(entryException.key == "New\nLine");
1354         assert(entryException.value == "Value3");
1355 
1356         entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("# Comment", "Value4"));
1357         assert(entryException !is null);
1358         assert(entryException.key == "# Comment");
1359         assert(entryException.value == "Value4");
1360 
1361         entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("Everyone=Is", "Equal"));
1362         assert(entryException !is null);
1363         assert(entryException.key == "Everyone=Is");
1364         assert(entryException.value == "Equal");
1365     }
1366 
1367     /**
1368      * Validate value for key before setting value to key for this group and throw exception if not valid.
1369      * Can be reimplemented in derived classes.
1370      * The key is provided because you may want to implement specific checks depending on the key name.
1371      *
1372      * Default implementation checks if value is escaped. It does not check the key in any way.
1373      * Params:
1374      *  key = key the value is being set to.
1375      *  value = value to validate. Considered to be escaped.
1376      * Throws: $(D inilike.exception.IniLikeEntryException) if value is invalid.
1377      * See_Also: $(D validateKey)
1378      */
1379     @trusted void validateValue(string key, string value) const {
1380         if (value.needEscaping()) {
1381             throw new IniLikeEntryException("The value needs to be escaped", _name, key, value);
1382         }
1383     }
1384 
1385     ///
1386     unittest
1387     {
1388         auto ilf = new IniLikeFile();
1389         ilf.addGenericGroup("Group");
1390 
1391         auto entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("Key", "New\nline"));
1392         assert(entryException !is null);
1393         assert(entryException.key == "Key");
1394         assert(entryException.value == "New\nline");
1395     }
1396 private:
1397     LineListMap _listMap;
1398     string _name;
1399 }
1400 
1401 /**
1402  * Ini-like file.
1403  */
1404 class IniLikeFile
1405 {
1406 private:
1407     alias ListMap!(string, IniLikeGroup, 8) GroupListMap;
1408 public:
1409     ///Behavior on duplicate key in the group.
1410     enum DuplicateKeyPolicy : ubyte
1411     {
1412         ///Throw error on entry with duplicate key.
1413         throwError,
1414         ///Skip duplicate without error.
1415         skip,
1416         ///Preserve all duplicates in the list. The first found value remains accessible by key.
1417         preserve
1418     }
1419 
1420     ///Behavior on group with duplicate name in the file.
1421     enum DuplicateGroupPolicy : ubyte
1422     {
1423         ///Throw error on group with duplicate name.
1424         throwError,
1425         ///Skip duplicate without error.
1426         skip,
1427         ///Preserve all duplicates in the list. The first found group remains accessible by key.
1428         preserve
1429     }
1430 
1431     ///Behavior of ini-like file reading.
1432     static struct ReadOptions
1433     {
1434         ///Behavior on groups with duplicate names.
1435         DuplicateGroupPolicy duplicateGroupPolicy = DuplicateGroupPolicy.throwError;
1436         ///Behavior on duplicate keys.
1437         DuplicateKeyPolicy duplicateKeyPolicy = DuplicateKeyPolicy.throwError;
1438 
1439         ///Behavior on invalid keys.
1440         IniLikeGroup.InvalidKeyPolicy invalidKeyPolicy = IniLikeGroup.InvalidKeyPolicy.throwError;
1441 
1442         ///Whether to preserve comments on reading.
1443         Flag!"preserveComments" preserveComments = Yes.preserveComments;
1444 
1445         ///Setting parameters in any order, leaving not mentioned ones in default state.
1446         @nogc @safe this(Args...)(Args args) nothrow pure {
1447             foreach(arg; args) {
1448                 assign(arg);
1449             }
1450         }
1451 
1452         ///
1453         unittest
1454         {
1455             ReadOptions readOptions;
1456 
1457             readOptions = ReadOptions(No.preserveComments);
1458             assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.throwError);
1459             assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.throwError);
1460             assert(!readOptions.preserveComments);
1461 
1462             readOptions = ReadOptions(DuplicateGroupPolicy.skip, DuplicateKeyPolicy.preserve);
1463             assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.skip);
1464             assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.preserve);
1465             assert(readOptions.preserveComments);
1466 
1467             const duplicateGroupPolicy = DuplicateGroupPolicy.preserve;
1468             immutable duplicateKeyPolicy = DuplicateKeyPolicy.skip;
1469             const preserveComments = No.preserveComments;
1470             readOptions = ReadOptions(duplicateGroupPolicy, IniLikeGroup.InvalidKeyPolicy.skip, preserveComments, duplicateKeyPolicy);
1471             assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.preserve);
1472             assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.skip);
1473             assert(readOptions.invalidKeyPolicy == IniLikeGroup.InvalidKeyPolicy.skip);
1474         }
1475 
1476         /**
1477          * Assign arg to the struct member of corresponding type.
1478          * Note:
1479          *  It's compile-time error to assign parameter of type which is not part of ReadOptions.
1480          */
1481         @nogc @safe void assign(T)(T arg) nothrow pure {
1482             alias Unqual!(T) ArgType;
1483             static if (is(ArgType == DuplicateKeyPolicy)) {
1484                 duplicateKeyPolicy = arg;
1485             } else static if (is(ArgType == DuplicateGroupPolicy)) {
1486                 duplicateGroupPolicy = arg;
1487             } else static if (is(ArgType == Flag!"preserveComments")) {
1488                 preserveComments = arg;
1489             } else static if (is(ArgType == IniLikeGroup.InvalidKeyPolicy)) {
1490                 invalidKeyPolicy = arg;
1491             } else {
1492                 static assert(false, "Unknown argument type " ~ typeof(arg).stringof);
1493             }
1494         }
1495     }
1496 
1497     ///
1498     unittest
1499     {
1500         string contents = `# The first comment
1501 [First Entry]
1502 # Comment
1503 GenericName=File manager
1504 GenericName[ru]=Файловый менеджер
1505 # Another comment
1506 [Another Group]
1507 Name=Commander
1508 # The last comment`;
1509 
1510         alias IniLikeFile.ReadOptions ReadOptions;
1511         alias IniLikeFile.DuplicateKeyPolicy DuplicateKeyPolicy;
1512         alias IniLikeFile.DuplicateGroupPolicy DuplicateGroupPolicy;
1513 
1514         IniLikeFile ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(No.preserveComments));
1515         assert(!ilf.readOptions().preserveComments);
1516         assert(ilf.leadingComments().empty);
1517         assert(equal(
1518             ilf.group("First Entry").byIniLine(),
1519             [IniLikeLine.fromKeyValue("GenericName", "File manager"), IniLikeLine.fromKeyValue("GenericName[ru]", "Файловый менеджер")]
1520         ));
1521         assert(equal(
1522             ilf.group("Another Group").byIniLine(),
1523             [IniLikeLine.fromKeyValue("Name", "Commander")]
1524         ));
1525 
1526         contents = `[Group]
1527 Duplicate=First
1528 Key=Value
1529 Duplicate=Second`;
1530 
1531         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateKeyPolicy.skip));
1532         assert(equal(
1533             ilf.group("Group").byIniLine(),
1534             [IniLikeLine.fromKeyValue("Duplicate", "First"), IniLikeLine.fromKeyValue("Key", "Value")]
1535         ));
1536 
1537         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateKeyPolicy.preserve));
1538         assert(equal(
1539             ilf.group("Group").byIniLine(),
1540             [IniLikeLine.fromKeyValue("Duplicate", "First"), IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromKeyValue("Duplicate", "Second")]
1541         ));
1542         assert(ilf.group("Group").escapedValue("Duplicate") == "First");
1543 
1544         contents = `[Duplicate]
1545 Key=First
1546 [Group]
1547 [Duplicate]
1548 Key=Second`;
1549 
1550         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateGroupPolicy.preserve));
1551         auto byGroup = ilf.byGroup();
1552         assert(byGroup.front.escapedValue("Key") == "First");
1553         assert(byGroup.back.escapedValue("Key") == "Second");
1554 
1555         auto byNode = ilf.byNode();
1556         assert(byNode.front.group.groupName == "Duplicate");
1557         assert(byNode.front.key == "Duplicate");
1558         assert(byNode.back.key is null);
1559 
1560         contents = `[Duplicate]
1561 Key=First
1562 [Group]
1563 [Duplicate]
1564 Key=Second`;
1565 
1566         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateGroupPolicy.skip));
1567         auto byGroup2 = ilf.byGroup();
1568         assert(byGroup2.front.escapedValue("Key") == "First");
1569         assert(byGroup2.back.groupName == "Group");
1570     }
1571 
1572     /**
1573      * Behavior of ini-like file saving.
1574      * See_Also: $(D save)
1575      */
1576     static struct WriteOptions
1577     {
1578         ///Whether to preserve comments (lines that starts with '#') on saving.
1579         Flag!"preserveComments" preserveComments = Yes.preserveComments;
1580         ///Whether to preserve empty lines on saving.
1581         Flag!"preserveEmptyLines" preserveEmptyLines = Yes.preserveEmptyLines;
1582         /**
1583          * Whether to write empty line after each group except for the last.
1584          * New line is not written when it already exists before the next group.
1585          */
1586         Flag!"lineBetweenGroups" lineBetweenGroups = No.lineBetweenGroups;
1587 
1588         /**
1589          * Pretty mode. Save comments, skip existing new lines, add line before the next group.
1590          */
1591         @nogc @safe static auto pretty() nothrow pure {
1592             return WriteOptions(Yes.preserveComments, No.preserveEmptyLines, Yes.lineBetweenGroups);
1593         }
1594 
1595         /**
1596          * Exact mode. Save all comments and empty lines as is.
1597          */
1598         @nogc @safe static auto exact() nothrow pure {
1599             return WriteOptions(Yes.preserveComments, Yes.preserveEmptyLines, No.lineBetweenGroups);
1600         }
1601 
1602         @nogc @safe this(Args...)(Args args) nothrow pure {
1603             foreach(arg; args) {
1604                 assign(arg);
1605             }
1606         }
1607 
1608         /**
1609          * Assign arg to the struct member of corresponding type.
1610          * Note:
1611          *  It's compile-time error to assign parameter of type which is not part of $(D WriteOptions).
1612          */
1613         @nogc @safe void assign(T)(T arg) nothrow pure {
1614             alias Unqual!(T) ArgType;
1615             static if (is(ArgType == Flag!"preserveEmptyLines")) {
1616                 preserveEmptyLines = arg;
1617             } else static if (is(ArgType == Flag!"lineBetweenGroups")) {
1618                 lineBetweenGroups = arg;
1619             } else static if (is(ArgType == Flag!"preserveComments")) {
1620                 preserveComments = arg;
1621             } else {
1622                 static assert(false, "Unknown argument type " ~ typeof(arg).stringof);
1623             }
1624         }
1625     }
1626 
1627     /**
1628      * Wrapper for internal $(D ListMap) node.
1629      */
1630     static struct GroupNode
1631     {
1632     private:
1633         GroupListMap.Node* node;
1634     public:
1635         /**
1636          * Key the group associated with.
1637          * While every group has groupName, it might be added to the group list without association, therefore will not have key.
1638          */
1639         @nogc @trusted string key() const pure nothrow {
1640             if (node) {
1641                 return node.key();
1642             } else {
1643                 return null;
1644             }
1645         }
1646 
1647         /**
1648          * Access underlined group.
1649          */
1650         @nogc @trusted IniLikeGroup group() pure nothrow {
1651             if (node) {
1652                 return node.value();
1653             } else {
1654                 return null;
1655             }
1656         }
1657 
1658         /**
1659          * Check if underlined node is null.
1660          */
1661         @nogc @safe bool isNull() pure nothrow const {
1662             return node is null;
1663         }
1664     }
1665 
1666 protected:
1667     /**
1668      * Insert group into $(D IniLikeFile) object and use its name as key.
1669      * Prerequisites: group must be non-null. It also should not be held by some other $(D IniLikeFile) object.
1670      */
1671     @trusted final auto insertGroup(IniLikeGroup group)
1672     in {
1673         assert(group !is null);
1674     }
1675     body {
1676         return GroupNode(_listMap.insertBack(group.groupName, group));
1677     }
1678 
1679     /**
1680      * Append group to group list without associating group name with it. Can be used to add groups with duplicated names.
1681      * Prerequisites: group must be non-null. It also should not be held by some other $(D IniLikeFile) object.
1682      */
1683     @trusted final auto putGroup(IniLikeGroup group)
1684     in {
1685         assert(group !is null);
1686     }
1687     body {
1688         return GroupNode(_listMap.append(group));
1689     }
1690 
1691     /**
1692      * Add comment before groups.
1693      * This function is called only in constructor and can be reimplemented in derived classes.
1694      * Params:
1695      *  comment = Comment line to add.
1696      */
1697     @trusted void onLeadingComment(string comment) {
1698         if (_readOptions.preserveComments) {
1699             appendLeadingComment(comment);
1700         }
1701     }
1702 
1703     /**
1704      * Add comment for group.
1705      * This function is called only in constructor and can be reimplemented in derived classes.
1706      * Params:
1707      *  comment = Comment line to add.
1708      *  currentGroup = The group returned recently by createGroup during parsing. Can be null (e.g. if discarded)
1709      *  groupName = The name of the currently parsed group. Set even if currentGroup is null.
1710      * See_Also: $(D createGroup), $(D IniLikeGroup.appendComment)
1711      */
1712     @trusted void onCommentInGroup(string comment, IniLikeGroup currentGroup, string groupName)
1713     {
1714         if (currentGroup && _readOptions.preserveComments) {
1715             currentGroup.appendComment(comment);
1716         }
1717     }
1718 
1719     /**
1720      * Add key/value pair for group.
1721      * This function is called only in constructor and can be reimplemented in derived classes.
1722      * Params:
1723      *  key = Key to insert or set.
1724      *  value = Value to set for key.
1725      *  currentGroup = The group returned recently by createGroup during parsing. Can be null (e.g. if discarded)
1726      *  groupName = The name of the currently parsed group. Set even if currentGroup is null.
1727      * See_Also: $(D createGroup)
1728      */
1729     @trusted void onKeyValue(string key, string value, IniLikeGroup currentGroup, string groupName)
1730     {
1731         if (currentGroup) {
1732             if (currentGroup.contains(key)) {
1733                 final switch(_readOptions.duplicateKeyPolicy) {
1734                     case DuplicateKeyPolicy.throwError:
1735                         throw new IniLikeEntryException("key already exists", groupName, key, value);
1736                     case DuplicateKeyPolicy.skip:
1737                         break;
1738                     case DuplicateKeyPolicy.preserve:
1739                         currentGroup.appendValue(key, value, _readOptions.invalidKeyPolicy);
1740                         break;
1741                 }
1742             } else {
1743                 currentGroup.setEscapedValue(key, value, _readOptions.invalidKeyPolicy);
1744             }
1745         }
1746     }
1747 
1748     /**
1749      * Create $(D IniLikeGroup) by groupName during file parsing.
1750      *
1751      * This function can be reimplemented in derived classes, e.g. to insert additional checks.
1752      * Returned value is later passed to $(D onCommentInGroup) and $(D onKeyValue) methods as currentGroup.
1753      * Reimplemented method also is allowed to return null.
1754      * Default implementation just creates and returns empty $(D IniLikeGroup) with name set to groupName.
1755      *  If group already exists and $(D DuplicateGroupPolicy) is skip, then null is returned.
1756      * Throws:
1757      *  $(D inilike.exception.IniLikeGroupException) if group with such name already exists.
1758      *  $(D inilike.exception.IniLikeException) if groupName is empty.
1759      * See_Also:
1760      *  $(D onKeyValue), $(D onCommentInGroup)
1761      */
1762     @trusted IniLikeGroup onGroup(string groupName) {
1763         if (group(groupName) !is null) {
1764             final switch(_readOptions.duplicateGroupPolicy) {
1765                 case DuplicateGroupPolicy.throwError:
1766                     throw new IniLikeGroupException("group with such name already exists", groupName);
1767                 case DuplicateGroupPolicy.skip:
1768                     return null;
1769                 case DuplicateGroupPolicy.preserve:
1770                     auto toPut = createGroupByName(groupName);
1771                     if (toPut) {
1772                         putGroup(toPut);
1773                     }
1774                     return toPut;
1775             }
1776         } else {
1777             auto toInsert = createGroupByName(groupName);
1778             if (toInsert) {
1779                 insertGroup(toInsert);
1780             }
1781             return toInsert;
1782         }
1783     }
1784 
1785     /**
1786      * Reimplement in derive class.
1787      */
1788     @trusted IniLikeGroup createGroupByName(string groupName) {
1789         return createEmptyGroup(groupName);
1790     }
1791 
1792     /**
1793      * Can be used in derived classes to create instance of IniLikeGroup.
1794      * Throws: $(D inilike.exception.IniLikeException) if groupName is empty.
1795      */
1796     @safe static createEmptyGroup(string groupName) {
1797         if (groupName.length == 0) {
1798             throw new IniLikeException("empty group name");
1799         }
1800         return new IniLikeGroup(groupName);
1801     }
1802 public:
1803     /**
1804      * Construct empty $(D IniLikeFile), i.e. without any groups or values
1805      */
1806     @nogc @safe this() nothrow {
1807 
1808     }
1809 
1810     /**
1811      * Read from file.
1812      * Throws:
1813      *  $(B ErrnoException) if file could not be opened.
1814      *  $(D inilike.exception.IniLikeReadException) if error occured while reading the file.
1815      */
1816     @trusted this(string fileName, ReadOptions readOptions = ReadOptions.init) {
1817         this(iniLikeFileReader(fileName), fileName, readOptions);
1818     }
1819 
1820     /**
1821      * Read from range of $(D inilike.range.IniLikeReader).
1822      * Note: All exceptions thrown within constructor are turning into $(D IniLikeReadException).
1823      * Throws:
1824      *  $(D inilike.exception.IniLikeReadException) if error occured while parsing.
1825      */
1826     this(IniLikeReader)(IniLikeReader reader, string fileName = null, ReadOptions readOptions = ReadOptions.init)
1827     {
1828         _readOptions = readOptions;
1829         IniLikeGroup currentGroup;
1830 
1831         version(DigitalMars) {
1832             static void foo(size_t ) {}
1833         }
1834 
1835         auto onMyLeadingComment = delegate void(string line) {
1836             onLeadingComment(line);
1837         };
1838         auto onMyGroup = delegate ActionOnGroup(string groupName) {
1839             currentGroup = onGroup(groupName);
1840             return ActionOnGroup.proceed;
1841         };
1842         auto onMyKeyValue = delegate void(string key, string value, string groupName) {
1843             onKeyValue(key, value, currentGroup, groupName);
1844         };
1845         auto onMyCommentInGroup = delegate void(string line, string groupName) {
1846             onCommentInGroup(line, currentGroup, groupName);
1847         };
1848 
1849         readIniLike(reader, onMyLeadingComment, onMyGroup, onMyKeyValue, onMyCommentInGroup, fileName);
1850         _fileName = fileName;
1851     }
1852 
1853     /**
1854      * Get group by name.
1855      * Returns: $(D IniLikeGroup) instance associated with groupName or null if not found.
1856      * See_Also: $(D byGroup)
1857      */
1858     @nogc @safe final inout(IniLikeGroup) group(string groupName) nothrow inout pure {
1859         auto pick = _listMap.getNode(groupName);
1860         if (pick) {
1861             return pick.value;
1862         }
1863         return null;
1864     }
1865 
1866     /**
1867      * Get $(D GroupNode) by groupName.
1868      */
1869     @nogc @safe final auto getNode(string groupName) nothrow pure {
1870         return GroupNode(_listMap.getNode(groupName));
1871     }
1872 
1873     /**
1874      * Create new group using groupName.
1875      * Returns: Newly created instance of $(D IniLikeGroup).
1876      * Throws:
1877      *  $(D inilike.exception.IniLikeGroupException) if group with such name already exists.
1878      *  $(D inilike.exception.IniLikeException) if groupName is empty.
1879      * See_Also: $(D removeGroup), $(D group)
1880      */
1881     @safe final IniLikeGroup addGenericGroup(string groupName) {
1882         if (group(groupName) !is null) {
1883             throw new IniLikeGroupException("group already exists", groupName);
1884         }
1885         auto toReturn = createEmptyGroup(groupName);
1886         insertGroup(toReturn);
1887         return toReturn;
1888     }
1889 
1890     /**
1891      * Remove group by name. Do nothing if group with such name does not exist.
1892      * Returns: true if group was deleted, false otherwise.
1893      * See_Also: $(D addGenericGroup), $(D group)
1894      */
1895     @safe bool removeGroup(string groupName) nothrow {
1896         return _listMap.remove(groupName);
1897     }
1898 
1899     /**
1900      * Range of groups in order how they were defined in file.
1901      * See_Also: $(D group)
1902      */
1903     @nogc @safe final auto byGroup() inout nothrow {
1904         return _listMap.byNode().map!(node => node.value);
1905     }
1906 
1907     /**
1908      * Iterate over $(D GroupNode)s.
1909      */
1910     @nogc @safe final auto byNode() nothrow {
1911         return _listMap.byNode().map!(node => GroupNode(node));
1912     }
1913 
1914     /**
1915      * Save object to the file using .ini-like format.
1916      * Throws: $(D ErrnoException) if the file could not be opened or an error writing to the file occured.
1917      * See_Also: $(D saveToString), $(D save)
1918      */
1919     @trusted final void saveToFile(string fileName, const WriteOptions options = WriteOptions.exact) const {
1920         import std.stdio : File;
1921 
1922         auto f = File(fileName, "w");
1923         void dg(in string line) {
1924             f.writeln(line);
1925         }
1926         save(&dg, options);
1927     }
1928 
1929     /**
1930      * Save object to string using .ini like format.
1931      * Returns: A string that represents the contents of file.
1932      * Note: The resulting string differs from the contents that would be written to file via $(D saveToFile)
1933      * in the way it does not add new line character at the end of the last line.
1934      * See_Also: $(D saveToFile), $(D save)
1935      */
1936     @trusted final string saveToString(const WriteOptions options = WriteOptions.exact) const {
1937         auto a = appender!(string[])();
1938         save(a, options);
1939         return a.data.join("\n");
1940     }
1941 
1942     ///
1943     unittest
1944     {
1945         string contents =
1946 `
1947 # Leading comment
1948 [First group]
1949 # Comment inside
1950 Key=Value
1951 [Second group]
1952 
1953 Key=Value
1954 
1955 [Third group]
1956 Key=Value`;
1957 
1958         auto ilf = new IniLikeFile(iniLikeStringReader(contents));
1959         assert(ilf.saveToString(WriteOptions.exact) == contents);
1960 
1961         assert(ilf.saveToString(WriteOptions.pretty) ==
1962 `# Leading comment
1963 [First group]
1964 # Comment inside
1965 Key=Value
1966 
1967 [Second group]
1968 Key=Value
1969 
1970 [Third group]
1971 Key=Value`);
1972 
1973         assert(ilf.saveToString(WriteOptions(No.preserveComments, No.preserveEmptyLines)) ==
1974 `[First group]
1975 Key=Value
1976 [Second group]
1977 Key=Value
1978 [Third group]
1979 Key=Value`);
1980 
1981         assert(ilf.saveToString(WriteOptions(No.preserveComments, No.preserveEmptyLines, Yes.lineBetweenGroups)) ==
1982 `[First group]
1983 Key=Value
1984 
1985 [Second group]
1986 Key=Value
1987 
1988 [Third group]
1989 Key=Value`);
1990     }
1991 
1992     /**
1993      * Use Output range or delegate to retrieve strings line by line.
1994      * Those strings can be written to the file or be showed in text area.
1995      * Note: Output strings don't have trailing newline character.
1996      * See_Also: $(D saveToFile), $(D saveToString)
1997      */
1998     final void save(OutRange)(OutRange sink, const WriteOptions options = WriteOptions.exact) const if (isOutputRange!(OutRange, string)) {
1999         foreach(line; leadingComments()) {
2000             if (options.preserveComments) {
2001                 if (line.empty && !options.preserveEmptyLines) {
2002                     continue;
2003                 }
2004                 put(sink, line);
2005             }
2006         }
2007         bool firstGroup = true;
2008         bool lastWasEmpty = false;
2009 
2010         foreach(group; byGroup()) {
2011             if (!firstGroup && !lastWasEmpty && options.lineBetweenGroups) {
2012                 put(sink, "");
2013             }
2014 
2015             put(sink, "[" ~ group.groupName ~ "]");
2016             foreach(line; group.byIniLine()) {
2017                 lastWasEmpty = false;
2018                 if (line.type == IniLikeLine.Type.Comment) {
2019                     if (!options.preserveComments) {
2020                         continue;
2021                     }
2022                     if (line.comment.empty) {
2023                         if (!options.preserveEmptyLines) {
2024                             continue;
2025                         }
2026                         lastWasEmpty = true;
2027                     }
2028                     put(sink, line.comment);
2029                 } else if (line.type == IniLikeLine.Type.KeyValue) {
2030                     put(sink, line.key ~ "=" ~ line.value);
2031                 }
2032             }
2033             firstGroup = false;
2034         }
2035     }
2036 
2037     /**
2038      * File path where the object was loaded from.
2039      * Returns: File name as was specified on the object creation.
2040      */
2041     @nogc @safe final string fileName() nothrow const pure {
2042         return _fileName;
2043     }
2044 
2045     /**
2046      * Leading comments.
2047      * Returns: Range of leading comments (before any group)
2048      * See_Also: $(D appendLeadingComment), $(D prependLeadingComment), $(D clearLeadingComments)
2049      */
2050     @nogc @safe final auto leadingComments() const nothrow pure {
2051         return _leadingComments;
2052     }
2053 
2054     ///
2055     unittest
2056     {
2057         auto ilf = new IniLikeFile();
2058         assert(ilf.appendLeadingComment("First") == "#First");
2059         assert(ilf.appendLeadingComment("#Second") == "#Second");
2060         assert(ilf.appendLeadingComment("Sneaky\nKey=Value") == "#Sneaky Key=Value");
2061         assert(ilf.appendLeadingComment("# New Line\n") == "# New Line");
2062         assert(ilf.appendLeadingComment("") == "");
2063         assert(ilf.appendLeadingComment("\n") == "");
2064         assert(ilf.prependLeadingComment("Shebang") == "#Shebang");
2065         assert(ilf.leadingComments().equal(["#Shebang", "#First", "#Second", "#Sneaky Key=Value", "# New Line", "", ""]));
2066         ilf.clearLeadingComments();
2067         assert(ilf.leadingComments().empty);
2068     }
2069 
2070     /**
2071      * Add leading comment. This will be appended to the list of leadingComments.
2072      * Note: # will be prepended automatically if line is not empty and does not have # at the start.
2073      *  The last new line character will be removed if present. Others will be replaced with whitespaces.
2074      * Returns: Line that was added as comment.
2075      * See_Also: $(D leadingComments), $(D prependLeadingComment)
2076      */
2077     @safe final string appendLeadingComment(string line) nothrow pure {
2078         line = makeComment(line);
2079         _leadingComments ~= line;
2080         return line;
2081     }
2082 
2083     /**
2084      * Prepend leading comment (e.g. for setting shebang line).
2085      * Returns: Line that was added as comment.
2086      * See_Also: $(D leadingComments), $(D appendLeadingComment)
2087      */
2088     @safe final string prependLeadingComment(string line) nothrow pure {
2089         line = makeComment(line);
2090         _leadingComments = line ~ _leadingComments;
2091         return line;
2092     }
2093 
2094     /**
2095      * Remove all coments met before groups.
2096      * See_Also: $(D leadingComments)
2097      */
2098     @nogc final @safe void clearLeadingComments() nothrow {
2099         _leadingComments = null;
2100     }
2101 
2102     /**
2103      * Move the group to make it the first.
2104      */
2105     @trusted final void moveGroupToFront(GroupNode toMove) nothrow pure {
2106         _listMap.moveToFront(toMove.node);
2107     }
2108 
2109     /**
2110      * Move the group to make it the last.
2111      */
2112     @trusted final void moveGroupToBack(GroupNode toMove) nothrow pure {
2113         _listMap.moveToBack(toMove.node);
2114     }
2115 
2116     /**
2117      * Move group before other.
2118      */
2119     @trusted final void moveGroupBefore(GroupNode other, GroupNode toMove) nothrow pure {
2120         _listMap.moveBefore(other.node, toMove.node);
2121     }
2122 
2123     /**
2124      * Move group after other.
2125      */
2126     @trusted final void moveGroupAfter(GroupNode other, GroupNode toMove) nothrow pure {
2127         _listMap.moveAfter(other.node, toMove.node);
2128     }
2129 
2130     @safe final ReadOptions readOptions() nothrow const pure {
2131         return _readOptions;
2132     }
2133 
2134     /**
2135      * Shortcut to $(D IniLikeGroup.escapedValue) of given group.
2136      * Returns null if the group does not exist.
2137      */
2138     @nogc @safe final string escapedValue(string groupName, string key)
2139     {
2140         auto g = group(groupName);
2141         if (g) {
2142             return g.escapedValue(key);
2143         } else {
2144             return null;
2145         }
2146     }
2147 
2148     /// ditto, localized version
2149     @safe final string escapedValue(string groupName, string key, string locale, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback)
2150     {
2151         auto g = group(groupName);
2152         if (g) {
2153             return g.escapedValue(key, locale, nonLocaleFallback);
2154         } else {
2155             return null;
2156         }
2157     }
2158 
2159     /**
2160      * Shortcut to $(D IniLikeGroup.unescapedValue) of given group.
2161      * Returns null if the group does not exist.
2162      */
2163     @safe final string unescapedValue(string groupName, string key, string locale = null, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback)
2164     {
2165         auto g = group(groupName);
2166         if (g) {
2167             return g.unescapedValue(key, locale, nonLocaleFallback);
2168         } else {
2169             return null;
2170         }
2171     }
2172 
2173 private:
2174     string _fileName;
2175     GroupListMap _listMap;
2176     string[] _leadingComments;
2177     ReadOptions _readOptions;
2178 }
2179 
2180 ///
2181 unittest
2182 {
2183     import std.file;
2184     import std.path;
2185     import std.stdio;
2186 
2187     string contents =
2188 `# The first comment
2189 [First Entry]
2190 # Comment
2191 GenericName=File manager
2192 GenericName[ru]=Файловый менеджер
2193 NeedUnescape=yes\\i\tneed
2194 NeedUnescape[ru]=да\\я\tнуждаюсь
2195 # Another comment
2196 [Another Group]
2197 Name=Commander
2198 Comment=Manage files
2199 # The last comment`;
2200 
2201     auto ilf = new IniLikeFile(iniLikeStringReader(contents), "contents.ini");
2202     assert(ilf.fileName() == "contents.ini");
2203     assert(equal(ilf.leadingComments(), ["# The first comment"]));
2204     assert(ilf.group("First Entry"));
2205     assert(ilf.group("Another Group"));
2206     assert(ilf.getNode("Another Group").group is ilf.group("Another Group"));
2207     assert(ilf.group("NonExistent") is null);
2208     assert(ilf.getNode("NonExistent").isNull());
2209     assert(ilf.getNode("NonExistent").key() is null);
2210     assert(ilf.getNode("NonExistent").group() is null);
2211     assert(ilf.saveToString(IniLikeFile.WriteOptions.exact) == contents);
2212 
2213     version(inilikeFileTest)
2214     {
2215         string tempFile = buildPath(tempDir(), "inilike-unittest-tempfile");
2216         try {
2217             assertNotThrown!IniLikeReadException(ilf.saveToFile(tempFile));
2218             auto fileContents = cast(string)std.file.read(tempFile);
2219             static if( __VERSION__ < 2067 ) {
2220                 assert(equal(fileContents.splitLines, contents.splitLines), "Contents should be preserved as is");
2221             } else {
2222                 assert(equal(fileContents.lineSplitter, contents.lineSplitter), "Contents should be preserved as is");
2223             }
2224 
2225             IniLikeFile filf;
2226             assertNotThrown!IniLikeReadException(filf = new IniLikeFile(tempFile));
2227             assert(filf.fileName() == tempFile);
2228             remove(tempFile);
2229         } catch(Exception e) {
2230             //environmental error in unittests
2231         }
2232     }
2233 
2234     assert(ilf.escapedValue("NonExistent", "Key") is null);
2235     assert(ilf.escapedValue("NonExistent", "Key", "ru") is null);
2236     assert(ilf.unescapedValue("NonExistent", "Key") is null);
2237 
2238     auto firstEntry = ilf.group("First Entry");
2239 
2240     assert(!firstEntry.contains("NonExistent"));
2241     assert(firstEntry.contains("GenericName"));
2242     assert(firstEntry.contains("GenericName[ru]"));
2243     assert(firstEntry.byNode().filter!(node => node.isNull()).empty);
2244     assert(firstEntry.escapedValue("GenericName") == "File manager");
2245     assert(firstEntry.getNode("GenericName").key == "GenericName");
2246     assert(firstEntry.getNode("NonExistent").key is null);
2247     assert(firstEntry.getNode("NonExistent").line.type == IniLikeLine.Type.None);
2248 
2249     assert(firstEntry.escapedValue("NeedUnescape") == `yes\\i\tneed`);
2250     assert(ilf.escapedValue("First Entry", "NeedUnescape") == `yes\\i\tneed`);
2251 
2252     assert(firstEntry.unescapedValue("NeedUnescape") == "yes\\i\tneed");
2253     assert(ilf.unescapedValue("First Entry", "NeedUnescape") == "yes\\i\tneed");
2254 
2255     assert(firstEntry.escapedValue("NeedUnescape", "ru") == `да\\я\tнуждаюсь`);
2256     assert(ilf.escapedValue("First Entry", "NeedUnescape", "ru") == `да\\я\tнуждаюсь`);
2257 
2258     assert(firstEntry.unescapedValue("NeedUnescape", "ru") == "да\\я\tнуждаюсь");
2259     assert(ilf.unescapedValue("First Entry", "NeedUnescape", "ru") == "да\\я\tнуждаюсь");
2260 
2261     firstEntry.setUnescapedValue("NeedEscape", "i\rneed\nescape");
2262     assert(firstEntry.escapedValue("NeedEscape") == `i\rneed\nescape`);
2263     firstEntry.setUnescapedValue("NeedEscape", "ru", "мне\rнужно\nэкранирование");
2264     assert(firstEntry.escapedValue("NeedEscape", "ru") == `мне\rнужно\nэкранирование`);
2265 
2266     firstEntry.setEscapedValue("GenericName", "Manager of files");
2267     assert(firstEntry.escapedValue("GenericName") == "Manager of files");
2268     firstEntry.setEscapedValue("Authors", "Unknown");
2269     assert(firstEntry.escapedValue("Authors") == "Unknown");
2270     firstEntry.getNode("Authors").setEscapedValue("Known");
2271     assert(firstEntry.escapedValue("Authors") == "Known");
2272 
2273     assert(firstEntry.escapedValue("GenericName", "ru") == "Файловый менеджер");
2274     firstEntry.setEscapedValue("GenericName", "ru", "Менеджер файлов");
2275     assert(firstEntry.escapedValue("GenericName", "ru") == "Менеджер файлов");
2276     firstEntry.setEscapedValue("Authors", "ru", "Неизвестны");
2277     assert(firstEntry.escapedValue("Authors", "ru") == "Неизвестны");
2278 
2279     firstEntry.removeEntry("GenericName");
2280     assert(!firstEntry.contains("GenericName"));
2281     firstEntry.removeEntry("GenericName", "ru");
2282     assert(!firstEntry.contains("GenericName[ru]"));
2283     firstEntry.setEscapedValue("GenericName", "File Manager");
2284     assert(firstEntry.escapedValue("GenericName") == "File Manager");
2285 
2286     assert(ilf.group("Another Group").escapedValue("Name") == "Commander");
2287     assert(equal(ilf.group("Another Group").byKeyValue(), [ keyValueTuple("Name", "Commander"), keyValueTuple("Comment", "Manage files") ]));
2288 
2289     auto latestCommentNode = ilf.group("Another Group").appendComment("The lastest comment");
2290     assert(latestCommentNode.line.comment == "#The lastest comment");
2291     latestCommentNode.setEscapedValue("The latest comment");
2292     assert(latestCommentNode.line.comment == "#The latest comment");
2293     assert(ilf.group("Another Group").prependComment("The first comment").line.comment == "#The first comment");
2294 
2295     assert(equal(
2296         ilf.group("Another Group").byIniLine(),
2297         [IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"), IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment"), IniLikeLine.fromComment("#The latest comment")]
2298     ));
2299 
2300     auto nameLineNode = ilf.group("Another Group").getNode("Name");
2301     assert(nameLineNode.line.value == "Commander");
2302     auto commentLineNode = ilf.group("Another Group").getNode("Comment");
2303     assert(commentLineNode.line.value == "Manage files");
2304 
2305     ilf.group("Another Group").addCommentAfter(nameLineNode, "Middle comment");
2306     ilf.group("Another Group").addCommentBefore(commentLineNode, "Average comment");
2307 
2308     assert(equal(
2309         ilf.group("Another Group").byIniLine(),
2310         [
2311             IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"),
2312             IniLikeLine.fromComment("#Middle comment"), IniLikeLine.fromComment("#Average comment"),
2313             IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment"), IniLikeLine.fromComment("#The latest comment")
2314         ]
2315     ));
2316 
2317     ilf.group("Another Group").removeEntry(latestCommentNode);
2318 
2319     assert(equal(
2320         ilf.group("Another Group").byIniLine(),
2321         [
2322             IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"),
2323             IniLikeLine.fromComment("#Middle comment"), IniLikeLine.fromComment("#Average comment"),
2324             IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment")
2325         ]
2326     ));
2327 
2328     assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry", "Another Group"]));
2329 
2330     assert(!ilf.removeGroup("NonExistent Group"));
2331 
2332     assert(ilf.removeGroup("Another Group"));
2333     assert(!ilf.group("Another Group"));
2334     assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry"]));
2335 
2336     ilf.addGenericGroup("Another Group");
2337     assert(ilf.group("Another Group"));
2338     assert(ilf.group("Another Group").byIniLine().empty);
2339     assert(ilf.group("Another Group").byKeyValue().empty);
2340 
2341     assertThrown(ilf.addGenericGroup("Another Group"));
2342 
2343     ilf.addGenericGroup("Other Group");
2344     assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry", "Another Group", "Other Group"]));
2345 
2346     assertThrown!IniLikeException(ilf.addGenericGroup(""));
2347 
2348     import std.range : isForwardRange;
2349 
2350     const IniLikeFile cilf = ilf;
2351     static assert(isForwardRange!(typeof(cilf.byGroup())));
2352     static assert(isForwardRange!(typeof(cilf.group("First Entry").byKeyValue())));
2353     static assert(isForwardRange!(typeof(cilf.group("First Entry").byIniLine())));
2354 
2355     contents =
2356 `[Group]
2357 GenericName=File manager
2358 [Group]
2359 GenericName=Commander`;
2360 
2361     auto shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents), "config.ini"));
2362     assert(shouldThrow !is null, "Duplicate groups should throw");
2363     assert(shouldThrow.lineNumber == 3);
2364     assert(shouldThrow.lineIndex == 2);
2365     assert(shouldThrow.fileName == "config.ini");
2366 
2367     contents =
2368 `[Group]
2369 Key=Value1
2370 Key=Value2`;
2371 
2372     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2373     assert(shouldThrow !is null, "Duplicate key should throw");
2374     assert(shouldThrow.lineNumber == 3);
2375 
2376     contents =
2377 `[Group]
2378 Key=Value
2379 =File manager`;
2380 
2381     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2382     assert(shouldThrow !is null, "Empty key should throw");
2383     assert(shouldThrow.lineNumber == 3);
2384 
2385     contents =
2386 `[Group]
2387 #Comment
2388 Valid=Key
2389 NotKeyNotGroupNotComment`;
2390 
2391     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2392     assert(shouldThrow !is null, "Invalid entry should throw");
2393     assert(shouldThrow.lineNumber == 4);
2394 
2395     contents =
2396 `#Comment
2397 NotComment
2398 [Group]
2399 Valid=Key`;
2400     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2401     assert(shouldThrow !is null, "Invalid comment should throw");
2402     assert(shouldThrow.lineNumber == 2);
2403 
2404 
2405     contents = `# The leading comment
2406 [One]
2407 # Comment1
2408 Key1=Value1
2409 Key2=Value2
2410 Key3=Value3
2411 [Two]
2412 Key1=Value1
2413 Key2=Value2
2414 Key3=Value3
2415 # Comment2
2416 [Three]
2417 Key1=Value1
2418 Key2=Value2
2419 # Comment3
2420 Key3=Value3`;
2421 
2422     ilf = new IniLikeFile(iniLikeStringReader(contents));
2423 
2424     ilf.moveGroupToFront(ilf.getNode("Two"));
2425     assert(ilf.byNode().map!(g => g.key).equal(["Two", "One", "Three"]));
2426 
2427     ilf.moveGroupToBack(ilf.getNode("One"));
2428     assert(ilf.byNode().map!(g => g.key).equal(["Two", "Three", "One"]));
2429 
2430     ilf.moveGroupBefore(ilf.getNode("Two"), ilf.getNode("Three"));
2431     assert(ilf.byGroup().map!(g => g.groupName).equal(["Three", "Two", "One"]));
2432 
2433     ilf.moveGroupAfter(ilf.getNode("Three"), ilf.getNode("One"));
2434     assert(ilf.byGroup().map!(g => g.groupName).equal(["Three", "One", "Two"]));
2435 
2436     auto groupOne = ilf.group("One");
2437     groupOne.moveLineToFront(groupOne.getNode("Key3"));
2438     groupOne.moveLineToBack(groupOne.getNode("Key1"));
2439 
2440     assert(groupOne.byIniLine().equal([
2441         IniLikeLine.fromKeyValue("Key3", "Value3"), IniLikeLine.fromComment("# Comment1"),
2442         IniLikeLine.fromKeyValue("Key2", "Value2"), IniLikeLine.fromKeyValue("Key1", "Value1")
2443     ]));
2444 
2445     auto groupTwo = ilf.group("Two");
2446     groupTwo.moveLineBefore(groupTwo.getNode("Key1"), groupTwo.getNode("Key3"));
2447     groupTwo.moveLineAfter(groupTwo.getNode("Key2"), groupTwo.getNode("Key1"));
2448 
2449     assert(groupTwo.byIniLine().equal([
2450         IniLikeLine.fromKeyValue("Key3", "Value3"), IniLikeLine.fromKeyValue("Key2", "Value2"),
2451          IniLikeLine.fromKeyValue("Key1", "Value1"), IniLikeLine.fromComment("# Comment2")
2452     ]));
2453 }