Skip to content

Commit 26bb3f2

Browse files
More fuzzing2 (#1170)
add mechanic for fuzzing subcommands, and several fixes for found issues from longer fuzzer runs This includes some issues with option group positional name ambiguity, issue with join multioption policy and config files, and a few edge cases for configuration of multiline output interpretation. Also added complex variables to the options, no issues found from this addition. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 569ca73 commit 26bb3f2

27 files changed

+946
-494
lines changed

fuzz/fuzzApp.cpp

Lines changed: 291 additions & 14 deletions
Large diffs are not rendered by default.

fuzz/fuzzApp.hpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#endif
1313

1414
#include <atomic>
15+
#include <complex>
1516
#include <iostream>
1617
#include <memory>
1718
#include <optional>
@@ -51,6 +52,18 @@ class stringWrapper {
5152
std::string val{};
5253
};
5354

55+
/// @brief class for extracting data for custom subcommand generation.
56+
class SubcommandData {
57+
public:
58+
std::string name{""};
59+
std::string description{""};
60+
std::string modifiers{""};
61+
std::string data{""};
62+
std::size_t next{std::string::npos};
63+
};
64+
65+
SubcommandData extract_subcomand_info(const std::string &description_string, std::size_t index);
66+
5467
class FuzzApp {
5568
public:
5669
FuzzApp() = default;
@@ -62,7 +75,9 @@ class FuzzApp {
6275
std::size_t add_custom_options(CLI::App *app, const std::string &description_string);
6376
/** modify an option based on string*/
6477
static void modify_option(CLI::Option *opt, const std::string &modifier);
65-
78+
/** modify a subcommand based on characters in a string*/
79+
static void modify_subcommand(CLI::App *app, const std::string &modifiers);
80+
/** return true if the app itself support conversiont to config files*/
6681
CLI11_NODISCARD bool supports_config_file() const { return !non_config_required; }
6782
int32_t val32{0};
6883
int16_t val16{0};
@@ -110,6 +125,9 @@ class FuzzApp {
110125
doubleWrapper dwrap{0.0};
111126
stringWrapper swrap{};
112127
std::string buffer{};
128+
std::complex<double> cv3{0.0, 0.0};
129+
std::complex<float> cv4{0.0, 0.0};
130+
113131
int intbuffer{0};
114132
std::atomic<double> doubleAtomic{0.0};
115133

fuzz/fuzz_dictionary1.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"--tup1"
3232
"--tup2"
3333
"--tup4"
34+
"--cv3"
35+
"--cv4"
3436
"--dwrap"
3537
"--iwrap"
3638
"--vtup"
@@ -118,6 +120,8 @@
118120
"tup1"
119121
"tup2"
120122
"tup4"
123+
"cv3"
124+
"cv4"
121125
"dwrap"
122126
"iwrap"
123127
"swrap"
@@ -181,6 +185,12 @@
181185
"<flag "
182186
"<flag>"
183187
"</flag>"
188+
"<subcommand"
189+
"<subcommand>"
190+
"</subcommand>"
184191
">-"
185192
">--"
186193
"modifiers="
194+
"name="
195+
"description="
196+
"alias="

include/CLI/Option.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ using callback_t = std::function<bool(const results_t &)>;
3434

3535
class Option;
3636
class App;
37+
class ConfigBase;
3738

3839
using Option_p = std::unique_ptr<Option>;
3940
/// Enumeration of the multiOption Policy selection
@@ -51,6 +52,7 @@ enum class MultiOptionPolicy : char {
5152
/// to share parts of the class; an OptionDefaults can copy to an Option.
5253
template <typename CRTP> class OptionBase {
5354
friend App;
55+
friend ConfigBase;
5456

5557
protected:
5658
/// The group membership
@@ -230,6 +232,7 @@ class OptionDefaults : public OptionBase<OptionDefaults> {
230232

231233
class Option : public OptionBase<Option> {
232234
friend App;
235+
friend ConfigBase;
233236

234237
protected:
235238
/// @name Names

include/CLI/StringTools.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,10 @@ CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string);
263263
CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string);
264264

265265
/// process a quoted string, remove the quotes and if appropriate handle escaped characters
266-
CLI11_INLINE bool process_quoted_string(std::string &str, char string_char = '\"', char literal_char = '\'');
266+
CLI11_INLINE bool process_quoted_string(std::string &str,
267+
char string_char = '\"',
268+
char literal_char = '\'',
269+
bool disable_secondary_array_processing = false);
267270

268271
/// This function formats the given text as a paragraph with fixed width and applies correct line wrapping
269272
/// with a custom line prefix. The paragraph will get streamed to the given ostream.

include/CLI/impl/App_inl.hpp

Lines changed: 98 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -165,94 +165,103 @@ CLI11_INLINE Option *App::add_option(std::string option_name,
165165
std::function<std::string()> func) {
166166
Option myopt{option_name, option_description, option_callback, this, allow_non_standard_options_};
167167

168-
if(std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) { return *v == myopt; }) ==
169-
std::end(options_)) {
170-
if(myopt.lnames_.empty() && myopt.snames_.empty()) {
171-
// if the option is positional only there is additional potential for ambiguities in config files and needs
172-
// to be checked
173-
std::string test_name = "--" + myopt.get_single_name();
174-
if(test_name.size() == 3) {
175-
test_name.erase(0, 1);
168+
// do a quick search in current subcommand for options
169+
auto res =
170+
std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) { return *v == myopt; });
171+
if(res != options_.end()) {
172+
const auto &matchname = (*res)->matching_name(myopt);
173+
throw(OptionAlreadyAdded("added option matched existing option name: " + matchname));
174+
}
175+
/** get a top level parent*/
176+
const App *top_level_parent = this;
177+
while(top_level_parent->name_.empty() && top_level_parent->parent_ != nullptr) {
178+
top_level_parent = top_level_parent->parent_;
179+
}
180+
181+
if(myopt.lnames_.empty() && myopt.snames_.empty()) {
182+
// if the option is positional only there is additional potential for ambiguities in config files and needs
183+
// to be checked
184+
std::string test_name = "--" + myopt.get_single_name();
185+
if(test_name.size() == 3) {
186+
test_name.erase(0, 1);
187+
}
188+
// if we are in option group
189+
const auto *op = top_level_parent->get_option_no_throw(test_name);
190+
if(op != nullptr && op->get_configurable()) {
191+
throw(OptionAlreadyAdded("added option positional name matches existing option: " + test_name));
192+
}
193+
// need to check if there is another positional with the same name that also doesn't have any long or
194+
// short names
195+
op = top_level_parent->get_option_no_throw(myopt.get_single_name());
196+
if(op != nullptr && op->lnames_.empty() && op->snames_.empty()) {
197+
throw(OptionAlreadyAdded("unable to disambiguate with existing option: " + test_name));
198+
}
199+
} else if(top_level_parent != this) {
200+
for(auto &ln : myopt.lnames_) {
201+
const auto *op = top_level_parent->get_option_no_throw(ln);
202+
if(op != nullptr && op->get_configurable()) {
203+
throw(OptionAlreadyAdded("added option matches existing positional option: " + ln));
176204
}
177-
178-
auto *op = get_option_no_throw(test_name);
205+
op = top_level_parent->get_option_no_throw("--" + ln);
179206
if(op != nullptr && op->get_configurable()) {
180-
throw(OptionAlreadyAdded("added option positional name matches existing option: " + test_name));
207+
throw(OptionAlreadyAdded("added option matches existing option: --" + ln));
181208
}
182-
// need to check if there is another positional with the same name that also doesn't have any long or short
183-
// names
184-
op = get_option_no_throw(myopt.get_single_name());
185-
if(op != nullptr && op->lnames_.empty() && op->snames_.empty()) {
186-
throw(OptionAlreadyAdded("unable to disambiguate with existing option: " + test_name));
209+
}
210+
for(auto &sn : myopt.snames_) {
211+
const auto *op = top_level_parent->get_option_no_throw(sn);
212+
if(op != nullptr && op->get_configurable()) {
213+
throw(OptionAlreadyAdded("added option matches existing positional option: " + sn));
187214
}
188-
} else if(parent_ != nullptr) {
189-
for(auto &ln : myopt.lnames_) {
190-
auto *op = parent_->get_option_no_throw(ln);
191-
if(op != nullptr && op->get_configurable()) {
192-
throw(OptionAlreadyAdded("added option matches existing positional option: " + ln));
193-
}
215+
op = top_level_parent->get_option_no_throw("-" + sn);
216+
if(op != nullptr && op->get_configurable()) {
217+
throw(OptionAlreadyAdded("added option matches existing option: -" + sn));
194218
}
195-
for(auto &sn : myopt.snames_) {
196-
auto *op = parent_->get_option_no_throw(sn);
197-
if(op != nullptr && op->get_configurable()) {
198-
throw(OptionAlreadyAdded("added option matches existing positional option: " + sn));
219+
}
220+
}
221+
if(allow_non_standard_options_ && !myopt.snames_.empty()) {
222+
223+
for(auto &sname : myopt.snames_) {
224+
if(sname.length() > 1) {
225+
std::string test_name;
226+
test_name.push_back('-');
227+
test_name.push_back(sname.front());
228+
const auto *op = top_level_parent->get_option_no_throw(test_name);
229+
if(op != nullptr) {
230+
throw(OptionAlreadyAdded("added option interferes with existing short option: " + sname));
199231
}
200232
}
201233
}
202-
if(allow_non_standard_options_ && !myopt.snames_.empty()) {
203-
for(auto &sname : myopt.snames_) {
204-
if(sname.length() > 1) {
234+
for(auto &opt : top_level_parent->get_options()) {
235+
for(const auto &osn : opt->snames_) {
236+
if(osn.size() > 1) {
205237
std::string test_name;
206-
test_name.push_back('-');
207-
test_name.push_back(sname.front());
208-
auto *op = get_option_no_throw(test_name);
209-
if(op != nullptr) {
210-
throw(OptionAlreadyAdded("added option interferes with existing short option: " + sname));
211-
}
212-
}
213-
}
214-
for(auto &opt : options_) {
215-
for(const auto &osn : opt->snames_) {
216-
if(osn.size() > 1) {
217-
std::string test_name;
218-
test_name.push_back(osn.front());
219-
if(myopt.check_sname(test_name)) {
220-
throw(OptionAlreadyAdded("added option interferes with existing non standard option: " +
221-
osn));
222-
}
238+
test_name.push_back(osn.front());
239+
if(myopt.check_sname(test_name)) {
240+
throw(OptionAlreadyAdded("added option interferes with existing non standard option: " + osn));
223241
}
224242
}
225243
}
226244
}
227-
options_.emplace_back();
228-
Option_p &option = options_.back();
229-
option.reset(new Option(option_name, option_description, option_callback, this, allow_non_standard_options_));
245+
}
246+
options_.emplace_back();
247+
Option_p &option = options_.back();
248+
option.reset(new Option(option_name, option_description, option_callback, this, allow_non_standard_options_));
230249

231-
// Set the default string capture function
232-
option->default_function(func);
250+
// Set the default string capture function
251+
option->default_function(func);
233252

234-
// For compatibility with CLI11 1.7 and before, capture the default string here
235-
if(defaulted)
236-
option->capture_default_str();
253+
// For compatibility with CLI11 1.7 and before, capture the default string here
254+
if(defaulted)
255+
option->capture_default_str();
237256

238-
// Transfer defaults to the new option
239-
option_defaults_.copy_to(option.get());
257+
// Transfer defaults to the new option
258+
option_defaults_.copy_to(option.get());
240259

241-
// Don't bother to capture if we already did
242-
if(!defaulted && option->get_always_capture_default())
243-
option->capture_default_str();
260+
// Don't bother to capture if we already did
261+
if(!defaulted && option->get_always_capture_default())
262+
option->capture_default_str();
244263

245-
return option.get();
246-
}
247-
// we know something matches now find what it is so we can produce more error information
248-
for(auto &opt : options_) {
249-
const auto &matchname = opt->matching_name(myopt);
250-
if(!matchname.empty()) {
251-
throw(OptionAlreadyAdded("added option matched existing option name: " + matchname));
252-
}
253-
}
254-
// this line should not be reached the above loop should trigger the throw
255-
throw(OptionAlreadyAdded("added option matched existing option name")); // LCOV_EXCL_LINE
264+
return option.get();
256265
}
257266

258267
CLI11_INLINE Option *App::set_help_flag(std::string flag_name, const std::string &help_description) {
@@ -841,8 +850,8 @@ CLI11_INLINE std::vector<Option *> App::get_options(const std::function<bool(Opt
841850
std::end(options));
842851
}
843852
for(auto &subc : subcommands_) {
844-
// also check down into nameless subcommands
845-
if(subc->get_name().empty() && !subc->get_group().empty() && subc->get_group().front() == '+') {
853+
// also check down into nameless subcommands and specific groups
854+
if(subc->get_name().empty() || (!subc->get_group().empty() && subc->get_group().front() == '+')) {
846855
auto subcopts = subc->get_options(filter);
847856
options.insert(options.end(), subcopts.begin(), subcopts.end());
848857
}
@@ -1539,7 +1548,8 @@ App::_add_flag_like_result(Option *op, const ConfigItem &item, const std::vector
15391548
return true;
15401549
}
15411550
if(static_cast<int>(inputs.size()) > op->get_items_expected_max() &&
1542-
op->get_multi_option_policy() != MultiOptionPolicy::TakeAll) {
1551+
op->get_multi_option_policy() != MultiOptionPolicy::TakeAll &&
1552+
op->get_multi_option_policy() != MultiOptionPolicy::Join) {
15431553
if(op->get_items_expected_max() > 1) {
15441554
throw ArgumentMismatch::AtMost(item.fullname(), op->get_items_expected_max(), inputs.size());
15451555
}
@@ -1608,11 +1618,6 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t
16081618
}
16091619
if(op == nullptr) {
16101620
op = get_option_no_throw(item.name);
1611-
} else if(!op->get_configurable()) {
1612-
auto *testop = get_option_no_throw(item.name);
1613-
if(testop != nullptr && testop->get_configurable()) {
1614-
op = testop;
1615-
}
16161621
}
16171622
} else if(!op->get_configurable()) {
16181623
if(item.name.size() == 1) {
@@ -1621,14 +1626,17 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t
16211626
op = testop;
16221627
}
16231628
}
1624-
if(!op->get_configurable()) {
1625-
auto *testop = get_option_no_throw(item.name);
1626-
if(testop != nullptr && testop->get_configurable()) {
1627-
op = testop;
1628-
}
1629+
}
1630+
if(op == nullptr || !op->get_configurable()) {
1631+
std::string iname = item.name;
1632+
auto options = get_options([iname](const CLI::Option *opt) {
1633+
return (opt->get_configurable() &&
1634+
(opt->check_name(iname) || opt->check_lname(iname) || opt->check_sname(iname)));
1635+
});
1636+
if(!options.empty()) {
1637+
op = options[0];
16291638
}
16301639
}
1631-
16321640
if(op == nullptr) {
16331641
// If the option was not present
16341642
if(get_allow_config_extras() == config_extras_mode::capture) {
@@ -1785,7 +1793,9 @@ CLI11_INLINE bool App::_parse_positional(std::vector<std::string> &args, bool ha
17851793
posOpt->add_result(std::string{});
17861794
}
17871795
}
1796+
results_t prev;
17881797
if(posOpt->get_trigger_on_parse() && posOpt->current_option_state_ == Option::option_state::callback_run) {
1798+
prev = posOpt->results();
17891799
posOpt->clear();
17901800
}
17911801
if(posOpt->get_expected_min() == 0) {
@@ -1801,6 +1811,10 @@ CLI11_INLINE bool App::_parse_positional(std::vector<std::string> &args, bool ha
18011811
if(posOpt->get_trigger_on_parse()) {
18021812
if(!posOpt->empty()) {
18031813
posOpt->run_callback();
1814+
} else {
1815+
if(!prev.empty()) {
1816+
posOpt->add_result(prev);
1817+
}
18041818
}
18051819
}
18061820

0 commit comments

Comments
 (0)