diff --git a/crates/bashkit/src/builtins/rg/mod.rs b/crates/bashkit/src/builtins/rg/mod.rs index cb7184ab..0458246e 100644 --- a/crates/bashkit/src/builtins/rg/mod.rs +++ b/crates/bashkit/src/builtins/rg/mod.rs @@ -2258,7 +2258,17 @@ fn apply_path_separator_to_display(display: &str, opts: &RgOptions) -> String { } } +const RG_GLOB_MAX_BRACE_DEPTH: usize = 32; + fn glob_to_regex(pattern: &str) -> String { + glob_to_regex_with_depth(pattern, 0) +} + +fn glob_to_regex_with_depth(pattern: &str, depth: usize) -> String { + if depth >= RG_GLOB_MAX_BRACE_DEPTH { + return format!("^{}$", regex::escape(pattern.trim_start_matches('/'))); + } + let mut out = String::new(); out.push('^'); @@ -2292,7 +2302,7 @@ fn glob_to_regex(pattern: &str) -> String { } } '{' => { - if let Some((alternation, next)) = glob_alternation_to_regex(&chars, i) { + if let Some((alternation, next)) = glob_alternation_to_regex(&chars, i, depth + 1) { out.push_str(&alternation); i = next; } else { @@ -2314,7 +2324,14 @@ fn glob_to_regex(pattern: &str) -> String { out } -fn glob_alternation_to_regex(chars: &[char], start: usize) -> Option<(String, usize)> { +fn glob_alternation_to_regex( + chars: &[char], + start: usize, + recursion_depth: usize, +) -> Option<(String, usize)> { + if recursion_depth >= RG_GLOB_MAX_BRACE_DEPTH { + return None; + } let mut alts = Vec::new(); let mut current = String::new(); let mut depth = 0usize; @@ -2343,7 +2360,7 @@ fn glob_alternation_to_regex(chars: &[char], start: usize) -> Option<(String, us if idx > 0 { out.push('|'); } - let alt_regex = glob_to_regex(alt); + let alt_regex = glob_to_regex_with_depth(alt, recursion_depth); out.push_str(&alt_regex[1..alt_regex.len() - 1]); } out.push(')'); @@ -5924,6 +5941,12 @@ mod tests { use std::path::{Path, PathBuf}; use std::sync::Arc; + #[test] + fn glob_brace_alternation_depth_limit_does_not_expand_nested_pattern() { + let regex = glob_to_regex_with_depth("{a,b}", RG_GLOB_MAX_BRACE_DEPTH); + assert_eq!(regex, r"^\{a,b\}$"); + } + #[test] fn rg_rejects_too_many_ignore_rules_per_file() { let content = test_ignore_content_with_n_rules(RG_IGNORE_RULES_MAX_PER_FILE + 1);