From e382f71aea92d05578a41bd6469601549f64561b Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 7 Oct 2021 00:39:23 +0100 Subject: [PATCH] Add implementation of config file parser lifted from Boost --- components/CMakeLists.txt | 2 +- components/files/configfileparser.cpp | 289 ++++++++++++++++++++++ components/files/configfileparser.hpp | 17 ++ components/files/configurationmanager.cpp | 3 +- 4 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 components/files/configfileparser.cpp create mode 100644 components/files/configfileparser.hpp diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7983de6190..ccac8f8cb7 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -105,7 +105,7 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF() add_component_dir (files linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager escape - lowlevelfile constrainedfilestream memorystream + lowlevelfile constrainedfilestream memorystream configfileparser ) add_component_dir (compiler diff --git a/components/files/configfileparser.cpp b/components/files/configfileparser.cpp new file mode 100644 index 0000000000..7c43e1f67c --- /dev/null +++ b/components/files/configfileparser.cpp @@ -0,0 +1,289 @@ +#include "configfileparser.hpp" + +#include +#include + +namespace Files +{ + namespace + { + /** Standalone parser for config files in ini-line format. + The parser is a model of single-pass lvalue iterator, and + default constructor creates past-the-end-iterator. The typical usage is: + config_file_iterator i(is, ... set of options ...), e; + for(; i !=e; ++i) { + *i; + } + + Syntax conventions: + + - config file can not contain positional options + - '#' is comment character: it is ignored together with + the rest of the line. + - variable assignments are in the form + name '=' value. + spaces around '=' are trimmed. + - Section names are given in brackets. + + The actual option name is constructed by combining current section + name and specified option name, with dot between. If section_name + already contains dot at the end, new dot is not inserted. For example: + @verbatim + [gui.accessibility] + visual_bell=yes + @endverbatim + will result in option "gui.accessibility.visual_bell" with value + "yes" been returned. + + TODO: maybe, we should just accept a pointer to options_description + class. + */ + class common_config_file_iterator + : public boost::eof_iterator + { + public: + common_config_file_iterator() { found_eof(); } + common_config_file_iterator( + const std::set& allowed_options, + bool allow_unregistered = false); + + virtual ~common_config_file_iterator() {} + + public: // Method required by eof_iterator + + void get(); + +#if BOOST_WORKAROUND(_MSC_VER, <= 1900) + void decrement() {} + void advance(difference_type) {} +#endif + + protected: // Stubs for derived classes + + // Obtains next line from the config file + // Note: really, this design is a bit ugly + // The most clean thing would be to pass 'line_iterator' to + // constructor of this class, but to avoid templating this class + // we'd need polymorphic iterator, which does not exist yet. + virtual bool getline(std::string&) { return false; } + + protected: + /** Adds another allowed option. If the 'name' ends with + '*', then all options with the same prefix are + allowed. For example, if 'name' is 'foo*', then 'foo1' and + 'foo_bar' are allowed. */ + void add_option(const char* name); + + // Returns true if 's' is a registered option name. + bool allowed_option(const std::string& s) const; + + // That's probably too much data for iterator, since + // it will be copied, but let's not bother for now. + std::set allowed_options; + // Invariant: no element is prefix of other element. + std::set allowed_prefixes; + std::string m_prefix; + bool m_allow_unregistered; + }; + + common_config_file_iterator::common_config_file_iterator( + const std::set& allowed_options, + bool allow_unregistered) + : allowed_options(allowed_options), + m_allow_unregistered(allow_unregistered) + { + for (std::set::const_iterator i = allowed_options.begin(); + i != allowed_options.end(); + ++i) + { + add_option(i->c_str()); + } + } + + void common_config_file_iterator::add_option(const char* name) + { + std::string s(name); + assert(!s.empty()); + if (*s.rbegin() == '*') { + s.resize(s.size() - 1); + bool bad_prefixes(false); + // If 's' is a prefix of one of allowed suffix, then + // lower_bound will return that element. + // If some element is prefix of 's', then lower_bound will + // return the next element. + std::set::iterator i = allowed_prefixes.lower_bound(s); + if (i != allowed_prefixes.end()) { + if (i->find(s) == 0) + bad_prefixes = true; + } + if (i != allowed_prefixes.begin()) { + --i; + if (s.find(*i) == 0) + bad_prefixes = true; + } + if (bad_prefixes) + boost::throw_exception(bpo::error("options '" + std::string(name) + "' and '" + + *i + "*' will both match the same " + "arguments from the configuration file")); + allowed_prefixes.insert(s); + } + } + + std::string trim_ws(const std::string& s) + { + std::string::size_type n, n2; + n = s.find_first_not_of(" \t\r\n"); + if (n == std::string::npos) + return std::string(); + else { + n2 = s.find_last_not_of(" \t\r\n"); + return s.substr(n, n2 - n + 1); + } + } + + void common_config_file_iterator::get() + { + std::string s; + std::string::size_type n; + bool found = false; + + while (this->getline(s)) { + + // strip '#' comments and whitespace + if ((n = s.find('#')) != std::string::npos) + s = s.substr(0, n); + s = trim_ws(s); + + if (!s.empty()) { + // Handle section name + if (*s.begin() == '[' && *s.rbegin() == ']') { + m_prefix = s.substr(1, s.size() - 2); + if (*m_prefix.rbegin() != '.') + m_prefix += '.'; + } + else if ((n = s.find('=')) != std::string::npos) { + + std::string name = m_prefix + trim_ws(s.substr(0, n)); + std::string value = trim_ws(s.substr(n + 1)); + + bool registered = allowed_option(name); + if (!registered && !m_allow_unregistered) + boost::throw_exception(bpo::unknown_option(name)); + + found = true; + this->value().string_key = name; + this->value().value.clear(); + this->value().value.push_back(value); + this->value().unregistered = !registered; + this->value().original_tokens.clear(); + this->value().original_tokens.push_back(name); + this->value().original_tokens.push_back(value); + break; + + } else { + boost::throw_exception(bpo::invalid_config_file_syntax(s, bpo::invalid_syntax::unrecognized_line)); + } + } + } + if (!found) + found_eof(); + } + + bool common_config_file_iterator::allowed_option(const std::string& s) const + { + std::set::const_iterator i = allowed_options.find(s); + if (i != allowed_options.end()) + return true; + // If s is "pa" where "p" is allowed prefix then + // lower_bound should find the element after "p". + // This depends on 'allowed_prefixes' invariant. + i = allowed_prefixes.lower_bound(s); + if (i != allowed_prefixes.begin() && s.find(*--i) == 0) + return true; + return false; + } + + + + template + class basic_config_file_iterator : public Files::common_config_file_iterator { + public: + basic_config_file_iterator() + { + found_eof(); + } + + /** Creates a config file parser for the specified stream. + */ + basic_config_file_iterator(std::basic_istream& is, + const std::set& allowed_options, + bool allow_unregistered = false); + + private: // base overrides + + bool getline(std::string&); + + private: // internal data + std::shared_ptr > is; + }; + + template + basic_config_file_iterator:: + basic_config_file_iterator(std::basic_istream& is, + const std::set& allowed_options, + bool allow_unregistered) + : common_config_file_iterator(allowed_options, allow_unregistered) + { + this->is.reset(&is, bpo::detail::null_deleter()); + get(); + } + + template + bool basic_config_file_iterator::getline(std::string& s) + { + std::basic_string in; + if (std::getline(*is, in)) { + s = bpo::to_internal(in); + return true; + } else { + return false; + } + } + } + + template + bpo::basic_parsed_options + parse_config_file(std::basic_istream& is, + const bpo::options_description& desc, + bool allow_unregistered) + { + std::set allowed_options; + + const std::vector >& options = desc.options(); + for (unsigned i = 0; i < options.size(); ++i) + { + const bpo::option_description& d = *options[i]; + + if (d.long_name().empty()) + boost::throw_exception( + bpo::error("abbreviated option names are not permitted in options configuration files")); + + allowed_options.insert(d.long_name()); + } + + // Parser return char strings + bpo::parsed_options result(&desc); + copy(basic_config_file_iterator( + is, allowed_options, allow_unregistered), + basic_config_file_iterator(), + back_inserter(result.options)); + // Convert char strings into desired type. + return bpo::basic_parsed_options(result); + } + + template + bpo::basic_parsed_options + parse_config_file(std::basic_istream& is, + const bpo::options_description& desc, + bool allow_unregistered); +} diff --git a/components/files/configfileparser.hpp b/components/files/configfileparser.hpp new file mode 100644 index 0000000000..60ab27a6d4 --- /dev/null +++ b/components/files/configfileparser.hpp @@ -0,0 +1,17 @@ +#ifndef COMPONENTS_FILES_CONFIGFILEPARSER_HPP +#define COMPONENTS_FILES_CONFIGFILEPARSER_HPP + +#include + +namespace Files +{ + +namespace bpo = boost::program_options; + +template +bpo::basic_parsed_options parse_config_file(std::basic_istream&, const bpo::options_description&, + bool allow_unregistered = false); + +} + +#endif // COMPONENTS_FILES_CONFIGFILEPARSER_HPP \ No newline at end of file diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 35679ef293..7fd134cf0f 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -1,6 +1,7 @@ #include "configurationmanager.hpp" #include +#include #include #include @@ -239,7 +240,7 @@ bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, configFileStream.push(configFileStreamUnfiltered); if (configFileStreamUnfiltered.is_open()) { - boost::program_options::store(boost::program_options::parse_config_file( + boost::program_options::store(Files::parse_config_file( configFileStream, description, true), variables); return true;