pub(crate) fn build_pattern(
patterns: &[String],
fixed_strings: bool,
word_regexp: bool,
ignore_case: bool,
smart_case: bool,
) -> anyhow::Result<String> {
let patterns: Vec<String> = if fixed_strings {
patterns.iter().map(|p| regex::escape(p)).collect()
} else {
patterns.to_vec()
};
let insensitive = ignore_case || (smart_case && smart_case_insensitive(&patterns));
sanitize_line_terminators(&compose_pattern(&patterns, word_regexp, insensitive))
}
fn compose_pattern(patterns: &[String], word_regexp: bool, insensitive: bool) -> String {
let transformed: Vec<String> = patterns
.iter()
.map(|p| {
if word_regexp {
format!(r"\b{{start-half}}(?:{p})\b{{end-half}}")
} else {
p.clone()
}
})
.collect();
let joined = if transformed.len() == 1 {
transformed.into_iter().next().expect("one pattern")
} else {
transformed
.iter()
.map(|p| format!("(?:{p})"))
.collect::<Vec<_>>()
.join("|")
};
if insensitive {
format!("(?mi){joined}")
} else {
format!("(?m){joined}")
}
}
fn sanitize_line_terminators(pattern: &str) -> anyhow::Result<String> {
use regex_syntax::hir::{Class, Hir, HirKind, Literal};
fn strip(hir: &Hir) -> anyhow::Result<Hir> {
Ok(match hir.kind() {
HirKind::Literal(Literal(bytes)) => {
anyhow::ensure!(
!bytes.contains(&b'\n'),
"the literal '\\n' is not allowed in a regex"
);
hir.clone()
}
HirKind::Class(Class::Bytes(class)) => {
let mut class = class.clone();
let mut newline =
regex_syntax::hir::ClassBytes::new([regex_syntax::hir::ClassBytesRange::new(
b'\n', b'\n',
)]);
newline.negate();
class.intersect(&newline);
Hir::class(Class::Bytes(class))
}
HirKind::Class(Class::Unicode(class)) => {
let mut class = class.clone();
let mut newline = regex_syntax::hir::ClassUnicode::new([
regex_syntax::hir::ClassUnicodeRange::new('\n', '\n'),
]);
newline.negate();
class.intersect(&newline);
Hir::class(Class::Unicode(class))
}
HirKind::Repetition(rep) => {
let mut rep = rep.clone();
rep.sub = Box::new(strip(&rep.sub)?);
Hir::repetition(rep)
}
HirKind::Capture(cap) => {
let mut cap = cap.clone();
cap.sub = Box::new(strip(&cap.sub)?);
Hir::capture(cap)
}
HirKind::Concat(subs) => {
Hir::concat(subs.iter().map(strip).collect::<anyhow::Result<_>>()?)
}
HirKind::Alternation(subs) => {
Hir::alternation(subs.iter().map(strip).collect::<anyhow::Result<_>>()?)
}
HirKind::Empty | HirKind::Look(_) => hir.clone(),
})
}
let hir = regex_syntax::ParserBuilder::new()
.utf8(false)
.build()
.parse(pattern)?;
Ok(strip(&hir)?.to_string())
}
fn smart_case_insensitive(patterns: &[String]) -> bool {
let mut literals = Vec::new();
for pattern in patterns {
let Ok(hir) = regex_syntax::parse(pattern) else {
return false; };
collect_literal_chars(&hir, &mut literals);
}
!literals.is_empty() && !literals.iter().any(|c| c.is_uppercase())
}
fn collect_literal_chars(hir: ®ex_syntax::hir::Hir, out: &mut Vec<char>) {
use regex_syntax::hir::HirKind;
match hir.kind() {
HirKind::Literal(lit) => out.extend(String::from_utf8_lossy(&lit.0).chars()),
HirKind::Concat(children) | HirKind::Alternation(children) => {
for child in children {
collect_literal_chars(child, out);
}
}
HirKind::Capture(capture) => collect_literal_chars(&capture.sub, out),
HirKind::Repetition(rep) => collect_literal_chars(&rep.sub, out),
HirKind::Empty | HirKind::Class(_) | HirKind::Look(_) => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn one(p: &str) -> Vec<String> {
vec![p.to_owned()]
}
#[test]
fn compose_matrix() {
assert_eq!(compose_pattern(&one(r"a\.b"), false, false), r"(?m)a\.b");
assert_eq!(
compose_pattern(&one("foo"), true, false),
r"(?m)\b{start-half}(?:foo)\b{end-half}"
);
assert_eq!(
compose_pattern(&["a".into(), "b".into()], false, false),
"(?m)(?:a)|(?:b)"
);
assert_eq!(compose_pattern(&one("foo"), false, true), "(?mi)foo");
assert_eq!(
compose_pattern(&[r"a\.".into(), "b".into()], true, true),
r"(?mi)(?:\b{start-half}(?:a\.)\b{end-half})|(?:\b{start-half}(?:b)\b{end-half})"
);
}
#[test]
fn build_pattern_escapes_fixed_strings() {
let re = |p: &str, fixed: bool| {
regex::bytes::Regex::new(&build_pattern(&one(p), fixed, false, false, false).unwrap())
.unwrap()
};
assert!(re("a.b", true).is_match(b"a.b"));
assert!(!re("a.b", true).is_match(b"axb"));
assert!(re("a.b", false).is_match(b"axb"));
}
#[test]
fn sanitize_strips_newline_from_classes_and_rejects_literals() {
let re =
|p: &str| regex::bytes::Regex::new(&sanitize_line_terminators(p).unwrap()).unwrap();
assert!(!re("[^x]").is_match(b"\n"));
assert!(!re("(?s).").is_match(b"\n"));
assert!(re("(?s)a.b").is_match(b"axb"));
assert!(!re("(?s)a.b").is_match(b"a\nb"));
assert!(!re(r"\D").is_match(b"\n"));
assert!(sanitize_line_terminators("a\nb").is_err());
assert!(sanitize_line_terminators(r"a\nb").is_err());
assert!(sanitize_line_terminators(r"a\x0Ab").is_err());
assert!(re("(?m)^needle$").is_match(b"x\nneedle\ny"));
}
#[test]
fn smart_case_rules() {
assert!(smart_case_insensitive(&one("foo")));
assert!(!smart_case_insensitive(&one("Foo")));
assert!(smart_case_insensitive(&one(r"foo\pL")));
assert!(!smart_case_insensitive(&one(r"Foo\pL")));
assert!(!smart_case_insensitive(&one(r"\d+"))); assert!(!smart_case_insensitive(&one("(")));
assert!(build_pattern(&one("("), false, false, false, true).is_err());
}
#[test]
fn fixed_strings_smart_case_analyzes_escaped_pattern() {
let re = |p: &str| {
regex::bytes::Regex::new(&build_pattern(&one(p), true, false, false, true).unwrap())
.unwrap()
};
assert!(re("foo(").is_match(b"FOO("));
assert!(re("Foo(").is_match(b"Foo("));
assert!(!re("Foo(").is_match(b"FOO("));
}
}