mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-28 13:53:10 +00:00
Introduce expansion limits
This adds the ability to limit how many expansions are produced. For example if $big contains 10 items, and is Cartesian-expanded as $big$big$big$big... 10 times, we would naviely get 10^10 = 10 billion results, which fish can't actually handle. Implement this in completion_receiver_t, which now can return false to indicate an overflow. The initial expansion limit 'k_default_expansion_limit' is set as 512k items. There's no way for users to change this at present.
This commit is contained in:
parent
48567c37de
commit
f11a60473a
7 changed files with 211 additions and 78 deletions
|
@ -236,24 +236,36 @@ void completion_t::prepend_token_prefix(const wcstring &prefix) {
|
|||
}
|
||||
}
|
||||
|
||||
void completion_receiver_t::add(completion_t &&comp) {
|
||||
bool completion_receiver_t::add(completion_t &&comp) {
|
||||
if (this->completions_.size() >= limit_) {
|
||||
return false;
|
||||
}
|
||||
this->completions_.push_back(std::move(comp));
|
||||
return true;
|
||||
}
|
||||
|
||||
void completion_receiver_t::add(wcstring &&comp) { this->add(std::move(comp), wcstring{}); }
|
||||
bool completion_receiver_t::add(wcstring &&comp) {
|
||||
return this->add(std::move(comp), wcstring{});
|
||||
}
|
||||
|
||||
void completion_receiver_t::add(wcstring &&comp, wcstring &&desc, complete_flags_t flags,
|
||||
bool completion_receiver_t::add(wcstring &&comp, wcstring &&desc, complete_flags_t flags,
|
||||
string_fuzzy_match_t match) {
|
||||
this->completions_.emplace_back(std::move(comp), std::move(desc), match, flags);
|
||||
return this->add(completion_t(std::move(comp), std::move(desc), match, flags));
|
||||
}
|
||||
|
||||
void completion_receiver_t::add_list(completion_list_t &&lst) {
|
||||
bool completion_receiver_t::add_list(completion_list_t &&lst) {
|
||||
size_t total_size = lst.size() + this->size();
|
||||
if (total_size < this->size() || total_size > limit_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (completions_.empty()) {
|
||||
completions_ = std::move(lst);
|
||||
} else {
|
||||
completions_.reserve(completions_.size() + lst.size());
|
||||
std::move(lst.begin(), lst.end(), std::back_inserter(completions_));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
completion_list_t completion_receiver_t::take() {
|
||||
|
@ -262,6 +274,11 @@ completion_list_t completion_receiver_t::take() {
|
|||
return res;
|
||||
}
|
||||
|
||||
completion_receiver_t completion_receiver_t::subreceiver() const {
|
||||
size_t remaining_capacity = limit_ < size() ? 0 : limit_ - size();
|
||||
return completion_receiver_t(remaining_capacity);
|
||||
}
|
||||
|
||||
// If these functions aren't force inlined, it is actually faster to call
|
||||
// stable_sort twice rather than to iterate once performing all comparisons in one go!
|
||||
__attribute__((always_inline)) static inline bool compare_completions_by_duplicate_arguments(
|
||||
|
@ -358,8 +375,8 @@ class completer_t {
|
|||
bool try_complete_variable(const wcstring &str);
|
||||
bool try_complete_user(const wcstring &str);
|
||||
|
||||
bool complete_param(const wcstring &cmd_orig, const wcstring &popt, const wcstring &str,
|
||||
bool use_switches);
|
||||
bool complete_param_for_command(const wcstring &cmd_orig, const wcstring &popt,
|
||||
const wcstring &str, bool use_switches, bool *out_do_file);
|
||||
|
||||
void complete_param_expand(const wcstring &str, bool do_file,
|
||||
bool handle_as_special_cd = false);
|
||||
|
@ -910,16 +927,18 @@ static void complete_load(const wcstring &name) {
|
|||
}
|
||||
|
||||
/// complete_param: Given a command, find completions for the argument str of command cmd_orig with
|
||||
/// previous option popt.
|
||||
/// previous option popt. If file completions should be disabled, then mark *out_do_file as false.
|
||||
///
|
||||
/// \return true if successful, false if there's an error.
|
||||
///
|
||||
/// Examples in format (cmd, popt, str):
|
||||
///
|
||||
/// echo hello world <tab> -> ("echo", "world", "")
|
||||
/// echo hello world<tab> -> ("echo", "hello", "world")
|
||||
///
|
||||
/// Insert results into comp_out. Return true to perform file completion, false to disable it.
|
||||
bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt,
|
||||
const wcstring &str, bool use_switches) {
|
||||
bool completer_t::complete_param_for_command(const wcstring &cmd_orig, const wcstring &popt,
|
||||
const wcstring &str, bool use_switches,
|
||||
bool *out_do_file) {
|
||||
bool use_common = true, use_files = true, has_force = false;
|
||||
|
||||
wcstring cmd, path;
|
||||
|
@ -1073,7 +1092,9 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt,
|
|||
// It's a match.
|
||||
wcstring desc = o.localized_desc();
|
||||
// Append a short-style option
|
||||
this->completions.add(wcstring{o.option}, std::move(desc), 0);
|
||||
if (!this->completions.add(wcstring{o.option}, std::move(desc), 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the long style option matches.
|
||||
|
@ -1116,15 +1137,23 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt,
|
|||
// functions.
|
||||
wcstring completion = format_string(L"%ls=", whole_opt.c_str() + offset);
|
||||
// Append a long-style option with a mandatory trailing equal sign
|
||||
this->completions.add(std::move(completion), C_(o.desc), flags | COMPLETE_NO_SPACE);
|
||||
if (!this->completions.add(std::move(completion), C_(o.desc),
|
||||
flags | COMPLETE_NO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Append a long-style option
|
||||
this->completions.add(whole_opt.substr(offset), C_(o.desc), flags);
|
||||
if (!this->completions.add(whole_opt.substr(offset), C_(o.desc), flags)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return has_force || use_files;
|
||||
if (!(has_force || use_files)) {
|
||||
*out_do_file = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Perform generic (not command-specific) expansions on the specified string.
|
||||
|
@ -1174,7 +1203,9 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file,
|
|||
for (completion_t &comp : local_completions) {
|
||||
comp.prepend_token_prefix(prefix_with_sep);
|
||||
}
|
||||
this->completions.add_list(std::move(local_completions));
|
||||
if (!this->completions.add_list(std::move(local_completions))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (complete_from_start) {
|
||||
|
@ -1189,6 +1220,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file,
|
|||
}
|
||||
|
||||
/// Complete the specified string as an environment variable.
|
||||
/// \return true if this was a variable, so we should stop completion.
|
||||
bool completer_t::complete_variable(const wcstring &str, size_t start_offset) {
|
||||
const wchar_t *const whole_var = str.c_str();
|
||||
const wchar_t *var = &whole_var[start_offset];
|
||||
|
@ -1237,7 +1269,8 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) {
|
|||
}
|
||||
|
||||
// Append matching environment variables
|
||||
this->completions.add(std::move(comp), std::move(desc), flags, *match);
|
||||
// TODO: need to propagate overflow here.
|
||||
(void)this->completions.add(std::move(comp), std::move(desc), flags, *match);
|
||||
|
||||
res = true;
|
||||
}
|
||||
|
@ -1340,14 +1373,15 @@ bool completer_t::try_complete_user(const wcstring &str) {
|
|||
const wchar_t *pw_name = pw_name_str.c_str();
|
||||
if (std::wcsncmp(user_name, pw_name, name_len) == 0) {
|
||||
wcstring desc = format_string(COMPLETE_USER_DESC, pw_name);
|
||||
// Append a user name
|
||||
this->completions.add(&pw_name[name_len], std::move(desc), COMPLETE_NO_SPACE);
|
||||
// Append a user name.
|
||||
// TODO: propagate overflow?
|
||||
(void)this->completions.add(&pw_name[name_len], std::move(desc), COMPLETE_NO_SPACE);
|
||||
result = true;
|
||||
} else if (wcsncasecmp(user_name, pw_name, name_len) == 0) {
|
||||
wcstring name = format_string(L"~%ls", pw_name);
|
||||
wcstring desc = format_string(COMPLETE_USER_DESC, pw_name);
|
||||
// Append a user name
|
||||
this->completions.add(
|
||||
(void)this->completions.add(
|
||||
std::move(name), std::move(desc),
|
||||
COMPLETE_REPLACES_TOKEN | COMPLETE_DONT_ESCAPE | COMPLETE_NO_SPACE);
|
||||
result = true;
|
||||
|
@ -1419,9 +1453,9 @@ void completer_t::complete_custom(const wcstring &cmd, const wcstring &cmdline,
|
|||
cleanup_t restore_vars{apply_var_assignments(*ad->var_assignments)};
|
||||
if (ctx.check_cancel()) return;
|
||||
|
||||
if (!complete_param(cmd, ad->previous_argument, ad->current_argument,
|
||||
!ad->had_ddash)) { // Invoke any custom completions for this command.
|
||||
ad->do_file = false;
|
||||
if (!complete_param_for_command(
|
||||
cmd, ad->previous_argument, ad->current_argument, !ad->had_ddash,
|
||||
&ad->do_file)) { // Invoke any custom completions for this command.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,22 +124,31 @@ using completion_list_t = std::vector<completion_t>;
|
|||
/// some conveniences.
|
||||
class completion_receiver_t {
|
||||
public:
|
||||
/// Construct, perhaps acquiring a list if necessary.
|
||||
completion_receiver_t() = default;
|
||||
explicit completion_receiver_t(completion_list_t &&v) : completions_(std::move(v)) {}
|
||||
/// The default limit on expansions.
|
||||
static constexpr size_t k_default_expansion_limit = 512 * 1024;
|
||||
|
||||
/// Construct with a limit.
|
||||
explicit completion_receiver_t(size_t limit = k_default_expansion_limit) : limit_(limit) {}
|
||||
|
||||
explicit completion_receiver_t(completion_list_t &&v, size_t limit = k_default_expansion_limit)
|
||||
: completions_(std::move(v)), limit_(limit) {}
|
||||
|
||||
/// Add a completion.
|
||||
void add(completion_t &&comp);
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add(completion_t &&comp);
|
||||
|
||||
/// Add a completion with the given string, and default other properties.
|
||||
void add(wcstring &&comp);
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add(wcstring &&comp);
|
||||
|
||||
/// Add a completion with the given string, description, flags, and fuzzy match.
|
||||
void add(wcstring &&comp, wcstring &&desc, complete_flags_t flags = 0,
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add(wcstring &&comp, wcstring &&desc, complete_flags_t flags = 0,
|
||||
string_fuzzy_match_t match = string_fuzzy_match_t::exact_match());
|
||||
|
||||
/// Add a list of completions.
|
||||
void add_list(completion_list_t &&lst);
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add_list(completion_list_t &&lst);
|
||||
|
||||
/// Swap our completions with a new list.
|
||||
void swap(completion_list_t &lst) { std::swap(completions_, lst); }
|
||||
|
@ -163,11 +172,21 @@ class completion_receiver_t {
|
|||
const completion_list_t &get_list() const { return completions_; }
|
||||
completion_list_t &get_list() { return completions_; }
|
||||
|
||||
/// \return the list of completions, clearing them.
|
||||
/// \return the list of completions, clearing it.
|
||||
completion_list_t take();
|
||||
|
||||
/// \return a new, empty receiver whose limit is our remaining capacity.
|
||||
/// This is useful for e.g. recursive calls when you want to act on the result before adding it.
|
||||
completion_receiver_t subreceiver() const;
|
||||
|
||||
private:
|
||||
// Our list of completions.
|
||||
completion_list_t completions_;
|
||||
|
||||
// The maximum number of completions to add. If our list length exceeds this, then new
|
||||
// completions are not added. Note 0 has no special significance here - use
|
||||
// numeric_limits<size_t>::max() instead.
|
||||
const size_t limit_;
|
||||
};
|
||||
|
||||
enum complete_option_type_t {
|
||||
|
|
111
src/expand.cpp
111
src/expand.cpp
|
@ -119,6 +119,20 @@ static void append_cmdsub_error(parse_error_list_t *errors, size_t source_start,
|
|||
errors->push_back(error);
|
||||
}
|
||||
|
||||
/// Append an overflow error, when expansion produces too much data.
|
||||
static expand_result_t append_overflow_error(parse_error_list_t *errors,
|
||||
size_t source_start = SOURCE_LOCATION_UNKNOWN) {
|
||||
if (errors) {
|
||||
parse_error_t error;
|
||||
error.source_start = source_start;
|
||||
error.source_length = 0;
|
||||
error.code = parse_error_generic;
|
||||
error.text = _(L"Expansion produced too many results");
|
||||
errors->push_back(std::move(error));
|
||||
}
|
||||
return expand_result_t::make_error(STATUS_EXPAND_ERROR);
|
||||
}
|
||||
|
||||
/// Test if the specified string does not contain character which can not be used inside a quoted
|
||||
/// string.
|
||||
static bool is_quotable(const wcstring &str) {
|
||||
|
@ -273,27 +287,26 @@ static size_t parse_slice(const wchar_t *in, wchar_t **end_ptr, std::vector<long
|
|||
/// Expand all environment variables in the string *ptr.
|
||||
///
|
||||
/// This function is slow, fragile and complicated. There are lots of little corner cases, like
|
||||
/// $$foo should do a double expansion, $foo$bar should not double expand bar, etc. Also, it's easy
|
||||
/// to accidentally leak memory on array out of bounds errors an various other situations. All in
|
||||
/// all, this function should be rewritten, split out into multiple logical units and carefully
|
||||
/// tested. After that, it can probably be optimized to do fewer memory allocations, fewer string
|
||||
/// scans and overall just less work. But until that happens, don't edit it unless you know exactly
|
||||
/// what you are doing, and do proper testing afterwards.
|
||||
/// $$foo should do a double expansion, $foo$bar should not double expand bar, etc.
|
||||
///
|
||||
/// This function operates on strings backwards, starting at last_idx.
|
||||
///
|
||||
/// Note: last_idx is considered to be where it previously finished procesisng. This means it
|
||||
/// actually starts operating on last_idx-1. As such, to process a string fully, pass string.size()
|
||||
/// as last_idx instead of string.size()-1.
|
||||
static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t last_idx,
|
||||
const environment_t &vars, parse_error_list_t *errors) {
|
||||
///
|
||||
/// \return the result of expansion.
|
||||
static expand_result_t expand_variables(wcstring instr, completion_receiver_t *out, size_t last_idx,
|
||||
const environment_t &vars, parse_error_list_t *errors) {
|
||||
const size_t insize = instr.size();
|
||||
|
||||
// last_idx may be 1 past the end of the string, but no further.
|
||||
assert(last_idx <= insize && "Invalid last_idx");
|
||||
if (last_idx == 0) {
|
||||
out->add(std::move(instr));
|
||||
return true;
|
||||
if (!out->add(std::move(instr))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
// Locate the last VARIABLE_EXPAND or VARIABLE_EXPAND_SINGLE
|
||||
|
@ -308,8 +321,10 @@ static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t
|
|||
}
|
||||
if (varexp_char_idx >= instr.size()) {
|
||||
// No variable expand char, we're done.
|
||||
out->add(std::move(instr));
|
||||
return true;
|
||||
if (!out->add(std::move(instr))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
// Get the variable name.
|
||||
|
@ -333,7 +348,7 @@ static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t
|
|||
parse_util_expand_variable_error(instr, 0 /* global_token_pos */, varexp_char_idx,
|
||||
errors);
|
||||
}
|
||||
return false;
|
||||
return expand_result_t::make_error(STATUS_EXPAND_ERROR);
|
||||
}
|
||||
|
||||
// Get the variable name as a string, then try to get the variable from env.
|
||||
|
@ -380,7 +395,7 @@ static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t
|
|||
} else {
|
||||
append_syntax_error(errors, slice_start + bad_pos, L"Invalid index value");
|
||||
}
|
||||
return false;
|
||||
return expand_result_t::make_error(STATUS_EXPAND_ERROR);
|
||||
}
|
||||
var_name_and_slice_stop = (slice_end - in);
|
||||
}
|
||||
|
@ -389,7 +404,7 @@ static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t
|
|||
// Expanding a non-existent variable.
|
||||
if (!is_single) {
|
||||
// Normal expansions of missing variables successfully expand to nothing.
|
||||
return true;
|
||||
return expand_result_t::ok;
|
||||
} else {
|
||||
// Expansion to single argument.
|
||||
// Replace the variable name and slice with VARIABLE_EXPAND_EMPTY.
|
||||
|
@ -460,7 +475,9 @@ static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t
|
|||
// Normal cartesian-product expansion.
|
||||
for (wcstring &item : var_item_list) {
|
||||
if (varexp_char_idx == 0 && var_name_and_slice_stop == insize) {
|
||||
out->add(std::move(item));
|
||||
if (!out->add(std::move(item))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
} else {
|
||||
wcstring new_in(instr, 0, varexp_char_idx);
|
||||
if (!new_in.empty()) {
|
||||
|
@ -472,13 +489,14 @@ static bool expand_variables(wcstring instr, completion_receiver_t *out, size_t
|
|||
}
|
||||
new_in.append(item);
|
||||
new_in.append(instr, var_name_and_slice_stop, wcstring::npos);
|
||||
if (!expand_variables(std::move(new_in), out, varexp_char_idx, vars, errors)) {
|
||||
return false;
|
||||
auto res = expand_variables(std::move(new_in), out, varexp_char_idx, vars, errors);
|
||||
if (res.result != expand_result_t::ok) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
/// Perform brace expansion, placing the expanded strings into \p out.
|
||||
|
@ -549,7 +567,9 @@ static expand_result_t expand_braces(wcstring &&instr, expand_flags_t flags,
|
|||
}
|
||||
|
||||
if (brace_begin == nullptr) {
|
||||
out->add(std::move(instr));
|
||||
if (!out->add(std::move(instr))) {
|
||||
return expand_result_t::error;
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
|
@ -608,7 +628,9 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
|
|||
return expand_result_t::make_error(STATUS_EXPAND_ERROR);
|
||||
}
|
||||
case 0: {
|
||||
out->add(std::move(input));
|
||||
if (!out->add(std::move(input))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
case 1: {
|
||||
|
@ -671,7 +693,7 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
|
|||
|
||||
// Recursively call ourselves to expand any remaining command substitutions. The result of this
|
||||
// recursive call using the tail of the string is inserted into the tail_expand array list
|
||||
completion_receiver_t tail_expand_recv;
|
||||
completion_receiver_t tail_expand_recv = out->subreceiver();
|
||||
expand_cmdsubst(input.substr(tail_begin), ctx, &tail_expand_recv,
|
||||
errors); // TODO: offset error locations
|
||||
completion_list_t tail_expand = tail_expand_recv.take();
|
||||
|
@ -689,7 +711,9 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
|
|||
whole_item.append(sub_item2);
|
||||
whole_item.push_back(INTERNAL_SEPARATOR);
|
||||
whole_item.append(tail_item.completion);
|
||||
out->add(std::move(whole_item));
|
||||
if (!out->add(std::move(whole_item))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -893,7 +917,9 @@ expand_result_t expander_t::stage_cmdsubst(wcstring input, completion_receiver_t
|
|||
size_t cur = 0, start = 0, end;
|
||||
switch (parse_util_locate_cmdsubst_range(input, &cur, nullptr, &start, &end, true)) {
|
||||
case 0:
|
||||
out->add(std::move(input));
|
||||
if (!out->add(std::move(input))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
case 1:
|
||||
append_cmdsub_error(errors, start, L"Command substitutions not allowed");
|
||||
|
@ -920,14 +946,14 @@ expand_result_t expander_t::stage_variables(wcstring input, completion_receiver_
|
|||
i = L'$';
|
||||
}
|
||||
}
|
||||
out->add(std::move(next));
|
||||
if (!out->add(std::move(next))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
} else {
|
||||
size_t size = next.size();
|
||||
if (!expand_variables(std::move(next), out, size, ctx.vars, errors)) {
|
||||
return expand_result_t::make_error(STATUS_EXPAND_ERROR);
|
||||
}
|
||||
return expand_variables(std::move(next), out, size, ctx.vars, errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
expand_result_t expander_t::stage_braces(wcstring input, completion_receiver_t *out) {
|
||||
|
@ -939,7 +965,9 @@ expand_result_t expander_t::stage_home_and_self(wcstring input, completion_recei
|
|||
expand_home_directory(input, ctx.vars);
|
||||
}
|
||||
expand_percent_self(input);
|
||||
out->add(std::move(input));
|
||||
if (!out->add(std::move(input))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
|
@ -1007,7 +1035,7 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_
|
|||
}
|
||||
|
||||
result = expand_result_t::wildcard_no_match;
|
||||
completion_receiver_t expanded_recv;
|
||||
completion_receiver_t expanded_recv = out->subreceiver();
|
||||
for (const auto &effective_working_dir : effective_working_dirs) {
|
||||
wildcard_expand_result_t expand_res = wildcard_expand_string(
|
||||
path_to_expand, effective_working_dir, flags, ctx.cancel_checker, &expanded_recv);
|
||||
|
@ -1017,6 +1045,9 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_
|
|||
break;
|
||||
case wildcard_expand_result_t::no_match:
|
||||
break;
|
||||
case wildcard_expand_result_t::overflow:
|
||||
result = expand_result_t::error;
|
||||
break;
|
||||
case wildcard_expand_result_t::cancel:
|
||||
result = expand_result_t::cancel;
|
||||
break;
|
||||
|
@ -1028,13 +1059,17 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_
|
|||
[&](const completion_t &a, const completion_t &b) {
|
||||
return wcsfilecmp_glob(a.completion.c_str(), b.completion.c_str()) < 0;
|
||||
});
|
||||
out->add_list(std::move(expanded));
|
||||
if (!out->add_list(std::move(expanded))) {
|
||||
result = expand_result_t::error;
|
||||
}
|
||||
} else {
|
||||
// Can't fully justify this check. I think it's that SKIP_WILDCARDS is used when completing
|
||||
// to mean don't do file expansions, so if we're not doing file expansions, just drop this
|
||||
// completion on the floor.
|
||||
if (!(flags & expand_flag::for_completions)) {
|
||||
out->add(std::move(path_to_expand));
|
||||
if (!out->add(std::move(path_to_expand))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -1047,7 +1082,9 @@ expand_result_t expander_t::expand_string(wcstring input, completion_receiver_t
|
|||
"Must have a parser if not skipping command substitutions");
|
||||
// Early out. If we're not completing, and there's no magic in the input, we're done.
|
||||
if (!(flags & expand_flag::for_completions) && expand_is_clean(input)) {
|
||||
out_completions->add(std::move(input));
|
||||
if (!out_completions->add(std::move(input))) {
|
||||
return append_overflow_error(errors);
|
||||
}
|
||||
return expand_result_t::ok;
|
||||
}
|
||||
|
||||
|
@ -1062,7 +1099,7 @@ expand_result_t expander_t::expand_string(wcstring input, completion_receiver_t
|
|||
completion_list_t completions;
|
||||
append_completion(&completions, input);
|
||||
|
||||
completion_receiver_t output_storage;
|
||||
completion_receiver_t output_storage = out_completions->subreceiver();
|
||||
expand_result_t total_result = expand_result_t::ok;
|
||||
for (stage_t stage : stages) {
|
||||
for (completion_t &comp : completions) {
|
||||
|
@ -1101,7 +1138,9 @@ expand_result_t expander_t::expand_string(wcstring input, completion_receiver_t
|
|||
if (!(flags & expand_flag::skip_home_directories)) {
|
||||
unexpand_tildes(input, ctx.vars, &completions);
|
||||
}
|
||||
out_completions->add_list(std::move(completions));
|
||||
if (!out_completions->add_list(std::move(completions))) {
|
||||
total_result = append_overflow_error(errors);
|
||||
}
|
||||
}
|
||||
return total_result;
|
||||
}
|
||||
|
|
|
@ -2118,6 +2118,37 @@ static void test_expand() {
|
|||
popd();
|
||||
}
|
||||
|
||||
static void test_expand_overflow() {
|
||||
say(L"Testing overflowing expansions");
|
||||
// Ensure that we have sane limits on number of expansions - see #7497.
|
||||
|
||||
// Make a list of 64 elements, then expand it cartesian-style 64 times.
|
||||
// This is far too large to expand.
|
||||
wcstring_list_t vals;
|
||||
wcstring expansion;
|
||||
for (int i = 1; i <= 64; i++) {
|
||||
vals.push_back(to_string(i));
|
||||
expansion.append(L"$bigvar");
|
||||
}
|
||||
|
||||
auto parser = parser_t::principal_parser().shared();
|
||||
parser->vars().push(true);
|
||||
int set = parser->vars().set(L"bigvar", ENV_LOCAL, std::move(vals));
|
||||
do_test(set == ENV_OK);
|
||||
|
||||
parse_error_list_t errors;
|
||||
operation_context_t ctx{parser, parser->vars(), no_cancel};
|
||||
|
||||
// We accept only 1024 completions.
|
||||
completion_receiver_t output{1024};
|
||||
|
||||
auto res = expand_string(expansion, &output, expand_flags_t{}, ctx, &errors);
|
||||
do_test(!errors.empty());
|
||||
do_test(res == expand_result_t::error);
|
||||
|
||||
parser->vars().pop();
|
||||
}
|
||||
|
||||
static void test_fuzzy_match() {
|
||||
say(L"Testing fuzzy string matching");
|
||||
// Check that a string fuzzy match has the expected type and case folding.
|
||||
|
@ -6131,6 +6162,7 @@ int main(int argc, char **argv) {
|
|||
if (should_test_function("pcre2_escape")) test_pcre2_escape();
|
||||
if (should_test_function("lru")) test_lru();
|
||||
if (should_test_function("expand")) test_expand();
|
||||
if (should_test_function("expand")) test_expand_overflow();
|
||||
if (should_test_function("fuzzy_match")) test_fuzzy_match();
|
||||
if (should_test_function("ifind")) test_ifind();
|
||||
if (should_test_function("ifind_fuzzy")) test_ifind_fuzzy();
|
||||
|
|
|
@ -226,6 +226,9 @@ enum class pipeline_position_t {
|
|||
/// Error message for wildcards with no matches.
|
||||
#define WILDCARD_ERR_MSG _(L"No matches for wildcard '%ls'. See `help expand`.")
|
||||
|
||||
/// Error message when an expansion produces too many results, e.g. `echo /**`.
|
||||
#define EXPAND_OVERFLOW_ERR_MSG _(L"Too many items produced by '%ls'.")
|
||||
|
||||
/// Error when using break outside of loop.
|
||||
#define INVALID_BREAK_ERR_MSG _(L"'break' while not inside of loop")
|
||||
|
||||
|
|
|
@ -253,7 +253,9 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
|
|||
// Note: out_completion may be empty if the completion really is empty, e.g. tab-completing
|
||||
// 'foo' when a file 'foo' exists.
|
||||
complete_flags_t local_flags = flags | (full_replacement ? COMPLETE_REPLACES_TOKEN : 0);
|
||||
out->add(std::move(out_completion), std::move(out_desc), local_flags, *match);
|
||||
if (!out->add(std::move(out_completion), std::move(out_desc), local_flags, *match)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (next_wc_char_pos > 0) {
|
||||
// The literal portion of a wildcard cannot be longer than the string itself,
|
||||
|
@ -529,6 +531,8 @@ class wildcard_expander_t {
|
|||
completion_receiver_t *resolved_completions;
|
||||
// Whether we have been interrupted.
|
||||
bool did_interrupt{false};
|
||||
// Whether we have overflowed.
|
||||
bool did_overflow{false};
|
||||
// Whether we have successfully added any completions.
|
||||
bool did_add{false};
|
||||
// Whether some parent expansion is fuzzy, and therefore completions always prepend their prefix
|
||||
|
@ -562,17 +566,18 @@ class wildcard_expander_t {
|
|||
const wcstring &prefix);
|
||||
|
||||
/// Indicate whether we should cancel wildcard expansion. This latches 'interrupt'.
|
||||
bool interrupted() {
|
||||
bool interrupted_or_overflowed() {
|
||||
did_interrupt = did_interrupt || cancel_checker();
|
||||
return did_interrupt;
|
||||
return did_interrupt || did_overflow;
|
||||
}
|
||||
|
||||
void add_expansion_result(wcstring &&result) {
|
||||
// This function is only for the non-completions case.
|
||||
assert(!(this->flags & expand_flag::for_completions));
|
||||
if (this->completion_set.insert(result).second) {
|
||||
this->resolved_completions->add(std::move(result));
|
||||
this->did_add = true;
|
||||
if (!this->resolved_completions->add(std::move(result))) {
|
||||
this->did_overflow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -610,7 +615,7 @@ class wildcard_expander_t {
|
|||
}
|
||||
|
||||
// We stop if we got two or more entries; also stop if we got zero or were interrupted
|
||||
if (unique_entry.empty() || interrupted()) {
|
||||
if (unique_entry.empty() || interrupted_or_overflowed()) {
|
||||
stop_descent = true;
|
||||
}
|
||||
|
||||
|
@ -716,7 +721,7 @@ class wildcard_expander_t {
|
|||
};
|
||||
|
||||
void wildcard_expander_t::expand_trailing_slash(const wcstring &base_dir, const wcstring &prefix) {
|
||||
if (interrupted()) {
|
||||
if (interrupted_or_overflowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -731,7 +736,7 @@ void wildcard_expander_t::expand_trailing_slash(const wcstring &base_dir, const
|
|||
DIR *dir = open_dir(base_dir);
|
||||
if (dir) {
|
||||
wcstring next;
|
||||
while (wreaddir(dir, next) && !interrupted()) {
|
||||
while (wreaddir(dir, next) && !interrupted_or_overflowed()) {
|
||||
if (!next.empty() && next.at(0) != L'.') {
|
||||
this->try_add_completion_result(base_dir + next, next, L"", prefix);
|
||||
}
|
||||
|
@ -746,7 +751,7 @@ void wildcard_expander_t::expand_intermediate_segment(const wcstring &base_dir,
|
|||
const wchar_t *wc_remainder,
|
||||
const wcstring &prefix) {
|
||||
wcstring name_str;
|
||||
while (!interrupted() && wreaddir_for_dirs(base_dir_fp, &name_str)) {
|
||||
while (!interrupted_or_overflowed() && wreaddir_for_dirs(base_dir_fp, &name_str)) {
|
||||
// Note that it's critical we ignore leading dots here, else we may descend into . and ..
|
||||
if (!wildcard_match(name_str, wc_segment, true)) {
|
||||
// Doesn't match the wildcard for this segment, skip it.
|
||||
|
@ -788,7 +793,7 @@ void wildcard_expander_t::expand_literal_intermediate_segment_with_fuzz(const wc
|
|||
// Mark that we are fuzzy for the duration of this function
|
||||
const scoped_push<bool> scoped_fuzzy(&this->has_fuzzy_ancestor, true);
|
||||
|
||||
while (!interrupted() && wreaddir_for_dirs(base_dir_fp, &name_str)) {
|
||||
while (!interrupted_or_overflowed() && wreaddir_for_dirs(base_dir_fp, &name_str)) {
|
||||
// Don't bother with . and ..
|
||||
if (name_str == L"." || name_str == L"..") {
|
||||
continue;
|
||||
|
@ -873,7 +878,7 @@ void wildcard_expander_t::expand(const wcstring &base_dir, const wchar_t *wc,
|
|||
const wcstring &effective_prefix) {
|
||||
assert(wc != nullptr);
|
||||
|
||||
if (interrupted()) {
|
||||
if (interrupted_or_overflowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ enum class wildcard_expand_result_t {
|
|||
no_match, /// The wildcard did not match.
|
||||
match, /// The wildcard did match.
|
||||
cancel, /// Expansion was cancelled (e.g. control-C).
|
||||
overflow, /// Expansion produced too many results.
|
||||
};
|
||||
wildcard_expand_result_t wildcard_expand_string(const wcstring &wc,
|
||||
const wcstring &working_directory,
|
||||
|
|
Loading…
Reference in a new issue