diff --git a/libgnucash/app-utils/gnc-optiondb.cpp b/libgnucash/app-utils/gnc-optiondb.cpp index 239a21f068..d09fa2317d 100644 --- a/libgnucash/app-utils/gnc-optiondb.cpp +++ b/libgnucash/app-utils/gnc-optiondb.cpp @@ -196,30 +196,265 @@ GncOptionDB::save_option_scheme(std::ostream& oss, return oss; } -std::istream& -GncOptionDB::load_option_scheme(std::istream& iss) noexcept + +static inline bool constexpr +is_eol(char c) { - std::string section{classifier_size_max}; - std::string name{classifier_size_max}; - iss.ignore(strlen(scheme_tags[0]) + 7, '\n'); //throw away the scheme noise; - iss >> section; // Whitespace automatically discarded. - iss >> name; // Ditto - if (section.size() > 2) - section = section.substr(1, section.size() - 2); //Trim the quotes - if (name.size() > 2 + strlen(scheme_tags[2])) - name = name.substr(1, name.size() - (2 + strlen(scheme_tags[2]))); + return c == '\n'; +} + +static inline bool constexpr +is_whitespace(char c) +{ + return c == ' ' || c == '\n' || c == '\t'; +} + +static inline bool constexpr +is_begin_paren(char c) +{ + return c == '('; +} + +static inline bool constexpr +is_end_paren(char c) +{ + return c == ')'; +} + +static inline bool constexpr +is_double_quote(char c) +{ + return c == '"'; +} + +static inline bool constexpr +is_single_quote(char c) +{ + return c == '\''; +} + +static inline bool constexpr +is_semicolon(char c) +{ + return c == ';'; +} + +static inline bool constexpr +is_delim(char c) +{ + return is_begin_paren(c) || is_end_paren(c) || is_whitespace(c) || + is_single_quote(c) || is_double_quote(c) || is_semicolon(c); +} + +static std::string +scan_scheme_symbol_from_streambuf(std::streambuf* sbuf) +{ + std::string retval; + while(sbuf->in_avail() && !is_delim(sbuf->sgetc())) + retval += sbuf->sbumpc(); + return retval; +} + +static inline void constexpr +consume_scheme_comment(std::streambuf* sbuf) +{ + while (sbuf->in_avail() && !is_eol(sbuf->sgetc())) + sbuf->sbumpc(); +} + +static inline std::string +scan_scheme_string_from_streambuf(std::streambuf* sbuf) +{ + std::string retval{static_cast(sbuf->sbumpc())}; + while(sbuf->in_avail() && !is_double_quote(sbuf->sgetc())) + retval += sbuf->sbumpc(); + retval += sbuf->sbumpc(); // Add the closing quote. + return retval; +} + +static inline void constexpr +consume_scheme_whitespace(std::streambuf* sbuf) +{ + while (sbuf->in_avail() && is_whitespace(sbuf->sgetc())) + sbuf->sbumpc(); +} + +enum class IdentType +{ + NAME, //no introducing mark + CONST, //introduced with single quote + STRING, //delimited by double-quotes. + LIST, //introduced ' and delimited by parentheses + FORM //delimited by parentheses without ' introduction. +}; + +struct SchemeId +{ + IdentType m_type; + std::string m_name; + std::vector m_ids; +}; + +/** + * Scheme Parse Tree + * An identifier is a string and a type (name, const, string, or form). A Form + * + */ + +static void scan_scheme_id_from_streambuf(std::streambuf* sbuf, SchemeId& id); + +static void +scan_scheme_form_from_streambuf(std::streambuf* sbuf, SchemeId& id) +{ + sbuf->sbumpc(); + if (!sbuf->in_avail()) + return; + char c = sbuf->sgetc(); + while (sbuf->in_avail() && !is_end_paren(c)) + { + SchemeId next_id; + scan_scheme_id_from_streambuf(sbuf, next_id); + if (id.m_name.empty() && next_id.m_type == IdentType::NAME) + { + id.m_name = std::move(next_id.m_name); + continue; + } + id.m_ids.emplace_back(std::move(next_id)); + if (!sbuf->in_avail()) + { + std::string err{"End of streambuf before end of form "}; + err += id.m_name; + throw std::runtime_error(err); + } + c = sbuf->sgetc(); + } + sbuf->sbumpc(); +} + +static void +scan_scheme_list_from_streambuf(std::streambuf* sbuf, std::string& str) +{ + + consume_scheme_whitespace(sbuf); + if (!sbuf->in_avail()) + return; + char c = sbuf->sgetc(); + while (sbuf->in_avail() && !is_end_paren(c)) + { + str += static_cast(sbuf->sbumpc()); + if (!sbuf->in_avail()) + return; + c = sbuf->sgetc(); + } + str += static_cast(sbuf->sbumpc()); +} + +static void +scan_scheme_id_from_streambuf(std::streambuf* sbuf, SchemeId& id) +{ + consume_scheme_whitespace(sbuf); + if (!sbuf->in_avail()) + return; + auto c{sbuf->sgetc()}; + switch(c) + { + case ';': + consume_scheme_comment(sbuf); + break; + case '"': + id.m_type = IdentType::STRING; + id.m_name = scan_scheme_string_from_streambuf(sbuf); + break; + case '\'': + { + std::string value{static_cast(sbuf->sbumpc())}; + if (sbuf->sgetc() == '(') + { + id.m_type == IdentType::LIST; + scan_scheme_list_from_streambuf(sbuf, value); + if (value.back() != ')') + throw std::runtime_error("End of streambuf before end of form "); + } + else if (sbuf->sgetc() == '"') + throw std::runtime_error("Malformed scheme particle starts '\""); + else + { + id.m_type = IdentType::CONST; + value += scan_scheme_symbol_from_streambuf(sbuf); + } + id.m_name = std::move(value); + break; + } + case '(': + id.m_type = IdentType::FORM; + scan_scheme_form_from_streambuf(sbuf, id); + break; + default: + id.m_type = IdentType::NAME; + id.m_name = scan_scheme_symbol_from_streambuf(sbuf); + break; + } + return; +} + +static inline std::string +unquote_scheme_string(const std::string& str) +{ + if (str.front() == '"' && str.back() == '"') + return str.substr(1, str.size() - 2); + + return str; +} + +std::istream& +GncOptionDB::load_option_scheme(std::istream& iss) +{ + auto sbuf{iss.rdbuf()}; + SchemeId toplevel; + bool form_found = false; + while (sbuf->in_avail() && !form_found) + { + scan_scheme_id_from_streambuf(sbuf, toplevel); + if (toplevel.m_type == IdentType::FORM && + toplevel.m_name == "let" && toplevel.m_ids.size() == 2) + if (const auto& let_block{toplevel.m_ids[0]}; + let_block.m_ids.size() == 1) + if (const auto& first_form{let_block.m_ids[0]}; + first_form.m_name == "option") + form_found = true; + } + const auto& classifier = toplevel.m_ids[0].m_ids[0].m_ids[0].m_ids; + if (classifier.size() != 3) + throw std::runtime_error("Malformed option classifier."); + const auto& section = unquote_scheme_string(classifier[1].m_name); + const auto& name = unquote_scheme_string(classifier[2].m_name); auto option = find_option(section.c_str(), name.c_str()); + std::string option_str{section}; + option_str += ':'; + option_str += name; if (!option) { - std::cerr << "Option " << section << ":" << name << " not found." << std::endl; - iss.ignore(stream_max, '\n'); // No option, discard the line - iss.ignore(stream_max, '\n'); // And the trailing newline - return iss; + std::string err{"Option not found: "}; + err += option_str; + throw std::runtime_error(err); } - iss.ignore(strlen(scheme_tags[2]) +1, '\n'); - iss.ignore(strlen(scheme_tags[3])); - option->get().from_scheme(iss); - iss.ignore(strlen(scheme_tags[4]) + 2, '\n'); //discard the noise at the end. + if (!(toplevel.m_type == IdentType::FORM && toplevel.m_ids.size() == 2 && + toplevel.m_ids[1].m_type == IdentType::FORM && + toplevel.m_ids[1].m_ids.size() == 2 && + toplevel.m_ids[1].m_ids[0].m_type == IdentType::FORM && + toplevel.m_ids[1].m_ids[0].m_ids.size() == 2 && + toplevel.m_ids[1].m_ids[0].m_ids[1].m_type == IdentType::FORM && + toplevel.m_ids[1].m_ids[0].m_ids[1].m_ids.size() == 2 && + toplevel.m_ids[1].m_ids[0].m_ids[1].m_ids[1].m_type == IdentType::FORM && + toplevel.m_ids[1].m_ids[0].m_ids[1].m_ids[1].m_ids.size() == 2)) + { + std::string err{"Option "}; + err += option_str; + throw std::runtime_error(err + " malformed value lambda form."); + } + auto value_id = toplevel.m_ids[1].m_ids[0].m_ids[1].m_ids[1].m_ids[1]; + std::istringstream value_iss{value_id.m_name}; + option->get().from_scheme(value_iss); return iss; } diff --git a/libgnucash/app-utils/gnc-optiondb.hpp b/libgnucash/app-utils/gnc-optiondb.hpp index 078224f6b0..042e78c46c 100644 --- a/libgnucash/app-utils/gnc-optiondb.hpp +++ b/libgnucash/app-utils/gnc-optiondb.hpp @@ -88,11 +88,18 @@ public: return static_cast(*this).find_option(section, name); } std::optional> find_option(const std::string& section, const std::string& name) const; + std::ostream& save_to_scheme(std::ostream& oss, + const char* options_prolog) const noexcept; + std::istream& load_from_scheme(std::istream& iss) noexcept; + std::ostream& save_to_key_value(std::ostream& oss) const noexcept; + std::istream& load_from_key_value(std::istream& iss) noexcept; + void save_to_kvp() const noexcept; + void load_from_kvp() noexcept; std::ostream& save_option_scheme(std::ostream& oss, const char* option_prolog, const std::string& section, const std::string& name) const noexcept; - std::istream& load_option_scheme(std::istream& iss) noexcept; + std::istream& load_option_scheme(std::istream& iss); std::ostream& save_option_key_value(std::ostream& oss, const char* section, const char* name) const noexcept;