Mercurial > irccd
annotate common/ini.cpp @ 60:223487a685b1 release-2.0
Added tag 2.0.0 for changeset 92b0be5ce4b0
author | David Demelier <markand@malikania.fr> |
---|---|
date | Tue, 01 Mar 2016 08:53:05 +0100 |
parents | 03068f5ed79d |
children |
rev | line source |
---|---|
0 | 1 /* |
2 * ini.cpp -- .ini file parsing | |
3 * | |
4 * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr> | |
5 * | |
6 * Permission to use, copy, modify, and/or distribute this software for any | |
7 * purpose with or without fee is hereby granted, provided that the above | |
8 * copyright notice and this permission notice appear in all copies. | |
9 * | |
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
17 */ | |
18 | |
19 #include <cassert> | |
20 #include <cctype> | |
21 #include <cstring> | |
22 #include <iostream> | |
23 #include <iterator> | |
24 #include <fstream> | |
25 #include <sstream> | |
26 #include <stdexcept> | |
27 | |
28 #if defined(_WIN32) | |
29 # include <Shlwapi.h> // for PathIsRelative | |
30 #endif | |
31 | |
32 #include "ini.h" | |
33 | |
34 namespace { | |
35 | |
36 using namespace irccd; | |
37 using namespace irccd::ini; | |
38 | |
39 using StreamIterator = std::istreambuf_iterator<char>; | |
40 using TokenIterator = std::vector<Token>::const_iterator; | |
41 | |
42 inline bool isAbsolute(const std::string &path) noexcept | |
43 { | |
44 #if defined(_WIN32) | |
45 return !PathIsRelative(path.c_str()); | |
46 #else | |
47 return path.size() > 0 && path[0] == '/'; | |
48 #endif | |
49 } | |
50 | |
51 inline bool isQuote(char c) noexcept | |
52 { | |
53 return c == '\'' || c == '"'; | |
54 } | |
55 | |
56 inline bool isSpace(char c) noexcept | |
57 { | |
58 /* Custom version because std::isspace includes \n as space */ | |
59 return c == ' ' || c == '\t'; | |
60 } | |
61 | |
62 inline bool isList(char c) noexcept | |
63 { | |
64 return c == '(' || c == ')' || c == ','; | |
65 } | |
66 | |
67 inline bool isReserved(char c) noexcept | |
68 { | |
69 return isList(c) || isQuote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '='; | |
70 } | |
71 | |
72 void analyzeLine(int &line, int &column, StreamIterator &it) noexcept | |
73 { | |
74 assert(*it == '\n'); | |
75 | |
76 ++ line; | |
77 ++ it; | |
78 column = 0; | |
79 } | |
80 | |
81 void analyzeComment(int &column, StreamIterator &it, StreamIterator end) noexcept | |
82 { | |
83 assert(*it == '#'); | |
84 | |
85 while (it != end && *it != '\n') { | |
86 ++ column; | |
87 ++ it; | |
88 } | |
89 } | |
90 | |
91 void analyzeSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept | |
92 { | |
93 assert(isSpace(*it)); | |
94 | |
95 while (it != end && isSpace(*it)) { | |
96 ++ column; | |
97 ++ it; | |
98 } | |
99 } | |
100 | |
101 void analyzeList(Tokens &list, int line, int &column, StreamIterator &it) noexcept | |
102 { | |
103 assert(isList(*it)); | |
104 | |
105 switch (*it++) { | |
106 case '(': | |
107 list.emplace_back(Token::ListBegin, line, column++); | |
108 break; | |
109 case ')': | |
110 list.emplace_back(Token::ListEnd, line, column++); | |
111 break; | |
112 case ',': | |
113 list.emplace_back(Token::Comma, line, column++); | |
114 break; | |
115 default: | |
116 break; | |
117 } | |
118 } | |
119 | |
120 void analyzeSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) | |
121 { | |
122 assert(*it == '['); | |
123 | |
124 std::string value; | |
125 int save = column; | |
126 | |
127 /* Read section name */ | |
128 ++ it; | |
129 while (it != end && *it != ']') { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
130 if (*it == '\n') |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
131 throw Error(line, column, "section not terminated, missing ']'"); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
132 if (isReserved(*it)) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
133 throw Error(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'"); |
0 | 134 ++ column; |
135 value += *it++; | |
136 } | |
137 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
138 if (it == end) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
139 throw Error(line, column, "section name expected after '[', got <EOF>"); |
0 | 140 |
141 /* Remove ']' */ | |
142 ++ it; | |
143 | |
144 list.emplace_back(Token::Section, line, save, std::move(value)); | |
145 } | |
146 | |
147 void analyzeAssign(Tokens &list, int &line, int &column, StreamIterator &it) | |
148 { | |
149 assert(*it == '='); | |
150 | |
151 list.push_back({ Token::Assign, line, column++ }); | |
152 ++ it; | |
153 } | |
154 | |
155 void analyzeQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) | |
156 { | |
157 std::string value; | |
158 int save = column; | |
159 char quote = *it++; | |
160 | |
161 while (it != end && *it != quote) { | |
162 // TODO: escape sequence | |
163 ++ column; | |
164 value += *it++; | |
165 } | |
166 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
167 if (it == end) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
168 throw Error(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>"); |
0 | 169 |
170 /* Remove quote */ | |
171 ++ it; | |
172 | |
173 list.push_back({ Token::QuotedWord, line, save, std::move(value) }); | |
174 } | |
175 | |
176 void analyzeWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) | |
177 { | |
178 assert(!isReserved(*it)); | |
179 | |
180 std::string value; | |
181 int save = column; | |
182 | |
183 while (it != end && !std::isspace(*it) && !isReserved(*it)) { | |
184 ++ column; | |
185 value += *it++; | |
186 } | |
187 | |
188 list.push_back({ Token::Word, line, save, std::move(value) }); | |
189 } | |
190 | |
191 void analyzeInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) | |
192 { | |
193 assert(*it == '@'); | |
194 | |
195 std::string include; | |
196 int save = column; | |
197 | |
198 /* Read include */ | |
199 ++ it; | |
200 while (it != end && !isSpace(*it)) { | |
201 ++ column; | |
202 include += *it++; | |
203 } | |
204 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
205 if (include != "include") |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
206 throw Error(line, column, "expected include after '@' token"); |
0 | 207 |
208 list.push_back({ Token::Include, line, save }); | |
209 } | |
210 | |
211 Tokens analyze(StreamIterator &it, StreamIterator end) | |
212 { | |
213 Tokens list; | |
214 int line = 1; | |
215 int column = 0; | |
216 | |
217 while (it != end) { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
218 if (*it == '\n') |
0 | 219 analyzeLine(line, column, it); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
220 else if (*it == '#') |
0 | 221 analyzeComment(column, it, end); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
222 else if (*it == '[') |
0 | 223 analyzeSection(list, line, column, it, end); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
224 else if (*it == '=') |
0 | 225 analyzeAssign(list, line, column, it); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
226 else if (isSpace(*it)) |
0 | 227 analyzeSpaces(column, it, end); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
228 else if (*it == '@') |
0 | 229 analyzeInclude(list, line, column, it, end); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
230 else if (isQuote(*it)) |
0 | 231 analyzeQuotedWord(list, line, column, it, end); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
232 else if (isList(*it)) |
0 | 233 analyzeList(list, line, column, it); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
234 else |
0 | 235 analyzeWord(list, line, column, it, end); |
236 } | |
237 | |
238 return list; | |
239 } | |
240 | |
241 void parseOptionValueSimple(Option &option, TokenIterator &it) | |
242 { | |
243 assert(it->type() == Token::Word || it->type() == Token::QuotedWord); | |
244 | |
245 option.push_back((it++)->value()); | |
246 } | |
247 | |
248 void parseOptionValueList(Option &option, TokenIterator &it, TokenIterator end) | |
249 { | |
250 assert(it->type() == Token::ListBegin); | |
251 | |
252 TokenIterator save = it++; | |
253 | |
254 while (it != end && it->type() != Token::ListEnd) { | |
255 switch (it->type()) { | |
256 case Token::Comma: | |
257 /* Previous must be a word */ | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
258 if (it[-1].type() != Token::Word && it[-1].type() != Token::QuotedWord) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
259 throw Error(it->line(), it->column(), "unexpected comma after '" + it[-1].value() + "'"); |
0 | 260 |
261 ++ it; | |
262 break; | |
263 case Token::Word: | |
264 case Token::QuotedWord: | |
265 option.push_back((it++)->value()); | |
266 break; | |
267 default: | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
268 throw Error(it->line(), it->column(), "unexpected '" + it[-1].value() + "' in list construct"); |
0 | 269 break; |
270 } | |
271 } | |
272 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
273 if (it == end) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
274 throw Error(save->line(), save->column(), "unterminated list construct"); |
0 | 275 |
276 /* Remove ) */ | |
277 ++ it; | |
278 } | |
279 | |
280 void parseOption(Section &sc, TokenIterator &it, TokenIterator end) | |
281 { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
282 Option option(it->value()); |
0 | 283 |
284 TokenIterator save = it; | |
285 | |
286 /* No '=' or something else? */ | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
287 if (++it == end) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
288 throw Error(save->line(), save->column(), "expected '=' assignment, got <EOF>"); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
289 if (it->type() != Token::Assign) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
290 throw Error(it->line(), it->column(), "expected '=' assignment, got " + it->value()); |
0 | 291 |
292 /* Empty options are allowed so just test for words */ | |
293 if (++it != end) { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
294 if (it->type() == Token::Word || it->type() == Token::QuotedWord) |
0 | 295 parseOptionValueSimple(option, it); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
296 else if (it->type() == Token::ListBegin) |
0 | 297 parseOptionValueList(option, it, end); |
298 } | |
299 | |
300 sc.push_back(std::move(option)); | |
301 } | |
302 | |
303 void parseInclude(Document &doc, TokenIterator &it, TokenIterator end) | |
304 { | |
305 TokenIterator save = it; | |
306 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
307 if (++it == end) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
308 throw Error(save->line(), save->column(), "expected file name after '@include' statement, got <EOF>"); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
309 if (it->type() != Token::Word && it->type() != Token::QuotedWord) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
310 throw Error(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value()); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
311 if (doc.path().empty()) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
312 throw Error(it->line(), it->column(), "'@include' statement invalid with buffer documents"); |
0 | 313 |
314 std::string value = (it++)->value(); | |
315 std::string file; | |
316 | |
317 if (!isAbsolute(value)) { | |
318 #if defined(_WIN32) | |
319 file = doc.path() + "\\" + value; | |
320 #else | |
321 file = doc.path() + "/" + value; | |
322 #endif | |
323 } else { | |
324 file = value; | |
325 } | |
326 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
327 Document child(File{file}); |
0 | 328 |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
329 for (const auto &sc : child) |
0 | 330 doc.push_back(sc); |
331 } | |
332 | |
333 void parseSection(Document &doc, TokenIterator &it, TokenIterator end) | |
334 { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
335 Section sc(it->value()); |
0 | 336 |
337 /* Skip [section] */ | |
338 ++ it; | |
339 | |
340 /* Read until next section */ | |
341 while (it != end && it->type() != Token::Section) { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
342 if (it->type() != Token::Word) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
343 throw Error(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition"); |
0 | 344 |
345 parseOption(sc, it, end); | |
346 } | |
347 | |
348 doc.push_back(std::move(sc)); | |
349 } | |
350 | |
351 void parse(Document &doc, const Tokens &tokens) | |
352 { | |
353 TokenIterator it = tokens.cbegin(); | |
354 TokenIterator end = tokens.cend(); | |
355 | |
356 while (it != end) { | |
357 /* Just ignore this */ | |
358 switch (it->type()) { | |
359 case Token::Include: | |
360 parseInclude(doc, it, end); | |
361 break; | |
362 case Token::Section: | |
363 parseSection(doc, it, end); | |
364 break; | |
365 default: | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
366 throw Error(it->line(), it->column(), "unexpected '" + it->value() + "' on root document"); |
0 | 367 } |
368 } | |
369 } | |
370 | |
371 } // !namespace | |
372 | |
373 namespace irccd { | |
374 | |
375 namespace ini { | |
376 | |
377 Tokens Document::analyze(const File &file) | |
378 { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
379 std::fstream stream(file.path); |
0 | 380 |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
381 if (!stream) |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
382 throw std::runtime_error(std::strerror(errno)); |
0 | 383 |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
384 std::istreambuf_iterator<char> it(stream); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
385 std::istreambuf_iterator<char> end; |
0 | 386 |
387 return ::analyze(it, end); | |
388 } | |
389 | |
390 Tokens Document::analyze(const Buffer &buffer) | |
391 { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
392 std::istringstream stream(buffer.text); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
393 std::istreambuf_iterator<char> it(stream); |
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
394 std::istreambuf_iterator<char> end; |
0 | 395 |
396 return ::analyze(it, end); | |
397 } | |
398 | |
399 Document::Document(const File &file) | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
400 : m_path(file.path) |
0 | 401 { |
402 /* Update path */ | |
403 auto pos = m_path.find_last_of("/\\"); | |
404 | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
405 if (pos != std::string::npos) |
0 | 406 m_path.erase(pos); |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
407 else |
0 | 408 m_path = "."; |
409 | |
410 parse(*this, analyze(file)); | |
411 } | |
412 | |
413 Document::Document(const Buffer &buffer) | |
414 { | |
415 parse(*this, analyze(buffer)); | |
416 } | |
417 | |
418 void Document::dump(const Tokens &tokens) | |
419 { | |
23
03068f5ed79d
Misc: various style issues, #419
David Demelier <markand@malikania.fr>
parents:
0
diff
changeset
|
420 for (const Token &token: tokens) |
0 | 421 // TODO: add better description |
422 std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl; | |
423 } | |
424 | |
425 } // !ini | |
426 | |
427 } // !irccd |