]> git.proxmox.com Git - ceph.git/blame - ceph/src/common/ConfUtils.cc
update sources to v12.1.0
[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{
255 string k(key);
256 ConfFile::trim_whitespace(k, true);
257 std::replace(k.begin(), k.end(), ' ', '_');
258 return k;
259}
260
261std::ostream &operator<<(std::ostream &oss, const ConfFile &cf)
262{
263 for (ConfFile::const_section_iter_t s = cf.sections_begin();
264 s != cf.sections_end(); ++s) {
265 oss << "[" << s->first << "]\n";
266 for (ConfSection::const_line_iter_t l = s->second.lines.begin();
267 l != s->second.lines.end(); ++l) {
268 if (!l->key.empty()) {
269 oss << "\t" << l->key << " = \"" << l->val << "\"\n";
270 }
271 }
272 }
273 return oss;
274}
275
276void ConfFile::
277load_from_buffer(const char *buf, size_t sz, std::deque<std::string> *errors,
278 std::ostream *warnings)
279{
280 errors->clear();
281
282 section_iter_t::value_type vt("global", ConfSection());
283 pair < section_iter_t, bool > vr(sections.insert(vt));
284 assert(vr.second);
285 section_iter_t cur_section = vr.first;
286 std::string acc;
287
288 const char *b = buf;
289 int line_no = 0;
290 size_t line_len = -1;
291 size_t rem = sz;
292 while (1) {
293 b += line_len + 1;
294 if ((line_len + 1) > rem)
295 break;
296 rem -= line_len + 1;
297 if (rem == 0)
298 break;
299 line_no++;
300
301 // look for the next newline
302 const char *end = (const char*)memchr(b, '\n', rem);
303 if (!end) {
304 ostringstream oss;
305 oss << "read_conf: ignoring line " << line_no << " because it doesn't "
306 << "end with a newline! Please end the config file with a newline.";
307 errors->push_back(oss.str());
308 break;
309 }
310
311 // find length of line, and search for NULLs
312 line_len = 0;
313 bool found_null = false;
314 for (const char *tmp = b; tmp != end; ++tmp) {
315 line_len++;
316 if (*tmp == '\0') {
317 found_null = true;
318 }
319 }
320
321 if (found_null) {
322 ostringstream oss;
323 oss << "read_conf: ignoring line " << line_no << " because it has "
324 << "an embedded null.";
325 errors->push_back(oss.str());
326 acc.clear();
327 continue;
328 }
329
330 if (check_utf8(b, line_len)) {
331 ostringstream oss;
332 oss << "read_conf: ignoring line " << line_no << " because it is not "
333 << "valid UTF8.";
334 errors->push_back(oss.str());
335 acc.clear();
336 continue;
337 }
338
339 if ((line_len >= 1) && (b[line_len-1] == '\\')) {
340 // A backslash at the end of a line serves as a line continuation marker.
341 // Combine the next line with this one.
342 // Remove the backslash itself from the text.
343 acc.append(b, line_len - 1);
344 continue;
345 }
346
347 acc.append(b, line_len);
348
349 //cerr << "acc = '" << acc << "'" << std::endl;
350 ConfLine *cline = process_line(line_no, acc.c_str(), errors);
351 acc.clear();
352 if (!cline)
353 continue;
354 const std::string &csection(cline->newsection);
355 if (!csection.empty()) {
356 std::map <std::string, ConfSection>::value_type nt(csection, ConfSection());
357 pair < section_iter_t, bool > nr(sections.insert(nt));
358 cur_section = nr.first;
359 }
360 else {
361 if (cur_section->second.lines.count(*cline)) {
362 // replace an existing key/line in this section, so that
363 // [mysection]
364 // foo = 1
365 // foo = 2
366 // will result in foo = 2.
367 cur_section->second.lines.erase(*cline);
368 if (cline->key.length() && warnings)
369 *warnings << "warning: line " << line_no << ": '" << cline->key << "' in section '"
370 << cur_section->first << "' redefined " << std::endl;
371 }
372 // add line to current section
373 //std::cerr << "cur_section = " << cur_section->first << ", " << *cline << std::endl;
374 cur_section->second.lines.insert(*cline);
375 }
376 delete cline;
377 }
378
379 if (!acc.empty()) {
380 ostringstream oss;
381 oss << "read_conf: don't end with lines that end in backslashes!";
382 errors->push_back(oss.str());
383 }
384}
385
386/*
387 * A simple state-machine based parser.
388 * This probably could/should be rewritten with something like boost::spirit
389 * or yacc if the grammar ever gets more complex.
390 */
391ConfLine* ConfFile::
392process_line(int line_no, const char *line, std::deque<std::string> *errors)
393{
394 enum acceptor_state_t {
395 ACCEPT_INIT,
396 ACCEPT_SECTION_NAME,
397 ACCEPT_KEY,
398 ACCEPT_VAL_START,
399 ACCEPT_UNQUOTED_VAL,
400 ACCEPT_QUOTED_VAL,
401 ACCEPT_COMMENT_START,
402 ACCEPT_COMMENT_TEXT,
403 };
404 const char *l = line;
405 acceptor_state_t state = ACCEPT_INIT;
406 string key, val, newsection, comment;
407 bool escaping = false;
408 while (true) {
409 char c = *l++;
410 switch (state) {
411 case ACCEPT_INIT:
412 if (c == '\0')
413 return NULL; // blank line. Not an error, but not interesting either.
414 else if (c == '[')
415 state = ACCEPT_SECTION_NAME;
416 else if ((c == '#') || (c == ';'))
417 state = ACCEPT_COMMENT_TEXT;
418 else if (c == ']') {
419 ostringstream oss;
420 oss << "unexpected right bracket at char " << (l - line)
421 << ", line " << line_no;
422 errors->push_back(oss.str());
423 return NULL;
424 }
425 else if (isspace(c)) {
426 // ignore whitespace here
427 }
428 else {
429 // try to accept this character as a key
430 state = ACCEPT_KEY;
431 --l;
432 }
433 break;
434 case ACCEPT_SECTION_NAME:
435 if (c == '\0') {
436 ostringstream oss;
437 oss << "error parsing new section name: expected right bracket "
438 << "at char " << (l - line) << ", line " << line_no;
439 errors->push_back(oss.str());
440 return NULL;
441 }
442 else if ((c == ']') && (!escaping)) {
443 trim_whitespace(newsection, true);
444 if (newsection.empty()) {
445 ostringstream oss;
446 oss << "error parsing new section name: no section name found? "
447 << "at char " << (l - line) << ", line " << line_no;
448 errors->push_back(oss.str());
449 return NULL;
450 }
451 state = ACCEPT_COMMENT_START;
452 }
453 else if (((c == '#') || (c == ';')) && (!escaping)) {
454 ostringstream oss;
455 oss << "unexpected comment marker while parsing new section name, at "
456 << "char " << (l - line) << ", line " << line_no;
457 errors->push_back(oss.str());
458 return NULL;
459 }
460 else if ((c == '\\') && (!escaping)) {
461 escaping = true;
462 }
463 else {
464 escaping = false;
465 newsection += c;
466 }
467 break;
468 case ACCEPT_KEY:
469 if ((((c == '#') || (c == ';')) && (!escaping)) || (c == '\0')) {
470 ostringstream oss;
471 if (c == '\0') {
472 oss << "end of key=val line " << line_no
473 << " reached, no \"=val\" found...missing =?";
474 } else {
475 oss << "unexpected character while parsing putative key value, "
476 << "at char " << (l - line) << ", line " << line_no;
477 }
478 errors->push_back(oss.str());
479 return NULL;
480 }
481 else if ((c == '=') && (!escaping)) {
482 key = normalize_key_name(key);
483 if (key.empty()) {
484 ostringstream oss;
485 oss << "error parsing key name: no key name found? "
486 << "at char " << (l - line) << ", line " << line_no;
487 errors->push_back(oss.str());
488 return NULL;
489 }
490 state = ACCEPT_VAL_START;
491 }
492 else if ((c == '\\') && (!escaping)) {
493 escaping = true;
494 }
495 else {
496 escaping = false;
497 key += c;
498 }
499 break;
500 case ACCEPT_VAL_START:
501 if (c == '\0')
502 return new ConfLine(key, val, newsection, comment, line_no);
503 else if ((c == '#') || (c == ';'))
504 state = ACCEPT_COMMENT_TEXT;
505 else if (c == '"')
506 state = ACCEPT_QUOTED_VAL;
507 else if (isspace(c)) {
508 // ignore whitespace
509 }
510 else {
511 // try to accept character as a val
512 state = ACCEPT_UNQUOTED_VAL;
513 --l;
514 }
515 break;
516 case ACCEPT_UNQUOTED_VAL:
517 if (c == '\0') {
518 if (escaping) {
519 ostringstream oss;
520 oss << "error parsing value name: unterminated escape sequence "
521 << "at char " << (l - line) << ", line " << line_no;
522 errors->push_back(oss.str());
523 return NULL;
524 }
525 trim_whitespace(val, false);
526 return new ConfLine(key, val, newsection, comment, line_no);
527 }
528 else if (((c == '#') || (c == ';')) && (!escaping)) {
529 trim_whitespace(val, false);
530 state = ACCEPT_COMMENT_TEXT;
531 }
532 else if ((c == '\\') && (!escaping)) {
533 escaping = true;
534 }
535 else {
536 escaping = false;
537 val += c;
538 }
539 break;
540 case ACCEPT_QUOTED_VAL:
541 if (c == '\0') {
542 ostringstream oss;
543 oss << "found opening quote for value, but not the closing quote. "
544 << "line " << line_no;
545 errors->push_back(oss.str());
546 return NULL;
547 }
548 else if ((c == '"') && (!escaping)) {
549 state = ACCEPT_COMMENT_START;
550 }
551 else if ((c == '\\') && (!escaping)) {
552 escaping = true;
553 }
554 else {
555 escaping = false;
556 // Add anything, including whitespace.
557 val += c;
558 }
559 break;
560 case ACCEPT_COMMENT_START:
561 if (c == '\0') {
562 return new ConfLine(key, val, newsection, comment, line_no);
563 }
564 else if ((c == '#') || (c == ';')) {
565 state = ACCEPT_COMMENT_TEXT;
566 }
567 else if (isspace(c)) {
568 // ignore whitespace
569 }
570 else {
571 ostringstream oss;
572 oss << "unexpected character at char " << (l - line) << " of line "
573 << line_no;
574 errors->push_back(oss.str());
575 return NULL;
576 }
577 break;
578 case ACCEPT_COMMENT_TEXT:
579 if (c == '\0')
580 return new ConfLine(key, val, newsection, comment, line_no);
581 else
582 comment += c;
583 break;
584 default:
585 ceph_abort();
586 break;
587 }
588 assert(c != '\0'); // We better not go past the end of the input string.
589 }
590}