From 91a6af2bc7c073d016c64f55f9c8e3a6004f39e2 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 30 Dec 2023 10:56:35 -0800 Subject: [PATCH 1/6] add support for quotes in the config naming to match TOML standard --- include/CLI/Config.hpp | 2 + include/CLI/impl/Config_inl.hpp | 73 ++++++---- tests/ConfigFileTest.cpp | 245 ++++++++++++++++++++++++++++++++ 3 files changed, 291 insertions(+), 29 deletions(-) diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index c9809801b..ba794f107 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -37,6 +37,8 @@ std::string ini_join(const std::vector &args, char stringQuote = '"', char literalQuote = '\''); +void clean_name_string(std::string& name,const std::string &keyChars); + std::vector generate_parents(const std::string §ion, std::string &name, char parentSeparator); /// assuming non default segments do a check on the close and open of the segments in a configItem structure diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 4723c5015..35d4a7da2 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -122,23 +122,20 @@ generate_parents(const std::string §ion, std::string &name, char parentSepar std::vector parents; if(detail::to_lower(section) != "default") { if(section.find(parentSeparator) != std::string::npos) { - parents = detail::split(section, parentSeparator); + parents = detail::split_up(section, parentSeparator); } else { parents = {section}; } } if(name.find(parentSeparator) != std::string::npos) { - std::vector plist = detail::split(name, parentSeparator); + std::vector plist = detail::split_up(name, parentSeparator); name = plist.back(); - detail::remove_quotes(name); + detail::process_quoted_string(name); plist.pop_back(); parents.insert(parents.end(), plist.begin(), plist.end()); } - // clean up quotes on the parents - for(auto &parent : parents) { - detail::remove_quotes(parent); - } + detail::remove_quotes(parents); return parents; } @@ -418,6 +415,25 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons return output; } +CLI11_INLINE std::string& clean_name_string(std::string& name,const std::string &keyChars) +{ + if(name.find_first_of(keyChars) != std::string::npos || + (name.front() == '[' && name.back() == ']') || + (name.find_first_of("'`\"") != std::string::npos)) { + if (name.find_first_of('\'') == std::string::npos) { + name.insert(0, 1, '\''); + name.push_back('\''); + } else { + if(detail::has_escapable_character(name)) { + name = detail::add_escaped_characters(name); + } + name.insert(0, 1, '\"'); + name.push_back('\"'); + } + } + return name; +} + CLI11_INLINE std::string ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const { std::stringstream out; @@ -429,6 +445,14 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, commentTest.push_back(commentChar); commentTest.push_back(parentSeparatorChar); + std::string keyChars=commentTest; + keyChars.push_back(literalQuote); + keyChars.push_back(stringQuote); + keyChars.push_back(arrayStart); + keyChars.push_back(arrayEnd); + keyChars.push_back(valueDelimiter); + keyChars.push_back(arraySeparator); + std::vector groups = app->get_groups(); bool defaultUsed = false; groups.insert(groups.begin(), std::string("Options")); @@ -498,24 +522,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, out << '\n'; out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n'; } - if(single_name.find_first_of(commentTest) != std::string::npos || - single_name.compare(0, 3, multiline_string_quote) == 0 || - single_name.compare(0, 3, multiline_literal_quote) == 0 || - (single_name.front() == '[' && single_name.back() == ']') || - (single_name.find_first_of(stringQuote) != std::string::npos) || - (single_name.find_first_of(literalQuote) != std::string::npos) || - (single_name.find_first_of('`') != std::string::npos)) { - if(single_name.find_first_of(literalQuote) == std::string::npos) { - single_name.insert(0, 1, literalQuote); - single_name.push_back(literalQuote); - } else { - if(detail::has_escapable_character(single_name)) { - single_name = detail::add_escaped_characters(single_name); - } - single_name.insert(0, 1, stringQuote); - single_name.push_back(stringQuote); - } - } + clean_name_string(single_name,keyChars); std::string name = prefix + single_name; @@ -554,14 +561,22 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, if(!default_also && (subcom->count_all() == 0)) { continue; } + std::string subname=subcom->get_name(); + clean_name_string(subname,keyChars); + if(subcom->get_configurable() && app->got_subcommand(subcom)) { if(!prefix.empty() || app->get_parent() == nullptr) { - out << '[' << prefix << subcom->get_name() << "]\n"; + + out << '[' << prefix << subname << "]\n"; } else { - std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name(); + std::string appname=app->get_name(); + clean_name_string(appname,keyChars); + subname = appname + parentSeparatorChar + subname; const auto *p = app->get_parent(); while(p->get_parent() != nullptr) { - subname = p->get_name() + parentSeparatorChar + subname; + std::string pname=p->get_name(); + clean_name_string(pname,keyChars); + subname = pname + parentSeparatorChar + subname; p = p->get_parent(); } out << '[' << subname << "]\n"; @@ -569,7 +584,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, out << to_config(subcom, default_also, write_description, ""); } else { out << to_config( - subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar); + subcom, default_also, write_description, prefix + subname + parentSeparatorChar); } } } diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 55ceac2cd..27e875227 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -501,6 +501,38 @@ TEST_CASE("StringBased: Layers2LevelChange", "[config]") { CHECK(checkSections(output)); } +TEST_CASE("StringBased: Layers2LevelChangeInQuotes", "[config]") { + std::stringstream ofile; + + ofile << "simple = true\n\n"; + ofile << "[\"other\".\"sub2\".cmd]\n"; + ofile << "[other.\"sub3\".\"cmd\"]\n"; + ofile << "absolute_newest = true\n"; + ofile.seekg(0, std::ios::beg); + + std::vector output = CLI::ConfigINI().from_config(ofile); + + // 2 flags and 5 openings and 5 closings + CHECK(output.size() == 12u); + CHECK(checkSections(output)); +} + +TEST_CASE("StringBased: Layers2LevelChangeInQuotesWithDot", "[config]") { + std::stringstream ofile; + + ofile << "simple = true\n\n"; + ofile << "[\"other\".\"sub2.cmd\"]\n"; + ofile << "[other.\"sub3.cmd\"]\n"; + ofile << "absolute_newest = true\n"; + ofile.seekg(0, std::ios::beg); + + std::vector output = CLI::ConfigINI().from_config(ofile); + + // 2 flags and 3 openings and 3 closings + CHECK(output.size() == 8u); + CHECK(checkSections(output)); +} + TEST_CASE("StringBased: Layers3LevelChange", "[config]") { std::stringstream ofile; @@ -1583,6 +1615,45 @@ TEST_CASE_METHOD(TApp, "IniLayeredDotSection", "[config]") { CHECK(three == 0); } +TEST_CASE_METHOD(TApp, "IniLayeredDotSectionInQuotes", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "['subcom']" << std::endl; + out << "val=2" << std::endl; + out << "['subcom'.\"subsubcom\"]" << std::endl; + out << "val=3" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto *subcom = app.add_subcommand("subcom"); + subcom->add_option("--val", two); + auto *subsubcom = subcom->add_subcommand("subsubcom"); + subsubcom->add_option("--val", three); + + run(); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); + + CHECK(0U == subcom->count()); + CHECK(!*subcom); + + three = 0; + // check maxlayers + app.get_config_formatter_base()->maxLayers(1); + run(); + CHECK(three == 0); +} + TEST_CASE_METHOD(TApp, "IniLayeredCustomSectionSeparator", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -1674,6 +1745,74 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurable", "[config]") { CHECK(app.got_subcommand(subcom)); } +TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableInQuotes", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[subcom]" << std::endl; + out << "val=2" << std::endl; + out << "\"subsubcom\".'val'=3" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto *subcom = app.add_subcommand("subcom"); + subcom->configurable(); + subcom->add_option("--val", two); + auto *subsubcom = subcom->add_subcommand("subsubcom"); + subsubcom->add_option("--val", three); + + run(); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); + + CHECK(1U == subcom->count()); + CHECK(*subcom); + CHECK(app.got_subcommand(subcom)); +} + +TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableInQuotesAlias", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[subcom]" << std::endl; + out << "val=2" << std::endl; + out << "\"sub\\tsub\\t.com\".'val'=3" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto *subcom = app.add_subcommand("subcom"); + subcom->configurable(); + subcom->add_option("--val", two); + auto *subsubcom = subcom->add_subcommand("subsubcom")->alias("sub\tsub\t.com"); + subsubcom->add_option("--val", three); + + run(); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); + + CHECK(1U == subcom->count()); + CHECK(*subcom); + CHECK(app.got_subcommand(subcom)); +} + TEST_CASE_METHOD(TApp, "IniSubcommandConfigurablePreParse", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -1715,6 +1854,8 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurablePreParse", "[config]") { CHECK(0U == subcom2->count()); } + + TEST_CASE_METHOD(TApp, "IniSection", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -2181,6 +2322,57 @@ TEST_CASE_METHOD(TApp, "IniShort", "[config]") { CHECK(3 == key); } +TEST_CASE_METHOD(TApp, "IniShortQuote1", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + int key{0}; + app.add_option("--flag,-f", key); + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "\"f\"=3" << std::endl; + } + + REQUIRE_NOTHROW(run()); + CHECK(3 == key); +} + +TEST_CASE_METHOD(TApp, "IniShortQuote2", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + int key{0}; + app.add_option("--flag,-f", key); + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "'f'=3" << std::endl; + } + + REQUIRE_NOTHROW(run()); + CHECK(3 == key); +} + +TEST_CASE_METHOD(TApp, "IniShortQuote3", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + int key{0}; + app.add_option("--flag,-f", key); + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "`f`=3" << std::endl; + } + + REQUIRE_NOTHROW(run()); + CHECK(3 == key); +} + TEST_CASE_METHOD(TApp, "IniDefaultPath", "[config]") { TempFile tmpini{"../TestIniTmp.ini"}; @@ -3388,6 +3580,23 @@ TEST_CASE_METHOD(TApp, "IniOutputSubsubcom", "[config]") { CHECK_THAT(str, Contains("other.sub2.newest=true")); } +TEST_CASE_METHOD(TApp, "IniOutputSubsubcomWithDot", "[config]") { + + app.add_flag("--simple"); + auto *subcom = app.add_subcommand("other"); + subcom->add_flag("--newer"); + auto *subsubcom = subcom->add_subcommand("sub2.bb"); + subsubcom->add_flag("--newest"); + app.config_formatter(std::make_shared()); + args = {"--simple", "other", "--newer", "sub2.bb", "--newest"}; + run(); + + std::string str = app.config_to_str(); + CHECK_THAT(str, Contains("simple=true")); + CHECK_THAT(str, Contains("other.newer=true")); + CHECK_THAT(str, Contains("other.'sub2.bb'.newest=true")); +} + TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSep", "[config]") { app.add_flag("--simple"); @@ -3406,6 +3615,42 @@ TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSep", "[config]") { CHECK_THAT(str, Contains("other|sub2|newest=true")); } +TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSepWithInternalSep", "[config]") { + + app.add_flag("--simple"); + auto *subcom = app.add_subcommand("other"); + subcom->add_flag("--newer"); + auto *subsubcom = subcom->add_subcommand("sub2|BB"); + subsubcom->add_flag("--newest"); + app.config_formatter(std::make_shared()); + app.get_config_formatter_base()->parentSeparator('|'); + args = {"--simple", "other", "--newer", "sub2|BB", "--newest"}; + run(); + + std::string str = app.config_to_str(); + CHECK_THAT(str, Contains("simple=true")); + CHECK_THAT(str, Contains("other|newer=true")); + CHECK_THAT(str, Contains("other|'sub2|BB'|newest=true")); +} + +TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSepWithInternalQuote", "[config]") { + + app.add_flag("--simple"); + auto *subcom = app.add_subcommand("other"); + subcom->add_flag("--newer"); + auto* subsubcom = subcom->add_subcommand("sub2'BB"); + subsubcom->add_flag("--newest"); + app.config_formatter(std::make_shared()); + app.get_config_formatter_base()->parentSeparator('|'); + args = {"--simple", "other", "--newer", "sub2'BB", "--newest"}; + run(); + + std::string str = app.config_to_str(); + CHECK_THAT(str, Contains("simple=true")); + CHECK_THAT(str, Contains("other|newer=true")); + CHECK_THAT(str, Contains("other|\"sub2'BB\"|newest=true")); +} + TEST_CASE_METHOD(TApp, "IniOutputSubsubcomConfigurable", "[config]") { app.add_flag("--simple"); From f925ea7f578ecb9b97fc4e8c29de44b282097588 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 30 Dec 2023 19:00:02 +0000 Subject: [PATCH 2/6] style: pre-commit.ci fixes --- include/CLI/Config.hpp | 2 +- include/CLI/impl/Config_inl.hpp | 31 ++++++++++++++----------------- tests/ConfigFileTest.cpp | 4 +--- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index ba794f107..2169e5e6f 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -37,7 +37,7 @@ std::string ini_join(const std::vector &args, char stringQuote = '"', char literalQuote = '\''); -void clean_name_string(std::string& name,const std::string &keyChars); +void clean_name_string(std::string &name, const std::string &keyChars); std::vector generate_parents(const std::string §ion, std::string &name, char parentSeparator); diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 35d4a7da2..2625a56e5 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -415,12 +415,10 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons return output; } -CLI11_INLINE std::string& clean_name_string(std::string& name,const std::string &keyChars) -{ - if(name.find_first_of(keyChars) != std::string::npos || - (name.front() == '[' && name.back() == ']') || - (name.find_first_of("'`\"") != std::string::npos)) { - if (name.find_first_of('\'') == std::string::npos) { +CLI11_INLINE std::string &clean_name_string(std::string &name, const std::string &keyChars) { + if(name.find_first_of(keyChars) != std::string::npos || (name.front() == '[' && name.back() == ']') || + (name.find_first_of("'`\"") != std::string::npos)) { + if(name.find_first_of('\'') == std::string::npos) { name.insert(0, 1, '\''); name.push_back('\''); } else { @@ -445,7 +443,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, commentTest.push_back(commentChar); commentTest.push_back(parentSeparatorChar); - std::string keyChars=commentTest; + std::string keyChars = commentTest; keyChars.push_back(literalQuote); keyChars.push_back(stringQuote); keyChars.push_back(arrayStart); @@ -522,7 +520,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, out << '\n'; out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n'; } - clean_name_string(single_name,keyChars); + clean_name_string(single_name, keyChars); std::string name = prefix + single_name; @@ -561,21 +559,21 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, if(!default_also && (subcom->count_all() == 0)) { continue; } - std::string subname=subcom->get_name(); - clean_name_string(subname,keyChars); + std::string subname = subcom->get_name(); + clean_name_string(subname, keyChars); if(subcom->get_configurable() && app->got_subcommand(subcom)) { if(!prefix.empty() || app->get_parent() == nullptr) { - + out << '[' << prefix << subname << "]\n"; } else { - std::string appname=app->get_name(); - clean_name_string(appname,keyChars); + std::string appname = app->get_name(); + clean_name_string(appname, keyChars); subname = appname + parentSeparatorChar + subname; const auto *p = app->get_parent(); while(p->get_parent() != nullptr) { - std::string pname=p->get_name(); - clean_name_string(pname,keyChars); + std::string pname = p->get_name(); + clean_name_string(pname, keyChars); subname = pname + parentSeparatorChar + subname; p = p->get_parent(); } @@ -583,8 +581,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, } out << to_config(subcom, default_also, write_description, ""); } else { - out << to_config( - subcom, default_also, write_description, prefix + subname + parentSeparatorChar); + out << to_config(subcom, default_also, write_description, prefix + subname + parentSeparatorChar); } } } diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 27e875227..ed3bd66f9 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1854,8 +1854,6 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurablePreParse", "[config]") { CHECK(0U == subcom2->count()); } - - TEST_CASE_METHOD(TApp, "IniSection", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -3638,7 +3636,7 @@ TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSepWithInternalQuote", "[config] app.add_flag("--simple"); auto *subcom = app.add_subcommand("other"); subcom->add_flag("--newer"); - auto* subsubcom = subcom->add_subcommand("sub2'BB"); + auto *subsubcom = subcom->add_subcommand("sub2'BB"); subsubcom->add_flag("--newest"); app.config_formatter(std::make_shared()); app.get_config_formatter_base()->parentSeparator('|'); From c887ec2c3af2a9723bc8e30f1f527d653f344171 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 30 Dec 2023 13:46:56 -0800 Subject: [PATCH 3/6] handle comment and value Delimiters in the subcommand names in the config file --- include/CLI/impl/Config_inl.hpp | 49 ++++++++++++++++-------- tests/ConfigFileTest.cpp | 66 ++++++++++++++++++++++++++++++++- tests/FuzzFailTest.cpp | 2 +- tests/fuzzFail/fuzz_file_fail5 | 1 + 4 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 tests/fuzzFail/fuzz_file_fail5 diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 2625a56e5..1014df262 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -130,7 +130,6 @@ generate_parents(const std::string §ion, std::string &name, char parentSepar if(name.find(parentSeparator) != std::string::npos) { std::vector plist = detail::split_up(name, parentSeparator); name = plist.back(); - detail::process_quoted_string(name); plist.pop_back(); parents.insert(parents.end(), plist.begin(), plist.end()); } @@ -215,10 +214,10 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator; int currentSectionIndex{0}; + std::string line_sep_chars{parentSeparatorChar,commentChar,valueDelimiter}; while(getline(input, buffer)) { std::vector items_buffer; std::string name; - bool literalName{false}; line = detail::trim_copy(buffer); std::size_t len = line.length(); // lines have to be at least 3 characters to have any meaning to CLI just skip the rest @@ -272,8 +271,33 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons continue; } std::size_t search_start = 0; - if(line.front() == stringQuote || line.front() == literalQuote || line.front() == '`') { - search_start = detail::close_sequence(line, 0, line.front()); + if (line.find_first_of("\"'`") != std::string::npos) + { + bool name_ended{false}; + while (!name_ended && search_start ConfigBase::from_config(std::istream &input) cons std::string item = detail::trim_copy(line.substr(delimiter_pos + 1, std::string::npos)); bool mlquote = (item.compare(0, 3, multiline_literal_quote) == 0 || item.compare(0, 3, multiline_string_quote) == 0); - if(!mlquote && comment_pos != std::string::npos && !literalName) { + if(!mlquote && comment_pos != std::string::npos ) { auto citems = detail::split_up(item, commentChar); item = detail::trim_copy(citems.front()); } @@ -362,9 +386,10 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons name = detail::trim_copy(line.substr(0, comment_pos)); items_buffer = {"true"}; } + std::vector parents; try { - literalName = detail::process_quoted_string(name, stringQuote, literalQuote); - + parents= detail::generate_parents(currentSection, name, parentSeparatorChar); + detail::process_quoted_string(name); // clean up quotes on the items and check for escaped strings for(auto &it : items_buffer) { detail::process_quoted_string(it, stringQuote, literalQuote); @@ -372,13 +397,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons } catch(const std::invalid_argument &ia) { throw CLI::ParseError(ia.what(), CLI::ExitCodes::InvalidError); } - std::vector parents; - if(literalName) { - std::string noname{}; - parents = detail::generate_parents(currentSection, noname, parentSeparatorChar); - } else { - parents = detail::generate_parents(currentSection, name, parentSeparatorChar); - } + if(parents.size() > maximumLayers) { continue; } @@ -417,7 +436,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons CLI11_INLINE std::string &clean_name_string(std::string &name, const std::string &keyChars) { if(name.find_first_of(keyChars) != std::string::npos || (name.front() == '[' && name.back() == ']') || - (name.find_first_of("'`\"") != std::string::npos)) { + (name.find_first_of("'`\"\\") != std::string::npos)) { if(name.find_first_of('\'') == std::string::npos) { name.insert(0, 1, '\''); name.push_back('\''); diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index ed3bd66f9..9b058ded8 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1791,7 +1791,7 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableInQuotesAlias", "[config]") { out << "val=1" << std::endl; out << "[subcom]" << std::endl; out << "val=2" << std::endl; - out << "\"sub\\tsub\\t.com\".'val'=3" << std::endl; + out << R"("sub\tsub\t.com".'val'=3)" << std::endl; } int one{0}, two{0}, three{0}; @@ -1813,6 +1813,70 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableInQuotesAlias", "[config]") { CHECK(app.got_subcommand(subcom)); } +TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableInQuotesAliasWithEquals", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[subcom]" << std::endl; + out << "val=2" << std::endl; + out << R"("sub=sub=.com".'val'=3)" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto *subcom = app.add_subcommand("subcom"); + subcom->configurable(); + subcom->add_option("--val", two); + auto *subsubcom = subcom->add_subcommand("subsubcom")->alias("sub=sub=.com"); + subsubcom->add_option("--val", three); + + run(); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); + + CHECK(1U == subcom->count()); + CHECK(*subcom); + CHECK(app.got_subcommand(subcom)); +} + +TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableInQuotesAliasWithComment", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[subcom]" << std::endl; + out << "val=2" << std::endl; + out << R"("sub#sub;.com".'val'=3)" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto *subcom = app.add_subcommand("subcom"); + subcom->configurable(); + subcom->add_option("--val", two); + auto *subsubcom = subcom->add_subcommand("subsubcom")->alias("sub#sub;.com"); + subsubcom->add_option("--val", three); + + run(); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); +} + TEST_CASE_METHOD(TApp, "IniSubcommandConfigurablePreParse", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; diff --git a/tests/FuzzFailTest.cpp b/tests/FuzzFailTest.cpp index 39e37fc5f..584dacfeb 100644 --- a/tests/FuzzFailTest.cpp +++ b/tests/FuzzFailTest.cpp @@ -50,7 +50,7 @@ TEST_CASE("file_fail") { CLI::FuzzApp fuzzdata; auto app = fuzzdata.generateApp(); - int index = GENERATE(range(1, 5)); + int index = GENERATE(range(1, 6)); auto parseData = loadFailureFile("fuzz_file_fail", index); std::stringstream out(parseData); try { diff --git a/tests/fuzzFail/fuzz_file_fail5 b/tests/fuzzFail/fuzz_file_fail5 new file mode 100644 index 000000000..95473ec87 --- /dev/null +++ b/tests/fuzzFail/fuzz_file_fail5 @@ -0,0 +1 @@ +"\uasdwrap¦¦¦-"¦¦-"--confiلللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللل.للللللللللللللللللللللللللللللللللللللg \ No newline at end of file From 203e481367f88b31803b209d828c80d79c63b7cb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 30 Dec 2023 21:47:17 +0000 Subject: [PATCH 4/6] style: pre-commit.ci fixes --- include/CLI/impl/Config_inl.hpp | 36 ++++++++++++--------------------- tests/fuzzFail/fuzz_file_fail5 | 2 +- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 1014df262..8b2c1fd7a 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -214,7 +214,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator; int currentSectionIndex{0}; - std::string line_sep_chars{parentSeparatorChar,commentChar,valueDelimiter}; + std::string line_sep_chars{parentSeparatorChar, commentChar, valueDelimiter}; while(getline(input, buffer)) { std::vector items_buffer; std::string name; @@ -271,32 +271,22 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons continue; } std::size_t search_start = 0; - if (line.find_first_of("\"'`") != std::string::npos) - { + if(line.find_first_of("\"'`") != std::string::npos) { bool name_ended{false}; - while (!name_ended && search_start ConfigBase::from_config(std::istream &input) cons std::string item = detail::trim_copy(line.substr(delimiter_pos + 1, std::string::npos)); bool mlquote = (item.compare(0, 3, multiline_literal_quote) == 0 || item.compare(0, 3, multiline_string_quote) == 0); - if(!mlquote && comment_pos != std::string::npos ) { + if(!mlquote && comment_pos != std::string::npos) { auto citems = detail::split_up(item, commentChar); item = detail::trim_copy(citems.front()); } @@ -388,7 +378,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons } std::vector parents; try { - parents= detail::generate_parents(currentSection, name, parentSeparatorChar); + parents = detail::generate_parents(currentSection, name, parentSeparatorChar); detail::process_quoted_string(name); // clean up quotes on the items and check for escaped strings for(auto &it : items_buffer) { @@ -397,7 +387,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons } catch(const std::invalid_argument &ia) { throw CLI::ParseError(ia.what(), CLI::ExitCodes::InvalidError); } - + if(parents.size() > maximumLayers) { continue; } diff --git a/tests/fuzzFail/fuzz_file_fail5 b/tests/fuzzFail/fuzz_file_fail5 index 95473ec87..2acfd3cba 100644 --- a/tests/fuzzFail/fuzz_file_fail5 +++ b/tests/fuzzFail/fuzz_file_fail5 @@ -1 +1 @@ -"\uasdwrap¦¦¦-"¦¦-"--confiلللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللل.للللللللللللللللللللللللللللللللللللللg \ No newline at end of file +"\uasdwrap¦¦¦-"¦¦-"--confiلللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللللل.للللللللللللللللللللللللللللللللللللللg From 1e4f880439c385b295664e96dd3d304e509a1879 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 30 Dec 2023 14:14:13 -0800 Subject: [PATCH 5/6] remove unneeded check From 0150846fe72d597a1f79f310ffddfb684ff73b6d Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 30 Dec 2023 14:17:37 -0800 Subject: [PATCH 6/6] remove unneeded check --- include/CLI/impl/Config_inl.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 8b2c1fd7a..e61cb0643 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -272,14 +272,12 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons } std::size_t search_start = 0; if(line.find_first_of("\"'`") != std::string::npos) { - bool name_ended{false}; - while(!name_ended && search_start < line.size()) { + while(search_start < line.size()) { auto test_char = line[search_start]; if(test_char == '\"' || test_char == '\'' || test_char == '`') { search_start = detail::close_sequence(line, search_start, line[search_start]); ++search_start; } else if(test_char == valueDelimiter || test_char == commentChar) { - name_ended = true; --search_start; break; } else if(test_char == ' ' || test_char == '\t' || test_char == parentSeparatorChar) {