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 }