use grep_regex::{RegexMatcher, RegexMatcherBuilder};
#[derive(Debug)]
pub(super) struct MatcherBuildResult {
pub(super) matchers: Vec<RegexMatcher>,
pub(super) patterns_adjusted: usize,
pub(super) literal_fallbacks: usize,
}
pub(super) fn build_matchers(patterns: &[String]) -> anyhow::Result<MatcherBuildResult> {
let mut result = MatcherBuildResult {
matchers: Vec::with_capacity(patterns.len()),
patterns_adjusted: 0,
literal_fallbacks: 0,
};
for pattern in patterns {
let built = build_matcher_with_stats(pattern)?;
result.patterns_adjusted += usize::from(built.adjusted);
result.literal_fallbacks += usize::from(built.literal_fallback);
result.matchers.push(built.matcher);
}
Ok(result)
}
struct BuiltMatcher {
matcher: RegexMatcher,
adjusted: bool,
literal_fallback: bool,
}
fn build_matcher_with_stats(pattern: &str) -> anyhow::Result<BuiltMatcher> {
let sanitized = sanitize_braces(pattern);
match compile(&sanitized) {
Ok(matcher) => Ok(BuiltMatcher {
matcher,
adjusted: sanitized != pattern,
literal_fallback: false,
}),
Err(error) if is_group_error(&error.to_string()) => {
let escaped = escape_unescaped_parentheses(&sanitized);
match compile(&escaped) {
Ok(matcher) => Ok(BuiltMatcher {
matcher,
adjusted: escaped != pattern,
literal_fallback: false,
}),
Err(_) => Ok(BuiltMatcher {
matcher: compile(®ex::escape(pattern))?,
adjusted: true,
literal_fallback: true,
}),
}
}
Err(_) => Ok(BuiltMatcher {
matcher: compile(®ex::escape(pattern))?,
adjusted: true,
literal_fallback: true,
}),
}
}
fn compile(pattern: &str) -> anyhow::Result<RegexMatcher> {
RegexMatcherBuilder::new()
.case_insensitive(false)
.multi_line(true)
.line_terminator(Some(b'\n'))
.build(pattern)
.map_err(Into::into)
}
fn is_group_error(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
lower.contains("unclosed group")
|| lower.contains("unopened group")
|| lower.contains("unclosed") && lower.contains("(")
|| lower.contains("unopened") && lower.contains(")")
}
pub(super) fn sanitize_braces(pattern: &str) -> String {
let chars = pattern.chars().collect::<Vec<_>>();
let mut out = String::with_capacity(pattern.len());
let mut index = 0;
let mut escaped = false;
while index < chars.len() {
let ch = chars[index];
if escaped {
out.push(ch);
if matches!(ch, 'x' | 'p' | 'P')
&& chars.get(index + 1) == Some(&'{')
&& let Some(end) = find_closing_brace(&chars, index + 2)
{
for c in &chars[index + 1..=end] {
out.push(*c);
}
index = end + 1;
escaped = false;
continue;
}
escaped = false;
index += 1;
continue;
}
match ch {
'\\' => {
out.push(ch);
escaped = true;
}
'{' if is_valid_quantifier_at(&chars, index) => {
let end = find_closing_brace(&chars, index + 1).unwrap_or(index);
for c in &chars[index..=end] {
out.push(*c);
}
index = end + 1;
continue;
}
'{' | '}' => {
out.push('\\');
out.push(ch);
}
_ => out.push(ch),
}
index += 1;
}
out
}
fn find_closing_brace(chars: &[char], mut index: usize) -> Option<usize> {
while index < chars.len() {
if chars[index] == '}' {
return Some(index);
}
index += 1;
}
None
}
fn is_valid_quantifier_at(chars: &[char], index: usize) -> bool {
if index == 0 || !can_quantify(chars[index - 1]) {
return false;
}
let mut cursor = index + 1;
let first_digits = cursor;
while chars.get(cursor).is_some_and(|ch| ch.is_ascii_digit()) {
cursor += 1;
}
if cursor == first_digits {
return false;
}
if chars.get(cursor) == Some(&',') {
cursor += 1;
while chars.get(cursor).is_some_and(|ch| ch.is_ascii_digit()) {
cursor += 1;
}
}
chars.get(cursor) == Some(&'}')
}
fn can_quantify(ch: char) -> bool {
!matches!(ch, '|' | '(' | '^')
}
pub(super) fn escape_unescaped_parentheses(pattern: &str) -> String {
let mut out = String::with_capacity(pattern.len());
let mut escaped = false;
for ch in pattern.chars() {
if escaped {
out.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => {
out.push(ch);
escaped = true;
}
'(' | ')' => {
out.push('\\');
out.push(ch);
}
_ => out.push(ch),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use grep_matcher::Matcher;
fn is_match(pattern: &str, haystack: &str) -> bool {
build_matchers(&[pattern.to_owned()])
.unwrap()
.matchers
.first()
.expect("one matcher")
.is_match(haystack.as_bytes())
.unwrap()
}
#[test]
fn sanitize_braces_preserves_valid_regex_braces() {
assert_eq!(sanitize_braces("a{2,4}"), "a{2,4}");
assert_eq!(sanitize_braces("a{2}"), "a{2}");
assert_eq!(sanitize_braces("a{2,}"), "a{2,}");
assert_eq!(sanitize_braces(r"\x{1F600}"), r"\x{1F600}");
assert_eq!(sanitize_braces(r"\p{L}"), r"\p{L}");
assert_eq!(sanitize_braces(r"\P{Greek}"), r"\P{Greek}");
}
#[test]
fn sanitize_braces_escapes_stray_braces() {
assert_eq!(sanitize_braces("${x}"), r"$\{x\}");
assert_eq!(sanitize_braces("a } b"), r"a \} b");
}
#[test]
fn escape_unescaped_parentheses_rescues_group_errors() {
assert_eq!(escape_unescaped_parentheses("foo(bar"), r"foo\(bar");
assert!(is_match("foo(bar", "foo(bar"));
assert!(is_match("foo)bar", "foo)bar"));
}
#[test]
fn literal_fallback_after_total_regex_failure() {
assert!(is_match("[", "["));
}
#[test]
fn line_anchor_pattern_compiles() {
assert!(is_match("^needle$", "needle"));
assert!(!is_match("^needle$", "xneedle"));
}
#[test]
fn matcher_builder_reports_adjustments_and_fallbacks() {
let result = build_matchers(&["needle".into(), "${x}".into(), "[".into()]).unwrap();
assert_eq!(result.matchers.len(), 3);
assert_eq!(result.patterns_adjusted, 2);
assert_eq!(result.literal_fallbacks, 1);
}
}