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