diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index ae24f2c5..445fc3fa 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -1412,7 +1412,11 @@ impl<'a> Lexer<'a> { /// Check if the content starting with { looks like a brace expansion /// Brace expansion: {a,b,c} or {1..5} (contains , or ..) /// Brace group: { cmd; } (contains spaces, semicolons, newlines) + /// THREAT[TM-DOS]: Caps lookahead to prevent O(n^2) scanning when input + /// contains many unmatched `{` characters (issue #997). fn looks_like_brace_expansion(&self) -> bool { + const MAX_LOOKAHEAD: usize = 10_000; + // Clone the iterator to peek ahead without consuming let mut chars = self.chars.clone(); @@ -1425,8 +1429,13 @@ impl<'a> Lexer<'a> { let mut has_comma = false; let mut has_dot_dot = false; let mut prev_char = None; + let mut scanned = 0usize; for ch in chars { + scanned += 1; + if scanned > MAX_LOOKAHEAD { + return false; + } match ch { '{' => depth += 1, '}' => { diff --git a/crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh b/crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh new file mode 100644 index 00000000..3c2618d6 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh @@ -0,0 +1,30 @@ +# Brace expansion lookahead limit +# Regression tests for issue #997 + +### normal_brace_expansion +# Normal brace expansion still works +echo {a,b,c} +### expect +a b c +### end + +### range_brace_expansion +# Range brace expansion still works +echo {1..3} +### expect +1 2 3 +### end + +### unmatched_brace_literal +# Unmatched { is treated as literal +echo {abc +### expect +{abc +### end + +### nested_brace_expansion +# Nested brace expansion works +echo {a,{b,c}} +### expect +a b c +### end