Mercurial > irccd
view libcommon/irccd/ini.cpp @ 700:91bc29e87399
Irccd: use Boost.Predef, closes #805 @1h
author | David Demelier <markand@malikania.fr> |
---|---|
date | Wed, 09 May 2018 22:34:47 +0200 |
parents | af963ff03c06 |
children |
line wrap: on
line source
/* * ini.cpp -- extended .ini file parser * * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include <cctype> #include <cstring> #include <iostream> #include <iterator> #include <fstream> #include <sstream> #include <stdexcept> #include <irccd/sysconfig.hpp> #include <boost/predef.h> // for PathIsRelative. #if BOOST_OS_WINDOWS # include <shlwapi.h> #endif #include "ini.hpp" namespace irccd { namespace ini { namespace { using stream_iterator = std::istreambuf_iterator<char>; using token_iterator = std::vector<token>::const_iterator; inline bool is_absolute(const std::string& path) noexcept { #if BOOST_OS_WINDOWS return !PathIsRelative(path.c_str()); #else return path.size() > 0 && path[0] == '/'; #endif } inline bool is_quote(char c) noexcept { return c == '\'' || c == '"'; } inline bool is_space(char c) noexcept { // Custom version because std::isspace includes \n as space. return c == ' ' || c == '\t'; } inline bool is_list(char c) noexcept { return c == '(' || c == ')' || c == ','; } inline bool is_reserved(char c) noexcept { return is_list(c) || is_quote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '='; } void analyse_line(int& line, int& column, stream_iterator& it) noexcept { assert(*it == '\n'); ++ line; ++ it; column = 0; } void analyse_comment(int& column, stream_iterator& it, stream_iterator end) noexcept { assert(*it == '#'); while (it != end && *it != '\n') { ++ column; ++ it; } } void analyse_spaces(int& column, stream_iterator& it, stream_iterator end) noexcept { assert(is_space(*it)); while (it != end && is_space(*it)) { ++ column; ++ it; } } void analyse_list(tokens& list, int line, int& column, stream_iterator& it) noexcept { assert(is_list(*it)); switch (*it++) { case '(': list.emplace_back(token::list_begin, line, column++); break; case ')': list.emplace_back(token::list_end, line, column++); break; case ',': list.emplace_back(token::comma, line, column++); break; default: break; } } void analyse_section(tokens& list, int& line, int& column, stream_iterator& it, stream_iterator end) { assert(*it == '['); std::string value; int save = column; // Read section name. ++ it; while (it != end && *it != ']') { if (*it == '\n') throw exception(line, column, "section not terminated, missing ']'"); if (is_reserved(*it)) throw exception(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'"); ++ column; value += *it++; } if (it == end) throw exception(line, column, "section name expected after '[', got <EOF>"); if (value.empty()) throw exception(line, column, "empty section name"); // Remove ']'. ++ it; list.emplace_back(token::section, line, save, std::move(value)); } void analyse_assign(tokens& list, int& line, int& column, stream_iterator& it) { assert(*it == '='); list.push_back({ token::assign, line, column++ }); ++ it; } void analyse_quoted_word(tokens& list, int& line, int& column, stream_iterator& it, stream_iterator end) { std::string value; int save = column; char quote = *it++; while (it != end && *it != quote) { // TODO: escape sequence ++ column; value += *it++; } if (it == end) throw exception(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>"); // Remove quote. ++ it; list.push_back({ token::quoted_word, line, save, std::move(value) }); } void analyse_word(tokens& list, int& line, int& column, stream_iterator& it, stream_iterator end) { assert(!is_reserved(*it)); std::string value; int save = column; while (it != end && !std::isspace(*it) && !is_reserved(*it)) { ++ column; value += *it++; } list.push_back({ token::word, line, save, std::move(value) }); } void analyse_include(tokens& list, int& line, int& column, stream_iterator& it, stream_iterator end) { assert(*it == '@'); std::string include; int save = column; // Read include. ++ it; while (it != end && !is_space(*it)) { ++ column; include += *it++; } if (include != "include") throw exception(line, column, "expected include after '@' token"); list.push_back({ token::include, line, save }); } void parse_option_value_simple(option& option, token_iterator& it) { assert(it->type() == token::word || it->type() == token::quoted_word); option.push_back((it++)->value()); } void parse_option_value_list(option& option, token_iterator& it, token_iterator end) { assert(it->type() == token::list_begin); token_iterator save = it++; while (it != end && it->type() != token::list_end) { switch (it->type()) { case token::comma: // Previous must be a word. if (it[-1].type() != token::word && it[-1].type() != token::quoted_word) throw exception(it->line(), it->column(), "unexpected comma after '" + it[-1].value() + "'"); ++ it; break; case token::word: case token::quoted_word: option.push_back((it++)->value()); break; default: throw exception(it->line(), it->column(), "unexpected '" + it[-1].value() + "' in list construct"); break; } } if (it == end) throw exception(save->line(), save->column(), "unterminated list construct"); // Remove ). ++ it; } void parse_option(section& sc, token_iterator& it, token_iterator end) { option option(it->value()); token_iterator save(it); // No '=' or something else? if (++it == end) throw exception(save->line(), save->column(), "expected '=' assignment, got <EOF>"); if (it->type() != token::assign) throw exception(it->line(), it->column(), "expected '=' assignment, got " + it->value()); // Empty options are allowed so just test for words. if (++it != end) { if (it->type() == token::word || it->type() == token::quoted_word) parse_option_value_simple(option, it); else if (it->type() == token::list_begin) parse_option_value_list(option, it, end); } sc.push_back(std::move(option)); } void parse_include(document& doc, const std::string& path, token_iterator& it, token_iterator end) { token_iterator save(it); if (++it == end) throw exception(save->line(), save->column(), "expected file name after '@include' statement, got <EOF>"); if (it->type() != token::word && it->type() != token::quoted_word) throw exception(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value()); std::string value = (it++)->value(); std::string file; if (!is_absolute(value)) { #if BOOST_OS_WINDOWS file = path + "\\" + value; #else file = path + "/" + value; #endif } else file = value; for (const auto& sc : read_file(file)) doc.push_back(sc); } void parse_section(document& doc, token_iterator& it, token_iterator end) { section sc(it->value()); // Skip [section]. ++ it; // Read until next section. while (it != end && it->type() != token::section) { if (it->type() != token::word) throw exception(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition"); parse_option(sc, it, end); } doc.push_back(std::move(sc)); } } // !namespace tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end) { tokens list; int line = 1; int column = 0; while (it != end) { if (*it == '\n') analyse_line(line, column, it); else if (*it == '#') analyse_comment(column, it, end); else if (*it == '[') analyse_section(list, line, column, it, end); else if (*it == '=') analyse_assign(list, line, column, it); else if (is_space(*it)) analyse_spaces(column, it, end); else if (*it == '@') analyse_include(list, line, column, it, end); else if (is_quote(*it)) analyse_quoted_word(list, line, column, it, end); else if (is_list(*it)) analyse_list(list, line, column, it); else analyse_word(list, line, column, it, end); } return list; } tokens analyse(std::istream& stream) { return analyse(std::istreambuf_iterator<char>(stream), {}); } document parse(const tokens& tokens, const std::string& path) { document doc; token_iterator it = tokens.cbegin(); token_iterator end = tokens.cend(); while (it != end) { switch (it->type()) { case token::include: parse_include(doc, path, it, end); break; case token::section: parse_section(doc, it, end); break; default: throw exception(it->line(), it->column(), "unexpected '" + it->value() + "' on root document"); } } return doc; } document read_file(const std::string& filename) { // Get parent path. auto parent = filename; auto pos = parent.find_last_of("/\\"); if (pos != std::string::npos) parent.erase(pos); else parent = "."; std::ifstream input(filename); if (!input) throw exception(0, 0, std::strerror(errno)); return parse(analyse(input), parent); } document read_string(const std::string& buffer) { std::istringstream iss(buffer); return parse(analyse(iss)); } void dump(const tokens& tokens) { for (const token& token: tokens) { // TODO: add better description std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl; } } } // !ini } // !irccd