Adopt expansion limits in wildcard expansions

This prevents e.g. `count /**` from consuming all of your memory.

Fixes #7226
This commit is contained in:
ridiculousfish 2020-12-05 13:07:19 -08:00
parent f11a60473a
commit 594a6a35e8
4 changed files with 69 additions and 60 deletions

View file

@ -124,12 +124,13 @@ using completion_list_t = std::vector<completion_t>;
/// some conveniences.
class completion_receiver_t {
public:
/// The default limit on expansions.
/// The default maximum number of items that something may expand to.
static constexpr size_t k_default_expansion_limit = 512 * 1024;
/// Construct with a limit.
/// Construct as empty, with a limit.
explicit completion_receiver_t(size_t limit = k_default_expansion_limit) : limit_(limit) {}
/// Acquire an existing list, with a limit.
explicit completion_receiver_t(completion_list_t &&v, size_t limit = k_default_expansion_limit)
: completions_(std::move(v)), limit_(limit) {}

View file

@ -1037,20 +1037,18 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_
result = expand_result_t::wildcard_no_match;
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(
wildcard_result_t expand_res = wildcard_expand_string(
path_to_expand, effective_working_dir, flags, ctx.cancel_checker, &expanded_recv);
switch (expand_res) {
case wildcard_expand_result_t::match:
case wildcard_result_t::match:
result = expand_result_t::ok;
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;
case wildcard_result_t::no_match:
break;
case wildcard_result_t::overflow:
return append_overflow_error(errors);
case wildcard_result_t::cancel:
return expand_result_t::cancel;
}
}

View file

@ -206,17 +206,19 @@ static bool has_prefix_match(const completion_receiver_t *comps, size_t first) {
///
/// We ignore ANY_STRING_RECURSIVE here. The consequence is that you cannot tab complete **
/// wildcards. This is historic behavior.
static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
const wchar_t *const wc, size_t wc_len,
const wc_complete_pack_t &params, complete_flags_t flags,
completion_receiver_t *out, bool is_first_call = false) {
static wildcard_result_t wildcard_complete_internal(const wchar_t *const str, size_t str_len,
const wchar_t *const wc, size_t wc_len,
const wc_complete_pack_t &params,
complete_flags_t flags,
completion_receiver_t *out,
bool is_first_call = false) {
assert(str != nullptr);
assert(wc != nullptr);
// Maybe early out for hidden files. We require that the wildcard match these exactly (i.e. a
// dot); ANY_STRING not allowed.
if (is_first_call && str[0] == L'.' && wc[0] != L'.') {
return false;
return wildcard_result_t::no_match;
}
// Locate the next wildcard character position, e.g. ANY_CHAR or ANY_STRING.
@ -226,17 +228,17 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
if (next_wc_char_pos == wcstring::npos) {
// Try matching.
maybe_t<string_fuzzy_match_t> match = string_fuzzy_match_string(wc, str);
if (!match) return false;
if (!match) return wildcard_result_t::no_match;
// If we're not allowing fuzzy match, then we require a prefix match.
bool needs_prefix_match = !(params.expand_flags & expand_flag::fuzzy_match);
if (needs_prefix_match && !match->is_exact_or_prefix()) {
return false;
return wildcard_result_t::no_match;
}
// The match was successful. If the string is not requested we're done.
if (out == nullptr) {
return true;
return wildcard_result_t::match;
}
// Wildcard complete.
@ -254,14 +256,14 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
// 'foo' when a file 'foo' exists.
complete_flags_t local_flags = flags | (full_replacement ? COMPLETE_REPLACES_TOKEN : 0);
if (!out->add(std::move(out_completion), std::move(out_desc), local_flags, *match)) {
return false;
return wildcard_result_t::overflow;
}
return true;
return wildcard_result_t::match;
} else if (next_wc_char_pos > 0) {
// The literal portion of a wildcard cannot be longer than the string itself,
// e.g. `abc*` can never match a string that is only two characters long.
if (next_wc_char_pos >= str_len) {
return false;
return wildcard_result_t::no_match;
}
// Here we have a non-wildcard prefix. Note that we don't do fuzzy matching for stuff before
@ -278,7 +280,7 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
wc + next_wc_char_pos, wc_len - next_wc_char_pos,
params, flags | COMPLETE_REPLACES_TOKEN, out);
}
return false; // no match
return wildcard_result_t::no_match;
}
// Our first character is a wildcard.
@ -286,7 +288,7 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
switch (wc[0]) {
case ANY_CHAR: {
if (str[0] == L'\0') {
return false;
return wildcard_result_t::no_match;
}
return wildcard_complete_internal(str + 1, str_len - 1, wc + 1, wc_len - 1, params,
flags, out);
@ -305,23 +307,30 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
bool has_match = false;
for (size_t i = 0; str[i] != L'\0'; i++) {
const size_t before_count = out ? out->size() : 0;
if (wildcard_complete_internal(str + i, str_len - i, wc + 1, wc_len - 1, params,
flags, out)) {
// We found a match.
has_match = true;
// If out is NULL, we don't care about the actual matches. If out is not
// NULL but we have a prefix match, stop there.
if (out == nullptr || has_prefix_match(out, before_count)) {
auto submatch_res = wildcard_complete_internal(str + i, str_len - i, wc + 1,
wc_len - 1, params, flags, out);
switch (submatch_res) {
case wildcard_result_t::no_match:
break;
}
case wildcard_result_t::match:
has_match = true;
// If out is NULL, we don't care about the actual matches. If out is not
// NULL but we have a prefix match, stop there.
if (out == nullptr || has_prefix_match(out, before_count)) {
return wildcard_result_t::match;
}
break;
case wildcard_result_t::cancel:
case wildcard_result_t::overflow:
// Note early return.
return submatch_res;
}
}
return has_match;
return has_match ? wildcard_result_t::match : wildcard_result_t::no_match;
}
case ANY_STRING_RECURSIVE: {
// We don't even try with this one.
return false;
return wildcard_result_t::no_match;
}
default: {
DIE("unreachable code reached");
@ -331,10 +340,10 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
DIE("unreachable code reached");
}
bool wildcard_complete(const wcstring &str, const wchar_t *wc,
const std::function<wcstring(const wcstring &)> &desc_func,
completion_receiver_t *out, expand_flags_t expand_flags,
complete_flags_t flags) {
wildcard_result_t wildcard_complete(const wcstring &str, const wchar_t *wc,
const std::function<wcstring(const wcstring &)> &desc_func,
completion_receiver_t *out, expand_flags_t expand_flags,
complete_flags_t flags) {
// Note out may be NULL.
assert(wc != nullptr);
wc_complete_pack_t params(str, desc_func, expand_flags);
@ -453,7 +462,7 @@ static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wc
const wchar_t *wc, expand_flags_t expand_flags,
completion_receiver_t *out) {
// Check if it will match before stat().
if (!wildcard_complete(filename, wc, {}, nullptr, expand_flags, 0)) {
if (wildcard_complete(filename, wc, {}, nullptr, expand_flags, 0) != wildcard_result_t::match) {
return false;
}
@ -511,9 +520,10 @@ static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wc
auto desc_func = const_desc(desc);
if (is_directory) {
return wildcard_complete(filename + L'/', wc, desc_func, out, expand_flags,
COMPLETE_NO_SPACE);
COMPLETE_NO_SPACE) == wildcard_result_t::match;
}
return wildcard_complete(filename, wc, desc_func, out, expand_flags, 0);
return wildcard_complete(filename, wc, desc_func, out, expand_flags, 0) ==
wildcard_result_t::match;
}
class wildcard_expander_t {
@ -712,11 +722,13 @@ class wildcard_expander_t {
// Do wildcard expansion. This is recursive.
void expand(const wcstring &base_dir, const wchar_t *wc, const wcstring &prefix);
wildcard_expand_result_t status_code() const {
wildcard_result_t status_code() const {
if (this->did_interrupt) {
return wildcard_expand_result_t::cancel;
return wildcard_result_t::cancel;
} else if (this->did_overflow) {
return wildcard_result_t::overflow;
}
return this->did_add ? wildcard_expand_result_t::match : wildcard_expand_result_t::no_match;
return this->did_add ? wildcard_result_t::match : wildcard_result_t::no_match;
}
};
@ -962,11 +974,10 @@ void wildcard_expander_t::expand(const wcstring &base_dir, const wchar_t *wc,
}
}
wildcard_expand_result_t wildcard_expand_string(const wcstring &wc,
const wcstring &working_directory,
expand_flags_t flags,
const cancel_checker_t &cancel_checker,
completion_receiver_t *output) {
wildcard_result_t wildcard_expand_string(const wcstring &wc, const wcstring &working_directory,
expand_flags_t flags,
const cancel_checker_t &cancel_checker,
completion_receiver_t *output) {
assert(output != nullptr);
// Fuzzy matching only if we're doing completions.
assert(flags.get(expand_flag::for_completions) || !flags.get(expand_flag::fuzzy_match));
@ -983,7 +994,7 @@ wildcard_expand_result_t wildcard_expand_string(const wcstring &wc,
// embedded nulls are never allowed in a filename, so we just check for them and return 0 (no
// matches) if there is an embedded null.
if (wc.find(L'\0') != wcstring::npos) {
return wildcard_expand_result_t::no_match;
return wildcard_result_t::no_match;
}
// Compute the prefix and base dir. The prefix is what we prepend for filesystem operations

View file

@ -41,17 +41,16 @@ enum {
/// executables_only
/// \param output The list in which to put the output
///
enum class wildcard_expand_result_t {
enum class wildcard_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,
expand_flags_t flags,
const cancel_checker_t &cancel_checker,
completion_receiver_t *output);
wildcard_result_t wildcard_expand_string(const wcstring &wc, const wcstring &working_directory,
expand_flags_t flags,
const cancel_checker_t &cancel_checker,
completion_receiver_t *output);
/// Test whether the given wildcard matches the string. Does not perform any I/O.
///
@ -69,8 +68,8 @@ bool wildcard_has(const wcstring &, bool internal);
bool wildcard_has(const wchar_t *, bool internal);
/// Test wildcard completion.
bool wildcard_complete(const wcstring &str, const wchar_t *wc, const description_func_t &desc_func,
completion_receiver_t *out, expand_flags_t expand_flags,
complete_flags_t flags);
wildcard_result_t wildcard_complete(const wcstring &str, const wchar_t *wc,
const description_func_t &desc_func, completion_receiver_t *out,
expand_flags_t expand_flags, complete_flags_t flags);
#endif