]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- |
2 | // vim: ts=8 sw=2 smarttab | |
3 | /* | |
4 | * Ceph - scalable distributed file system | |
5 | * | |
6 | * Copyright (C) 2011 New Dream Network | |
7 | * | |
8 | * This is free software; you can redistribute it and/or | |
9 | * modify it under the terms of the GNU Lesser General Public | |
10 | * License version 2.1, as published by the Free Software | |
11 | * Foundation. See file COPYING. | |
12 | * | |
13 | */ | |
14 | ||
15 | #include <algorithm> | |
7c673cae FG |
16 | #include <map> |
17 | #include <sstream> | |
7c673cae FG |
18 | #include <sys/stat.h> |
19 | #include <iostream> | |
20 | ||
21 | #include "include/buffer.h" | |
22 | #include "common/errno.h" | |
23 | #include "common/utf8.h" | |
24 | #include "common/ConfUtils.h" | |
25 | ||
7c673cae FG |
26 | using std::ostringstream; |
27 | using std::pair; | |
28 | using std::string; | |
29 | ||
30 | #define MAX_CONFIG_FILE_SZ 0x40000000 | |
31 | ||
32 | ////////////////////////////// ConfLine ////////////////////////////// | |
33 | ConfLine:: | |
34 | ConfLine(const std::string &key_, const std::string &val_, | |
35 | const std::string &newsection_, const std::string &comment_, int line_no_) | |
36 | : key(key_), val(val_), newsection(newsection_) | |
37 | { | |
38 | // If you want to implement writable ConfFile support, you'll need to save | |
39 | // the comment and line_no arguments here. | |
40 | } | |
41 | ||
42 | bool ConfLine:: | |
43 | operator<(const ConfLine &rhs) const | |
44 | { | |
45 | // We only compare keys. | |
46 | // If you have more than one line with the same key in a given section, the | |
47 | // last one wins. | |
48 | if (key < rhs.key) | |
49 | return true; | |
50 | else | |
51 | return false; | |
52 | } | |
53 | ||
54 | std::ostream &operator<<(std::ostream& oss, const ConfLine &l) | |
55 | { | |
56 | oss << "ConfLine(key = '" << l.key << "', val='" | |
57 | << l.val << "', newsection='" << l.newsection << "')"; | |
58 | return oss; | |
59 | } | |
60 | ///////////////////////// ConfFile ////////////////////////// | |
61 | ConfFile:: | |
62 | ConfFile() | |
63 | { | |
64 | } | |
65 | ||
66 | ConfFile:: | |
67 | ~ConfFile() | |
68 | { | |
69 | } | |
70 | ||
71 | void ConfFile:: | |
72 | clear() | |
73 | { | |
74 | sections.clear(); | |
75 | } | |
76 | ||
77 | /* We load the whole file into memory and then parse it. Although this is not | |
78 | * the optimal approach, it does mean that most of this code can be shared with | |
79 | * the bufferlist loading function. Since bufferlists are always in-memory, the | |
80 | * load_from_buffer interface works well for them. | |
81 | * In general, configuration files should be a few kilobytes at maximum, so | |
82 | * loading the whole configuration into memory shouldn't be a problem. | |
83 | */ | |
84 | int ConfFile:: | |
85 | parse_file(const std::string &fname, std::deque<std::string> *errors, | |
86 | std::ostream *warnings) | |
87 | { | |
88 | clear(); | |
89 | ||
90 | int ret = 0; | |
91 | size_t sz; | |
92 | char *buf = NULL; | |
93 | FILE *fp = fopen(fname.c_str(), "r"); | |
94 | if (!fp) { | |
95 | ostringstream oss; | |
96 | oss << __func__ << ": cannot open " << fname << ": " << cpp_strerror(errno); | |
97 | errors->push_back(oss.str()); | |
98 | ret = -errno; | |
99 | return ret; | |
100 | } | |
101 | ||
102 | struct stat st_buf; | |
103 | if (fstat(fileno(fp), &st_buf)) { | |
104 | ret = -errno; | |
105 | ostringstream oss; | |
106 | oss << __func__ << ": failed to fstat '" << fname << "': " << cpp_strerror(ret); | |
107 | errors->push_back(oss.str()); | |
108 | goto done; | |
109 | } | |
110 | ||
111 | if (st_buf.st_size > MAX_CONFIG_FILE_SZ) { | |
112 | ostringstream oss; | |
113 | oss << __func__ << ": config file '" << fname << "' is " << st_buf.st_size | |
114 | << " bytes, but the maximum is " << MAX_CONFIG_FILE_SZ; | |
115 | errors->push_back(oss.str()); | |
116 | ret = -EINVAL; | |
117 | goto done; | |
118 | } | |
119 | ||
120 | sz = (size_t)st_buf.st_size; | |
121 | buf = (char*)malloc(sz); | |
122 | if (!buf) { | |
123 | ret = -ENOMEM; | |
124 | goto done; | |
125 | } | |
126 | ||
127 | if (fread(buf, 1, sz, fp) != sz) { | |
128 | if (ferror(fp)) { | |
129 | ret = -errno; | |
130 | ostringstream oss; | |
131 | oss << __func__ << ": fread error while reading '" << fname << "': " | |
132 | << cpp_strerror(ret); | |
133 | errors->push_back(oss.str()); | |
134 | goto done; | |
135 | } | |
136 | else { | |
137 | ostringstream oss; | |
138 | oss << __func__ << ": unexpected EOF while reading '" << fname << "': " | |
139 | << "possible concurrent modification?"; | |
140 | errors->push_back(oss.str()); | |
141 | ret = -EIO; | |
142 | goto done; | |
143 | } | |
144 | } | |
145 | ||
146 | load_from_buffer(buf, sz, errors, warnings); | |
147 | ret = 0; | |
148 | ||
149 | done: | |
150 | free(buf); | |
151 | fclose(fp); | |
152 | return ret; | |
153 | } | |
154 | ||
155 | int ConfFile:: | |
156 | parse_bufferlist(ceph::bufferlist *bl, std::deque<std::string> *errors, | |
157 | std::ostream *warnings) | |
158 | { | |
159 | clear(); | |
160 | ||
161 | load_from_buffer(bl->c_str(), bl->length(), errors, warnings); | |
162 | return 0; | |
163 | } | |
164 | ||
165 | int ConfFile:: | |
166 | read(const std::string §ion, const std::string &key, std::string &val) const | |
167 | { | |
168 | string k(normalize_key_name(key)); | |
169 | ||
170 | const_section_iter_t s = sections.find(section); | |
171 | if (s == sections.end()) | |
172 | return -ENOENT; | |
173 | ConfLine exemplar(k, "", "", "", 0); | |
174 | ConfSection::const_line_iter_t l = s->second.lines.find(exemplar); | |
175 | if (l == s->second.lines.end()) | |
176 | return -ENOENT; | |
177 | val = l->val; | |
178 | return 0; | |
179 | } | |
180 | ||
181 | ConfFile::const_section_iter_t ConfFile:: | |
182 | sections_begin() const | |
183 | { | |
184 | return sections.begin(); | |
185 | } | |
186 | ||
187 | ConfFile::const_section_iter_t ConfFile:: | |
188 | sections_end() const | |
189 | { | |
190 | return sections.end(); | |
191 | } | |
192 | ||
193 | void ConfFile:: | |
194 | trim_whitespace(std::string &str, bool strip_internal) | |
195 | { | |
196 | // strip preceding | |
197 | const char *in = str.c_str(); | |
198 | while (true) { | |
199 | char c = *in; | |
200 | if ((!c) || (!isspace(c))) | |
201 | break; | |
202 | ++in; | |
203 | } | |
204 | char output[strlen(in) + 1]; | |
205 | strcpy(output, in); | |
206 | ||
207 | // strip trailing | |
208 | char *o = output + strlen(output); | |
209 | while (true) { | |
210 | if (o == output) | |
211 | break; | |
212 | --o; | |
213 | if (!isspace(*o)) { | |
214 | ++o; | |
215 | *o = '\0'; | |
216 | break; | |
217 | } | |
218 | } | |
219 | ||
220 | if (!strip_internal) { | |
221 | str.assign(output); | |
222 | return; | |
223 | } | |
224 | ||
225 | // strip internal | |
226 | char output2[strlen(output) + 1]; | |
227 | char *out2 = output2; | |
228 | bool prev_was_space = false; | |
229 | for (char *u = output; *u; ++u) { | |
230 | char c = *u; | |
231 | if (isspace(c)) { | |
232 | if (!prev_was_space) | |
233 | *out2++ = c; | |
234 | prev_was_space = true; | |
235 | } | |
236 | else { | |
237 | *out2++ = c; | |
238 | prev_was_space = false; | |
239 | } | |
240 | } | |
241 | *out2++ = '\0'; | |
242 | str.assign(output2); | |
243 | } | |
244 | ||
245 | /* Normalize a key name. | |
246 | * | |
247 | * Normalized key names have no leading or trailing whitespace, and all | |
248 | * whitespace is stored as underscores. The main reason for selecting this | |
249 | * normal form is so that in common/config.cc, we can use a macro to stringify | |
250 | * the field names of md_config_t and get a key in normal form. | |
251 | */ | |
252 | std::string ConfFile:: | |
253 | normalize_key_name(const std::string &key) | |
254 | { | |
11fdf7f2 TL |
255 | if (key.find_first_of(" \t\r\n\f\v\xa0") == string::npos) { |
256 | return key; | |
257 | } | |
258 | ||
7c673cae FG |
259 | string k(key); |
260 | ConfFile::trim_whitespace(k, true); | |
261 | std::replace(k.begin(), k.end(), ' ', '_'); | |
262 | return k; | |
263 | } | |
264 | ||
265 | std::ostream &operator<<(std::ostream &oss, const ConfFile &cf) | |
266 | { | |
267 | for (ConfFile::const_section_iter_t s = cf.sections_begin(); | |
268 | s != cf.sections_end(); ++s) { | |
269 | oss << "[" << s->first << "]\n"; | |
270 | for (ConfSection::const_line_iter_t l = s->second.lines.begin(); | |
271 | l != s->second.lines.end(); ++l) { | |
272 | if (!l->key.empty()) { | |
273 | oss << "\t" << l->key << " = \"" << l->val << "\"\n"; | |
274 | } | |
275 | } | |
276 | } | |
277 | return oss; | |
278 | } | |
279 | ||
280 | void ConfFile:: | |
281 | load_from_buffer(const char *buf, size_t sz, std::deque<std::string> *errors, | |
282 | std::ostream *warnings) | |
283 | { | |
284 | errors->clear(); | |
285 | ||
286 | section_iter_t::value_type vt("global", ConfSection()); | |
287 | pair < section_iter_t, bool > vr(sections.insert(vt)); | |
11fdf7f2 | 288 | ceph_assert(vr.second); |
7c673cae FG |
289 | section_iter_t cur_section = vr.first; |
290 | std::string acc; | |
291 | ||
292 | const char *b = buf; | |
293 | int line_no = 0; | |
294 | size_t line_len = -1; | |
295 | size_t rem = sz; | |
296 | while (1) { | |
297 | b += line_len + 1; | |
298 | if ((line_len + 1) > rem) | |
299 | break; | |
300 | rem -= line_len + 1; | |
301 | if (rem == 0) | |
302 | break; | |
303 | line_no++; | |
304 | ||
305 | // look for the next newline | |
306 | const char *end = (const char*)memchr(b, '\n', rem); | |
307 | if (!end) { | |
308 | ostringstream oss; | |
309 | oss << "read_conf: ignoring line " << line_no << " because it doesn't " | |
310 | << "end with a newline! Please end the config file with a newline."; | |
311 | errors->push_back(oss.str()); | |
312 | break; | |
313 | } | |
314 | ||
315 | // find length of line, and search for NULLs | |
316 | line_len = 0; | |
317 | bool found_null = false; | |
318 | for (const char *tmp = b; tmp != end; ++tmp) { | |
319 | line_len++; | |
320 | if (*tmp == '\0') { | |
321 | found_null = true; | |
322 | } | |
323 | } | |
324 | ||
325 | if (found_null) { | |
326 | ostringstream oss; | |
327 | oss << "read_conf: ignoring line " << line_no << " because it has " | |
328 | << "an embedded null."; | |
329 | errors->push_back(oss.str()); | |
330 | acc.clear(); | |
331 | continue; | |
332 | } | |
333 | ||
334 | if (check_utf8(b, line_len)) { | |
335 | ostringstream oss; | |
336 | oss << "read_conf: ignoring line " << line_no << " because it is not " | |
337 | << "valid UTF8."; | |
338 | errors->push_back(oss.str()); | |
339 | acc.clear(); | |
340 | continue; | |
341 | } | |
342 | ||
343 | if ((line_len >= 1) && (b[line_len-1] == '\\')) { | |
344 | // A backslash at the end of a line serves as a line continuation marker. | |
345 | // Combine the next line with this one. | |
346 | // Remove the backslash itself from the text. | |
347 | acc.append(b, line_len - 1); | |
348 | continue; | |
349 | } | |
350 | ||
351 | acc.append(b, line_len); | |
352 | ||
353 | //cerr << "acc = '" << acc << "'" << std::endl; | |
354 | ConfLine *cline = process_line(line_no, acc.c_str(), errors); | |
355 | acc.clear(); | |
356 | if (!cline) | |
357 | continue; | |
358 | const std::string &csection(cline->newsection); | |
359 | if (!csection.empty()) { | |
360 | std::map <std::string, ConfSection>::value_type nt(csection, ConfSection()); | |
361 | pair < section_iter_t, bool > nr(sections.insert(nt)); | |
362 | cur_section = nr.first; | |
363 | } | |
364 | else { | |
365 | if (cur_section->second.lines.count(*cline)) { | |
366 | // replace an existing key/line in this section, so that | |
367 | // [mysection] | |
368 | // foo = 1 | |
369 | // foo = 2 | |
370 | // will result in foo = 2. | |
371 | cur_section->second.lines.erase(*cline); | |
372 | if (cline->key.length() && warnings) | |
373 | *warnings << "warning: line " << line_no << ": '" << cline->key << "' in section '" | |
374 | << cur_section->first << "' redefined " << std::endl; | |
375 | } | |
376 | // add line to current section | |
377 | //std::cerr << "cur_section = " << cur_section->first << ", " << *cline << std::endl; | |
378 | cur_section->second.lines.insert(*cline); | |
379 | } | |
380 | delete cline; | |
381 | } | |
382 | ||
383 | if (!acc.empty()) { | |
384 | ostringstream oss; | |
385 | oss << "read_conf: don't end with lines that end in backslashes!"; | |
386 | errors->push_back(oss.str()); | |
387 | } | |
388 | } | |
389 | ||
390 | /* | |
391 | * A simple state-machine based parser. | |
392 | * This probably could/should be rewritten with something like boost::spirit | |
393 | * or yacc if the grammar ever gets more complex. | |
394 | */ | |
395 | ConfLine* ConfFile:: | |
396 | process_line(int line_no, const char *line, std::deque<std::string> *errors) | |
397 | { | |
398 | enum acceptor_state_t { | |
399 | ACCEPT_INIT, | |
400 | ACCEPT_SECTION_NAME, | |
401 | ACCEPT_KEY, | |
402 | ACCEPT_VAL_START, | |
403 | ACCEPT_UNQUOTED_VAL, | |
404 | ACCEPT_QUOTED_VAL, | |
405 | ACCEPT_COMMENT_START, | |
406 | ACCEPT_COMMENT_TEXT, | |
407 | }; | |
408 | const char *l = line; | |
409 | acceptor_state_t state = ACCEPT_INIT; | |
410 | string key, val, newsection, comment; | |
411 | bool escaping = false; | |
412 | while (true) { | |
413 | char c = *l++; | |
414 | switch (state) { | |
415 | case ACCEPT_INIT: | |
416 | if (c == '\0') | |
417 | return NULL; // blank line. Not an error, but not interesting either. | |
418 | else if (c == '[') | |
419 | state = ACCEPT_SECTION_NAME; | |
420 | else if ((c == '#') || (c == ';')) | |
421 | state = ACCEPT_COMMENT_TEXT; | |
422 | else if (c == ']') { | |
423 | ostringstream oss; | |
424 | oss << "unexpected right bracket at char " << (l - line) | |
425 | << ", line " << line_no; | |
426 | errors->push_back(oss.str()); | |
427 | return NULL; | |
428 | } | |
429 | else if (isspace(c)) { | |
430 | // ignore whitespace here | |
431 | } | |
432 | else { | |
433 | // try to accept this character as a key | |
434 | state = ACCEPT_KEY; | |
435 | --l; | |
436 | } | |
437 | break; | |
438 | case ACCEPT_SECTION_NAME: | |
439 | if (c == '\0') { | |
440 | ostringstream oss; | |
441 | oss << "error parsing new section name: expected right bracket " | |
442 | << "at char " << (l - line) << ", line " << line_no; | |
443 | errors->push_back(oss.str()); | |
444 | return NULL; | |
445 | } | |
446 | else if ((c == ']') && (!escaping)) { | |
447 | trim_whitespace(newsection, true); | |
448 | if (newsection.empty()) { | |
449 | ostringstream oss; | |
450 | oss << "error parsing new section name: no section name found? " | |
451 | << "at char " << (l - line) << ", line " << line_no; | |
452 | errors->push_back(oss.str()); | |
453 | return NULL; | |
454 | } | |
455 | state = ACCEPT_COMMENT_START; | |
456 | } | |
457 | else if (((c == '#') || (c == ';')) && (!escaping)) { | |
458 | ostringstream oss; | |
459 | oss << "unexpected comment marker while parsing new section name, at " | |
460 | << "char " << (l - line) << ", line " << line_no; | |
461 | errors->push_back(oss.str()); | |
462 | return NULL; | |
463 | } | |
464 | else if ((c == '\\') && (!escaping)) { | |
465 | escaping = true; | |
466 | } | |
467 | else { | |
468 | escaping = false; | |
469 | newsection += c; | |
470 | } | |
471 | break; | |
472 | case ACCEPT_KEY: | |
473 | if ((((c == '#') || (c == ';')) && (!escaping)) || (c == '\0')) { | |
474 | ostringstream oss; | |
475 | if (c == '\0') { | |
476 | oss << "end of key=val line " << line_no | |
477 | << " reached, no \"=val\" found...missing =?"; | |
478 | } else { | |
479 | oss << "unexpected character while parsing putative key value, " | |
480 | << "at char " << (l - line) << ", line " << line_no; | |
481 | } | |
482 | errors->push_back(oss.str()); | |
483 | return NULL; | |
484 | } | |
485 | else if ((c == '=') && (!escaping)) { | |
486 | key = normalize_key_name(key); | |
487 | if (key.empty()) { | |
488 | ostringstream oss; | |
489 | oss << "error parsing key name: no key name found? " | |
490 | << "at char " << (l - line) << ", line " << line_no; | |
491 | errors->push_back(oss.str()); | |
492 | return NULL; | |
493 | } | |
494 | state = ACCEPT_VAL_START; | |
495 | } | |
496 | else if ((c == '\\') && (!escaping)) { | |
497 | escaping = true; | |
498 | } | |
499 | else { | |
500 | escaping = false; | |
501 | key += c; | |
502 | } | |
503 | break; | |
504 | case ACCEPT_VAL_START: | |
505 | if (c == '\0') | |
506 | return new ConfLine(key, val, newsection, comment, line_no); | |
507 | else if ((c == '#') || (c == ';')) | |
508 | state = ACCEPT_COMMENT_TEXT; | |
509 | else if (c == '"') | |
510 | state = ACCEPT_QUOTED_VAL; | |
511 | else if (isspace(c)) { | |
512 | // ignore whitespace | |
513 | } | |
514 | else { | |
515 | // try to accept character as a val | |
516 | state = ACCEPT_UNQUOTED_VAL; | |
517 | --l; | |
518 | } | |
519 | break; | |
520 | case ACCEPT_UNQUOTED_VAL: | |
521 | if (c == '\0') { | |
522 | if (escaping) { | |
523 | ostringstream oss; | |
524 | oss << "error parsing value name: unterminated escape sequence " | |
525 | << "at char " << (l - line) << ", line " << line_no; | |
526 | errors->push_back(oss.str()); | |
527 | return NULL; | |
528 | } | |
529 | trim_whitespace(val, false); | |
530 | return new ConfLine(key, val, newsection, comment, line_no); | |
531 | } | |
532 | else if (((c == '#') || (c == ';')) && (!escaping)) { | |
533 | trim_whitespace(val, false); | |
534 | state = ACCEPT_COMMENT_TEXT; | |
535 | } | |
536 | else if ((c == '\\') && (!escaping)) { | |
537 | escaping = true; | |
538 | } | |
539 | else { | |
540 | escaping = false; | |
541 | val += c; | |
542 | } | |
543 | break; | |
544 | case ACCEPT_QUOTED_VAL: | |
545 | if (c == '\0') { | |
546 | ostringstream oss; | |
547 | oss << "found opening quote for value, but not the closing quote. " | |
548 | << "line " << line_no; | |
549 | errors->push_back(oss.str()); | |
550 | return NULL; | |
551 | } | |
552 | else if ((c == '"') && (!escaping)) { | |
553 | state = ACCEPT_COMMENT_START; | |
554 | } | |
555 | else if ((c == '\\') && (!escaping)) { | |
556 | escaping = true; | |
557 | } | |
558 | else { | |
559 | escaping = false; | |
560 | // Add anything, including whitespace. | |
561 | val += c; | |
562 | } | |
563 | break; | |
564 | case ACCEPT_COMMENT_START: | |
565 | if (c == '\0') { | |
566 | return new ConfLine(key, val, newsection, comment, line_no); | |
567 | } | |
568 | else if ((c == '#') || (c == ';')) { | |
569 | state = ACCEPT_COMMENT_TEXT; | |
570 | } | |
571 | else if (isspace(c)) { | |
572 | // ignore whitespace | |
573 | } | |
574 | else { | |
575 | ostringstream oss; | |
576 | oss << "unexpected character at char " << (l - line) << " of line " | |
577 | << line_no; | |
578 | errors->push_back(oss.str()); | |
579 | return NULL; | |
580 | } | |
581 | break; | |
582 | case ACCEPT_COMMENT_TEXT: | |
583 | if (c == '\0') | |
584 | return new ConfLine(key, val, newsection, comment, line_no); | |
585 | else | |
586 | comment += c; | |
587 | break; | |
588 | default: | |
589 | ceph_abort(); | |
590 | break; | |
591 | } | |
11fdf7f2 | 592 | ceph_assert(c != '\0'); // We better not go past the end of the input string. |
7c673cae FG |
593 | } |
594 | } |