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 }