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/FreeSlave, 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 (!isValidDesktopFileKey(key)) {
44                 throw new IniLikeEntryException("key is invalid", groupName(), key, value);
45             }
46         }
47     }
48 
49     final class DesktopFile : IniLikeFile
50     {
51         //Options to manage .ini like file reading
52         static struct DesktopReadOptions
53         {
54             IniLikeFile.ReadOptions baseOptions;
55 
56             alias baseOptions this;
57 
58             bool skipExtensionGroups;
59             bool ignoreUnknownGroups;
60             bool skipUnknownGroups;
61         }
62 
63         @trusted this(IniLikeReader)(IniLikeReader reader, DesktopReadOptions options = DesktopReadOptions.init)
64         {
65             _options = options;
66             super(reader, null, options.baseOptions);
67             enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0));
68         }
69 
70         @safe override bool removeGroup(string groupName) nothrow {
71             if (groupName == "Desktop Entry") {
72                 return false;
73             }
74             return super.removeGroup(groupName);
75         }
76 
77     protected:
78         @trusted override IniLikeGroup createGroupByName(string groupName)
79         {
80             if (groupName == "Desktop Entry") {
81                 _desktopEntry = new DesktopEntry();
82                 return _desktopEntry;
83             } else if (groupName.startsWith("X-")) {
84                 if (_options.skipExtensionGroups) {
85                     return null;
86                 }
87                 return createEmptyGroup(groupName);
88             } else {
89                 if (_options.ignoreUnknownGroups) {
90                     if (_options.skipUnknownGroups) {
91                         return null;
92                     } else {
93                         return createEmptyGroup(groupName);
94                     }
95                 } else {
96                     throw new IniLikeException("Unknown group");
97                 }
98             }
99         }
100 
101         inout(DesktopEntry) desktopEntry() inout {
102             return _desktopEntry;
103         }
104 
105     private:
106         DesktopEntry _desktopEntry;
107         DesktopReadOptions _options;
108     }
109 
110     string contents =
111 `# First comment
112 [Desktop Entry]
113 Key=Value
114 # Comment in group`;
115     DesktopFile.DesktopReadOptions options;
116 
117     auto df = new DesktopFile(iniLikeStringReader(contents), options);
118     assert(!df.removeGroup("Desktop Entry"));
119     assert(!df.removeGroup("NonExistent"));
120     assert(df.group("Desktop Entry") !is null);
121     assert(df.desktopEntry() !is null);
122     assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromComment("# Comment in group")]));
123     assert(equal(df.leadingComments(), ["# First comment"]));
124 
125     assertThrown(df.desktopEntry().writeEntry("$Invalid", "Valid value"));
126 
127     IniLikeEntryException entryException;
128     try {
129         df.desktopEntry().writeEntry("$Invalid", "Valid value");
130     } catch(IniLikeEntryException e) {
131         entryException = e;
132     }
133     assert(entryException !is null);
134     df.desktopEntry().writeEntry("$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.save);
135     assert(df.desktopEntry().value("$Invalid") == "Valid value");
136 
137     assert(df.desktopEntry().appendValue("Another$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.skip).isNull());
138     assert(df.desktopEntry().setValue("Another$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.skip) is null);
139     assert(df.desktopEntry().value("Another$Invalid") is null);
140 
141     contents =
142 `[X-SomeGroup]
143 Key=Value`;
144 
145     auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents)));
146     assert(thrown !is null);
147     assert(thrown.lineNumber == 0);
148 
149     contents =
150 `[Desktop Entry]
151 Valid=Key
152 $=Invalid`;
153 
154     thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents)));
155     assert(thrown !is null);
156     assert(thrown.entryException !is null);
157     assert(thrown.entryException.key == "$");
158     assert(thrown.entryException.value == "Invalid");
159 
160     options = DesktopFile.DesktopReadOptions.init;
161     options.invalidKeyPolicy = IniLikeGroup.InvalidKeyPolicy.skip;
162     assertNotThrown(new DesktopFile(iniLikeStringReader(contents), options));
163 
164     contents =
165 `[Desktop Entry]
166 Name=Name
167 [Unknown]
168 Key=Value`;
169 
170     assertThrown(new DesktopFile(iniLikeStringReader(contents)));
171 
172     options = DesktopFile.DesktopReadOptions.init;
173     options.ignoreUnknownGroups = true;
174 
175     assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), options));
176     assert(df.group("Unknown") !is null);
177 
178     options.skipUnknownGroups = true;
179     df = new DesktopFile(iniLikeStringReader(contents), options);
180     assert(df.group("Unknown") is null);
181 
182     contents =
183 `[Desktop Entry]
184 Name=Name1
185 [X-Extension]
186 Name=Name2`;
187 
188     options = DesktopFile.DesktopReadOptions.init;
189     options.skipExtensionGroups = true;
190 
191     df = new DesktopFile(iniLikeStringReader(contents), options);
192     assert(df.group("X-Extension") is null);
193 }