1 /**
2  * Reading and writing ini-like files used in some Unix systems and Freedesktop specifications.
3  *
4  * ini-like is informal name for the file format that look like this:
5  * ---
6 # Comment
7 [Group name]
8 Key=Value
9 # Comment inside group
10 AnotherKey=Value
11 
12 [Another group]
13 Key=English value
14 Key[fr_FR]=Francais value
15 
16  * ---
17  * To work with ini-like files correctly it's essential to understand the difference between escaped values and unescaped ones.
18  * Escaping is needed to represent new line characters in values.
19  * ---
20 NewLine="\n"
21 Slash="\\"
22 CarriageReturn="\r"
23  * ---
24  *
25  * In $(D inilike.file.IniLikeGroup) internally all values are stored in the escaped form.
26  * This is 'true' form of values as this is how they are stored in the file.
27  *
28  * Authors:
29  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
30  * Copyright:
31  *  Roman Chistokhodov, 2015-2017
32  * License:
33  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
34  * See_Also:
35  *  $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
36  */
37 
38 module inilike;
39 
40 public import inilike.common;
41 public import inilike.range;
42 public import inilike.file;
43 
44 unittest
45 {
46     import std.exception;
47 
48     final class DesktopEntry : IniLikeGroup
49     {
50         this() {
51             super("Desktop Entry");
52         }
53     protected:
54         @trusted override void validateKey(string key, string value) const {
55             if (!isValidDesktopFileKey(key)) {
56                 throw new IniLikeEntryException("key is invalid", groupName(), key, value);
57             }
58         }
59     }
60 
61     final class DesktopFile : IniLikeFile
62     {
63         //Options to manage .ini like file reading
64         static struct DesktopReadOptions
65         {
66             IniLikeFile.ReadOptions baseOptions;
67 
68             alias baseOptions this;
69 
70             bool skipExtensionGroups;
71             bool ignoreUnknownGroups;
72             bool skipUnknownGroups;
73         }
74 
75         @trusted this(IniLikeReader)(IniLikeReader reader, DesktopReadOptions options = DesktopReadOptions.init)
76         {
77             _options = options;
78             super(reader, null, options.baseOptions);
79             enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0));
80         }
81 
82         @safe override bool removeGroup(string groupName) nothrow {
83             if (groupName == "Desktop Entry") {
84                 return false;
85             }
86             return super.removeGroup(groupName);
87         }
88 
89     protected:
90         @trusted override IniLikeGroup createGroupByName(string groupName)
91         {
92             if (groupName == "Desktop Entry") {
93                 _desktopEntry = new DesktopEntry();
94                 return _desktopEntry;
95             } else if (groupName.startsWith("X-")) {
96                 if (_options.skipExtensionGroups) {
97                     return null;
98                 }
99                 return createEmptyGroup(groupName);
100             } else {
101                 if (_options.ignoreUnknownGroups) {
102                     if (_options.skipUnknownGroups) {
103                         return null;
104                     } else {
105                         return createEmptyGroup(groupName);
106                     }
107                 } else {
108                     throw new IniLikeException("Unknown group");
109                 }
110             }
111         }
112 
113         inout(DesktopEntry) desktopEntry() inout {
114             return _desktopEntry;
115         }
116 
117     private:
118         DesktopEntry _desktopEntry;
119         DesktopReadOptions _options;
120     }
121 
122     string contents =
123 `# First comment
124 [Desktop Entry]
125 Key=Value
126 # Comment in group`;
127     DesktopFile.DesktopReadOptions options;
128 
129     auto df = new DesktopFile(iniLikeStringReader(contents), options);
130     assert(!df.removeGroup("Desktop Entry"));
131     assert(!df.removeGroup("NonExistent"));
132     assert(df.group("Desktop Entry") !is null);
133     assert(df.desktopEntry() !is null);
134     assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromComment("# Comment in group")]));
135     assert(equal(df.leadingComments(), ["# First comment"]));
136 
137     assertThrown(df.desktopEntry().setUnescapedValue("$Invalid", "Valid value"));
138 
139     IniLikeEntryException entryException;
140     try {
141         df.desktopEntry().setUnescapedValue("$Invalid", "Valid value");
142     } catch(IniLikeEntryException e) {
143         entryException = e;
144     }
145     assert(entryException !is null);
146     df.desktopEntry().setUnescapedValue("$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.save);
147     assert(df.desktopEntry().escapedValue("$Invalid") == "Valid value");
148 
149     assert(df.desktopEntry().appendValue("Another$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.skip).isNull());
150     assert(df.desktopEntry().setEscapedValue("Another$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.skip) is null);
151     assert(df.desktopEntry().escapedValue("Another$Invalid") is null);
152 
153     contents =
154 `[X-SomeGroup]
155 Key=Value`;
156 
157     auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents)));
158     assert(thrown !is null);
159     assert(thrown.lineNumber == 0);
160 
161     contents =
162 `[Desktop Entry]
163 Valid=Key
164 $=Invalid`;
165 
166     thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents)));
167     assert(thrown !is null);
168     assert(thrown.entryException !is null);
169     assert(thrown.entryException.key == "$");
170     assert(thrown.entryException.value == "Invalid");
171 
172     options = DesktopFile.DesktopReadOptions.init;
173     options.invalidKeyPolicy = IniLikeGroup.InvalidKeyPolicy.skip;
174     assertNotThrown(new DesktopFile(iniLikeStringReader(contents), options));
175 
176     contents =
177 `[Desktop Entry]
178 Name=Name
179 [Unknown]
180 Key=Value`;
181 
182     assertThrown(new DesktopFile(iniLikeStringReader(contents)));
183 
184     options = DesktopFile.DesktopReadOptions.init;
185     options.ignoreUnknownGroups = true;
186 
187     assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), options));
188     assert(df.group("Unknown") !is null);
189 
190     options.skipUnknownGroups = true;
191     df = new DesktopFile(iniLikeStringReader(contents), options);
192     assert(df.group("Unknown") is null);
193 
194     contents =
195 `[Desktop Entry]
196 Name=Name1
197 [X-Extension]
198 Name=Name2`;
199 
200     options = DesktopFile.DesktopReadOptions.init;
201     options.skipExtensionGroups = true;
202 
203     df = new DesktopFile(iniLikeStringReader(contents), options);
204     assert(df.group("X-Extension") is null);
205 }