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