inicpp
C++ parser of INI files with schema validation.
parser.cpp
1 #include "parser.h"
2 
3 namespace inicpp
4 {
5  size_t parser::find_first_nonescaped(const std::string &str, char ch)
6  {
7  size_t result = std::string::npos;
8  bool escaped = false;
9 
10  for (size_t i = 0; i < str.length(); ++i) {
11  if (escaped) {
12  // escaped character, do not do anything
13  escaped = false;
14  } else if (str[i] == '\\') {
15  // next character will be escaped
16  escaped = true;
17  } else if (str[i] == ch) {
18  // we tracked down non escaped character... return it
19  result = i;
20  break;
21  }
22  }
23 
24  return result;
25  }
26 
27  size_t parser::find_last_escaped(const std::string &str, char ch)
28  {
29  size_t result = std::string::npos;
30  bool escaped = false;
31 
32  for (size_t i = 0; i < str.length(); ++i) {
33  if (escaped) {
34  // escaped character, do not do anything
35  escaped = false;
36 
37  if (str[i] == ch) {
38  result = i;
39  }
40  } else if (str[i] == '\\') {
41  // next character will be escaped
42  escaped = true;
43  }
44  }
45 
46  return result;
47  }
48 
49  std::string parser::unescape(const std::string &str)
50  {
51  std::string result = str;
52  bool escaped = false;
53 
54  auto it = result.begin();
55  while (it != result.end()) {
56  if (escaped) {
57  // escaped character, it should remain in string
58  escaped = false;
59  } else if (*it == '\\') {
60  // next character will be escaped, so delete escaping character
61  escaped = true;
62  it = result.erase(it);
63  continue;
64  }
65 
66  // we have while cycle so we have to do this manually
67  ++it;
68  }
69 
70  return result;
71  }
72 
73  std::string parser::delete_comment(const std::string &str)
74  {
75  return str.substr(0, find_first_nonescaped(str, ';'));
76  }
77 
78  std::vector<std::string> parser::parse_option_list(const std::string &str)
79  {
80  using namespace string_utils;
81 
82  std::string searched = str;
83  std::vector<std::string> result;
84  char delim = ',';
85 
86  size_t pos = find_first_nonescaped(searched, ',');
87  if (pos == std::string::npos) {
88  // if no escaped strokes are present in given string,
89  // try to use colon
90  delim = ':';
91  }
92 
93  while (true) {
94  pos = find_first_nonescaped(searched, delim);
95 
96  // extract option value and process it
97  std::string value = searched.substr(0, pos);
98  value = left_trim(value);
99  // check if last escaped character is whitespace
100  size_t whitespace_pos = find_last_escaped(value, ' ');
101  value = right_trim(value);
102  if (whitespace_pos != std::string::npos) {
103  // last character is escaped whitespace
104  if (value.size() == whitespace_pos) {
105  value.push_back(' ');
106  }
107  }
108  // finally unescape
109  value = unescape(value);
110 
111  // save extracted and processed option value and cut searched string
112  result.push_back(value);
113 
114  if (pos == std::string::npos) {
115  // no delimiter found
116  break;
117  }
118  searched = searched.substr(pos + 1);
119  }
120 
121  return result;
122  }
123 
124  void parser::handle_links(
125  const config &cfg, const section &last_section, std::vector<std::string> &option_val_list, size_t line_number)
126  {
127  using namespace string_utils;
128 
129  for (auto &opt_value : option_val_list) {
130  if (starts_with(opt_value, "${") && ends_with(opt_value, "}")) {
131  std::string link = opt_value.substr(2, opt_value.length() - 3);
132  size_t delim = find_first_nonescaped(link, '#');
133 
134  // link always has to be in format "section#option"
135  // section and option cannot be empty
136  if (delim == std::string::npos || (delim + 1) == link.length()) {
137  throw parser_exception("Bad format of link on line " + std::to_string(line_number));
138  }
139 
140  std::string sect_link = link.substr(0, delim);
141  std::string opt_link = link.substr(delim + 1);
142 
143  if (sect_link.empty()) {
144  throw parser_exception(
145  "Section name in link cannot be empty on line " + std::to_string(line_number));
146  }
147 
148  // find section with name specifid in link
149  const section *selected_section = nullptr;
150  if (last_section.get_name() == sect_link) {
151  selected_section = &last_section;
152  } else if (cfg.contains(sect_link)) {
153  selected_section = &cfg[sect_link];
154  } else {
155  throw parser_exception("Bad link on line " + std::to_string(line_number));
156  }
157 
158  // from selected section take appropriate option and set its value to options list
159  if (selected_section->contains(opt_link)) {
160  opt_value = selected_section->operator[](opt_link).get<string_ini_t>();
161  } else {
162  throw parser_exception("Option name in link not found on line " + std::to_string(line_number));
163  }
164  }
165  }
166  }
167 
168  void parser::validate_identifier(const std::string &str, size_t line_number)
169  {
170  std::regex reg_expr("^[a-zA-Z.$:][-a-zA-Z0-9_~.:$ ]*$");
171  if (!std::regex_match(str, reg_expr)) {
172  throw parser_exception("Identifier contains forbidden characters on line " + std::to_string(line_number));
173  }
174  }
175 
176  config parser::internal_load(std::istream &str)
177  {
178  using namespace string_utils;
179 
180  config cfg;
181  std::shared_ptr<section> last_section = nullptr;
182  std::string line;
183  size_t line_number = 0;
184 
185  while (std::getline(str, line)) {
186  line_number++;
187 
188  // if there was comment delete it
189  line = delete_comment(line);
190  line = left_trim(line);
191 
192  if (line.empty()) { // empty line
193  continue;
194  } else if (starts_with(line, "[")) { // start of section
195  line = right_trim(line);
196  if (ends_with(line, "]")) {
197  // empty section name cannot be present
198  if (line.length() == 2) {
199  throw parser_exception("Section name cannot be empty on line " + std::to_string(line_number));
200  }
201 
202  // if there is cached section, save it
203  if (last_section != nullptr) {
204  cfg.add_section(*last_section);
205  }
206 
207  // extract name and validate it and finally create section object
208  std::string sect_name = unescape(line.substr(1, line.length() - 2));
209  validate_identifier(sect_name, line_number);
210  last_section = std::make_shared<section>(sect_name);
211  } else {
212  throw parser_exception("Section not ended on line " + std::to_string(line_number));
213  }
214  } else { // option
215  size_t opt_delim = find_first_nonescaped(line, '=');
216  if (opt_delim == std::string::npos) {
217  throw parser_exception("Unknown element option expected on line " + std::to_string(line_number));
218  }
219 
220  // if there is no opened section, option has no parent section
221  if (last_section == nullptr) {
222  throw parser_exception("Option not in section on line " + std::to_string(line_number));
223  }
224 
225  // equals character was right at the end of line, should not be
226  if ((opt_delim + 1) == line.length()) {
227  throw parser_exception("Option value cannot be empty on line " + std::to_string(line_number));
228  }
229 
230  // retrieve option name and value from line
231  std::string option_name = unescape(trim(line.substr(0, opt_delim)));
232  std::string option_val = line.substr(opt_delim + 1);
233 
234  // validate option name
235  validate_identifier(option_name, line_number);
236 
237  if (option_name.empty()) {
238  throw parser_exception("Option name cannot be empty on line " + std::to_string(line_number));
239  }
240 
241  auto option_val_list = parse_option_list(option_val);
242  if (option_val_list.empty()) {
243  throw parser_exception("Option value cannot be empty on line " + std::to_string(line_number));
244  }
245 
246  handle_links(cfg, *last_section, option_val_list, line_number);
247 
248  // and finally create option and store it in current section
249  option opt(option_name, option_val_list);
250  last_section->add_option(opt);
251  }
252  }
253 
254  // if there is cached section we have to add it to created config too
255  if (last_section != nullptr) {
256  cfg.add_section(*last_section);
257  }
258 
259  return cfg;
260  }
261 
262  void parser::internal_save(const config &cfg, const schema &schm, std::ostream &str)
263  {
264  for (auto &sect : cfg) {
265  bool contains = schm.contains(sect.get_name());
266  if (!contains) {
267  // write section which is not in schema
268  // if this happens we can safely write all section and its option to output
269  // we do not have to go through them and write their additional info
270  str << sect;
271  continue;
272  }
273 
274  // if schema contains section from config, write additional info and name first
275  auto &sect_schema = schm[sect.get_name()];
276  sect_schema.write_additional_info(str);
277  sect_schema.write_section_name(str);
278 
279  // go through options and write them to output with info from option_schema
280  for (auto &opt : sect) {
281  if (sect_schema.contains(opt.get_name())) {
282  // if option is in section_schema, then write additional info
283  sect_schema[opt.get_name()].write_additional_info(str);
284  }
285 
286  // write option name and value to output
287  str << opt;
288  }
289 
290  // get through option_schema in appropriate section_schema
291  for (size_t i = 0; i < sect_schema.size(); ++i) {
292  auto &opt_schema = sect_schema[i];
293  if (sect.contains(opt_schema.get_name())) {
294  // already written to output stream
295  continue;
296  }
297 
298  // option with this name does not exist in config,
299  // so write its option_schema interpretation
300  str << opt_schema;
301  }
302  }
303  }
304 
305  config parser::load(const std::string &str)
306  {
307  std::istringstream input(str);
308  return internal_load(input);
309  }
310 
311  config parser::load(const std::string &str, const schema &schm, schema_mode mode)
312  {
313  std::istringstream input(str);
314  config cfg = internal_load(input);
315  cfg.validate(schm, mode);
316  return cfg;
317  }
318 
319  config parser::load(std::istream &str)
320  {
321  return internal_load(str);
322  }
323 
324  config parser::load(std::istream &str, const schema &schm, schema_mode mode)
325  {
326  config cfg = internal_load(str);
327  cfg.validate(schm, mode);
328  return cfg;
329  }
330 
331  config parser::load_file(const std::string &file)
332  {
333  std::ifstream input(file);
334  if (input.fail()) {
335  throw parser_exception("File reading error");
336  }
337 
338  return internal_load(input);
339  }
340 
341  config parser::load_file(const std::string &file, const schema &schm, schema_mode mode)
342  {
343  std::ifstream input(file);
344  if (input.fail()) {
345  throw parser_exception("File reading error");
346  }
347 
348  config cfg = internal_load(input);
349  cfg.validate(schm, mode);
350  return cfg;
351  }
352 
353  void parser::save(const config &cfg, const std::string &file)
354  {
355  std::ofstream output(file);
356  output << cfg;
357  output.close();
358  }
359 
360  void parser::save(const config &cfg, std::ostream &str)
361  {
362  str << cfg;
363  }
364 
365  void parser::save(const config &cfg, const schema &schm, const std::string &file)
366  {
367  std::ofstream output(file);
368  internal_save(cfg, schm, output);
369  output.close();
370  }
371 
372  void parser::save(const config &cfg, const schema &schm, std::ostream &str)
373  {
374  internal_save(cfg, schm, str);
375  }
376 
377  void parser::save(const schema &schm, const std::string &file)
378  {
379  std::ofstream output(file);
380  output << schm;
381  output.close();
382  }
383 
384  void parser::save(const schema &schm, std::ostream &str)
385  {
386  str << schm;
387  }
388 }
std::string trim(const std::string &str)
std::string right_trim(const std::string &str)
bool starts_with(const std::string &str, const std::string &search_str)
Definition: config.h:15
static config load_file(const std::string &file)
Definition: parser.cpp:331
bool ends_with(const std::string &str, const std::string &search_str)
static void save(const config &cfg, const std::string &file)
Definition: parser.cpp:353
void validate(const schema &schm, schema_mode mode)
Definition: config.cpp:164
std::string left_trim(const std::string &str)
static config load(const std::string &str)
Definition: parser.cpp:305