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 }