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 }