1 /**
2  * Reading and writing ini-like files used in some Unix systems and Freedesktop specifications.
3  * ini-like is informal name for the file format that look like this:
4  * ---
5 # Comment
6 [Group name]
7 Key=Value
8 # Comment inside group
9 AnotherKey=Value
10 
11 [Another group]
12 Key=English value
13 Key[fr_FR]=Francais value
14 
15  * ---
16  * Authors: 
17  *  $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov)
18  * Copyright:
19  *  Roman Chistokhodov, 2015-2016
20  * License: 
21  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
22  * See_Also: 
23  *  $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
24  */
25 
26 module inilike;
27 
28 public import inilike.common;
29 public import inilike.range;
30 public import inilike.file;
31 
32 unittest
33 {
34     import std.exception;
35     
36     final class DesktopEntry : IniLikeGroup
37     {
38         this() {
39             super("Desktop Entry");
40         }
41     protected:
42         @trusted override void validateKey(string key, string value) const {
43             if (!isValidKey(key)) {
44                 throw new IniLikeEntryException("key is invalid", groupName(), key, value);
45             }
46         }
47     }
48 
49     final class DesktopFile : IniLikeFile
50     {
51         //Flags to manage .ini like file reading
52         enum ReadOptions
53         {
54             noOptions = 0,              // Read all groups, skip comments and empty lines, stop on any error.
55             preserveComments = 2,       // Preserve comments and empty lines. Use this when you want to keep them across writing.
56             ignoreGroupDuplicates = 4,  // Ignore group duplicates. The first found will be used.
57             ignoreInvalidKeys = 8,      // Skip invalid keys during parsing.
58             ignoreKeyDuplicates = 16,   // Ignore key duplicates. The first found will be used.
59             ignoreUnknownGroups = 32,   // Don't throw on unknown groups. Still save them.
60             skipUnknownGroups = 64,     // Don't save unknown groups.
61             skipExtensionGroups = 128   // Skip groups started with X-
62         }
63         
64         @trusted this(IniLikeReader)(IniLikeReader reader, ReadOptions options = ReadOptions.noOptions)
65         {
66             _options = options;
67             super(reader);
68             enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0));
69             _options = ReadOptions.noOptions;
70         }
71         
72         @safe override bool removeGroup(string groupName) nothrow {
73             if (groupName == "Desktop Entry") {
74                 return false;
75             }
76             return super.removeGroup(groupName);
77         }
78         
79         @trusted override string appendLeadingComment(string line) nothrow {
80             if (_options & ReadOptions.preserveComments) {
81                 return super.appendLeadingComment(line);
82             }
83             return null;
84         }
85         
86     protected:
87         @trusted override void addCommentForGroup(string comment, IniLikeGroup currentGroup, string groupName)
88         {
89             if (currentGroup && (_options & ReadOptions.preserveComments)) {
90                 currentGroup.appendComment(comment);
91             }
92         }
93         
94         @trusted override void addKeyValueForGroup(string key, string value, IniLikeGroup currentGroup, string groupName)
95         {
96             if (currentGroup) {
97                 if (!isValidKey(key) && (_options & ReadOptions.ignoreInvalidKeys)) {
98                     return;
99                 }
100                 if (currentGroup.contains(key)) {
101                     if (_options & ReadOptions.ignoreKeyDuplicates) {
102                         return;
103                     } else {
104                         throw new Exception("key already exists");
105                     }
106                 }
107                 currentGroup[key] = value;
108             }
109         }
110         
111         @trusted override IniLikeGroup createGroup(string groupName)
112         {
113             if (group(groupName) !is null) {
114                 if (_options & ReadOptions.ignoreGroupDuplicates) {
115                     return null;
116                 } else {
117                     throw new Exception("group already exists");
118                 }
119             }
120             
121             if (groupName == "Desktop Entry") {
122                 _desktopEntry = new DesktopEntry();
123                 return _desktopEntry;
124             } else if (groupName.startsWith("X-")) {
125                 if (_options & ReadOptions.skipExtensionGroups) {
126                     return null;
127                 }
128                 return createEmptyGroup(groupName);
129             } else {
130                 if (_options & ReadOptions.ignoreUnknownGroups) {
131                     if (_options & ReadOptions.skipUnknownGroups) {
132                         return null;
133                     } else {
134                         return createEmptyGroup(groupName);
135                     }
136                 } else {
137                     throw new Exception("Unknown group");
138                 }
139             }
140         }
141         
142         inout(DesktopEntry) desktopEntry() inout {
143             return _desktopEntry;
144         }
145         
146     private:
147         DesktopEntry _desktopEntry;
148         ReadOptions _options;
149     }
150     
151     string contents = 
152 `# First comment
153 [Desktop Entry]
154 Key=Value
155 # Comment in group`;
156 
157     auto df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions);
158     assert(!df.removeGroup("Desktop Entry"));
159     assert(!df.removeGroup("NonExistent"));
160     assert(df.group("Desktop Entry") !is null);
161     assert(df.desktopEntry() !is null);
162     assert(df.leadingComments().empty);
163     assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value")]));
164     
165     df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.preserveComments);
166     assert(equal(df.leadingComments(), ["# First comment"]));
167     assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromComment("# Comment in group")]));
168     
169     contents = 
170 `[X-SomeGroup]
171 Key=Value`;
172 
173     auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions));
174     assert(thrown !is null);
175     assert(thrown.lineNumber == 0);
176     
177     contents = 
178 `[Desktop Entry]
179 Valid=Key
180 $=Invalid`;
181 
182     thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions));
183     assert(thrown !is null);
184     assert(thrown.entryException !is null);
185     assert(thrown.entryException.key == "$");
186     assert(thrown.entryException.value == "Invalid");
187     
188     assertNotThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreInvalidKeys));
189     
190     contents = 
191 `[Desktop Entry]
192 Key=Value1
193 Key=Value2`;
194 
195     assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions));
196     assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreKeyDuplicates));
197     assert(df.desktopEntry().value("Key") == "Value1");
198     
199     contents = 
200 `[Desktop Entry]
201 Name=Name
202 [Unknown]
203 Key=Value`;
204 
205     assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions));
206     assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreUnknownGroups));
207     assert(df.group("Unknown") !is null);
208     
209     df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreUnknownGroups|DesktopFile.ReadOptions.skipUnknownGroups);
210     assert(df.group("Unknown") is null);
211     
212     contents = 
213 `[Desktop Entry]
214 Name=Name1
215 [Desktop Entry]
216 Name=Name2`;
217     
218     assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions));
219     assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreGroupDuplicates));
220     
221     assert(df.desktopEntry().value("Name") == "Name1");
222     
223     contents = 
224 `[Desktop Entry]
225 Name=Name1
226 [X-Extension]
227 Name=Name2`;
228 
229     df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.skipExtensionGroups);
230     assert(df.group("X-Extension") is null);
231 }