1 module d_properties.reader; 2 3 import std.stdio; 4 import std.file : exists, isFile, readText; 5 import std.range; 6 import std.conv : to; 7 import std.uni; 8 import std.format : format; 9 import std.regex : replaceAll, regex; 10 import std.string : strip; 11 import object : Exception; 12 import d_properties.properties : Properties; 13 14 /** 15 * This exception is thrown when a properties file cannot be parsed. 16 */ 17 class PropertiesParseException : Exception { 18 public const string filename; 19 public const uint lineNumber; 20 21 /** 22 * Constructs a parse exception. 23 * Params: 24 * filename = The name of the file which gave a parse exception. 25 * lineNumber = The line number of the error, or -1 if none. 26 * message = The error message. 27 */ 28 this(string filename, uint lineNumber, string message) { 29 super(format( 30 "Error parsing file \"%s\"%s: %s", 31 filename, 32 (lineNumber > 0) ? " on line " ~ to!string(lineNumber) : "", 33 message 34 )); 35 this.filename = filename; 36 this.lineNumber = lineNumber; 37 } 38 } 39 40 /** 41 * Reads properties from a file. 42 * Params: 43 * filename = The name of the file to read. 44 * Returns: The properties that were read. 45 */ 46 public Properties readFromFile(string filename) { 47 if (!exists(filename) || !isFile(filename)) { 48 throw new PropertiesParseException(filename, -1, "File not found."); 49 } 50 Properties props; 51 char[] content = replaceAll(readText(filename).strip(), regex(r"\r\n"), "\n").dup; 52 uint lineNumber = 1; 53 while (!content.empty) { 54 while (content.front == '#' || content.front == '!') { 55 parseComment(content, lineNumber); 56 } 57 if (!content.empty) { 58 string key = parseKey(content, filename, lineNumber); 59 string value = parseValue(content, filename, lineNumber); 60 props[key] = value; 61 } 62 } 63 return props; 64 } 65 66 /** 67 * Parses and discards a comment line from the input. 68 * Params: 69 * content = The remaining file input. 70 * lineNumber = The current line number. 71 */ 72 private void parseComment(ref char[] content, ref uint lineNumber) { 73 if (content.empty) return; 74 dchar c = content.front; 75 if (c == '#' || c == '!') { 76 content.popFront; // Remove the comment char. 77 while (c != '\n' && !content.empty) { 78 c = content.front; 79 content.popFront; 80 } 81 lineNumber++; 82 } 83 } 84 85 /** 86 * Parses a property key from the input. 87 * Params: 88 * content = The remaining file content to parse. 89 * filename = The name of the file. 90 * lineNumber = The current line number. 91 * Returns: The key that was parsed. 92 */ 93 private string parseKey(ref char[] content, string filename, ref uint lineNumber) { 94 // Start by stripping away all whitespace before the start of the key. 95 while (content.front == ' ' || content.front == '\n' || content.front == '\t') { 96 content.popFront; 97 } 98 dchar c = content.front; 99 content.popFront; 100 dchar[] keyChars = [c]; 101 bool keyFound = false; 102 while (!keyFound) { 103 if (content.empty) throw new PropertiesParseException(filename, lineNumber, "Unexpected end of file while parsing key."); 104 c = content.front; 105 content.popFront; 106 // Detect the beginning of the separator. 107 if (keyChars[$ - 1] != '\\' && (c == ' ' || c == '=' || c == ':')) { 108 dchar separatorChar = ' '; 109 if (c != ' ') separatorChar = c; 110 while (c == ' ') { 111 if (content.empty) throw new PropertiesParseException(filename, lineNumber, "Unexpected end of file while parsing separator."); 112 c = content.front; 113 if (c == ' ' || c == '=' || c == ':') { 114 content.popFront; 115 if (c == '=' || c == ':') break; 116 } 117 } 118 // We have consumed as much whitespace as possible. If the separator char is a space, there's still the possibility to encounter a separator. 119 if (separatorChar == ' ' && (c == '=' || c == ':')) { 120 do { 121 if (content.empty) throw new PropertiesParseException(filename, lineNumber, "Unexpected end of file while parsing separator trailing whitespace."); 122 c = content.front; 123 if (c == ' ') content.popFront; 124 } while (c == ' '); 125 } 126 keyFound = true; 127 } else if (keyChars[$ - 1] == '\\' && (c == ' ' || c == '=' || c == ':')) { 128 keyChars[$ - 1] = c; 129 } else if (c == '\n' || c == '\t') { 130 throw new PropertiesParseException(filename, lineNumber, "Invalid key character."); 131 } else { 132 keyChars ~= c; 133 } 134 } 135 return to!string(keyChars); 136 } 137 138 /** 139 * Parses a property value from the input. 140 * Params: 141 * content = The remaining file content to parse. 142 * filename = The name of the file. 143 * lineNumber = The current line number. 144 * Returns: The value that was parsed. 145 */ 146 private string parseValue(ref char[] content, string filename, ref uint lineNumber) { 147 dchar[] valueChars = []; 148 bool valueFound = false; 149 while (!valueFound) { 150 if (content.empty) { // If we've reached the end of the file, return this as the last value. 151 return to!string(valueChars); 152 } 153 dchar c = content.front; 154 content.popFront; 155 // Check for some sort of escape sequence. 156 if (c == '\\') { 157 if (content.empty) throw new PropertiesParseException(filename, lineNumber, "Unexpected end of file while parsing escape sequence."); 158 dchar next = content.front; 159 content.popFront; 160 if (next == '\\') { 161 valueChars ~= '\\'; 162 } else if (next == '\n') { 163 lineNumber++; 164 do { 165 c = content.front; 166 if (c == ' ' || c == '\t') content.popFront; 167 } while (c == ' ' || c == '\t'); 168 } else if (next == 'u') { 169 valueChars ~= '\\'; 170 valueChars ~= 'u'; 171 for (int i = 0; i < 4; i++) { 172 if (content.empty || (!isAlphaNum(content.front))) throw new PropertiesParseException(filename, lineNumber, "Invalid unicode sequence."); 173 valueChars ~= content.front; 174 content.popFront; 175 } 176 } else { 177 throw new Error("Unknown escape sequence: \"\\" ~ to!string(next) ~ "\""); 178 } 179 } else if (c == '\n') { 180 valueFound = true; 181 lineNumber++; 182 } else { 183 valueChars ~= c; 184 } 185 } 186 return to!string(valueChars); 187 }