]> git.proxmox.com Git - ceph.git/blame - ceph/src/common/ConfUtils.cc
update sources to ceph Nautilus 14.2.1
[ceph.git] / ceph / src / common / ConfUtils.cc
CommitLineData
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
26using std::ostringstream;
27using std::pair;
28using std::string;
29
30#define MAX_CONFIG_FILE_SZ 0x40000000
31
32////////////////////////////// ConfLine //////////////////////////////
33ConfLine::
34ConfLine(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
42bool ConfLine::
43operator<(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
54std::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 //////////////////////////
61ConfFile::
62ConfFile()
63{
64}
65
66ConfFile::
67~ConfFile()
68{
69}
70
71void ConfFile::
72clear()
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 */
84int ConfFile::
85parse_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
149done:
150 free(buf);
151 fclose(fp);
152 return ret;
153}
154
155int ConfFile::
156parse_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
165int ConfFile::
166read(const std::string &section, 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
181ConfFile::const_section_iter_t ConfFile::
182sections_begin() const
183{
184 return sections.begin();
185}
186
187ConfFile::const_section_iter_t ConfFile::
188sections_end() const
189{
190 return sections.end();
191}
192
193void ConfFile::
194trim_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 */
252std::string ConfFile::
253normalize_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
265std::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
280void ConfFile::
281load_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 */
395ConfLine* ConfFile::
396process_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}