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
}