pub(crate) fn compose_pattern(
patterns: &[String],
fixed_strings: bool,
word_regexp: bool,
insensitive: bool,
) -> String {
let transformed: Vec<String> = patterns
.iter()
.map(|p| {
let p = if fixed_strings {
regex::escape(p)
} else {
p.clone()
};
if word_regexp {
format!(r"\b{{start-half}}(?:{p})\b{{end-half}}")
} else {
p
}
})
.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}")
}
}
pub(crate) 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())
}
pub(crate) fn is_insensitive(ignore_case: bool, smart_case: bool, patterns: &[String]) -> bool {
if ignore_case {
return true;
}
if smart_case {
return smart_case_insensitive(patterns);
}
false
}
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("a.b"), true, false, false),
r"(?m)a\.b"
);
assert_eq!(
compose_pattern(&one("foo"), false, true, false),
r"(?m)\b{start-half}(?:foo)\b{end-half}"
);
assert_eq!(
compose_pattern(&["a".into(), "b".into()], false, false, false),
"(?m)(?:a)|(?:b)"
);
assert_eq!(compose_pattern(&one("foo"), false, false, true), "(?mi)foo");
assert_eq!(
compose_pattern(&["a.".into(), "b".into()], true, true, true),
r"(?mi)(?:\b{start-half}(?:a\.)\b{end-half})|(?:\b{start-half}(?:b)\b{end-half})"
);
}
#[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!(is_insensitive(false, true, &one("foo")));
assert!(!is_insensitive(false, true, &one("Foo")));
assert!(is_insensitive(false, true, &one(r"foo\pL")));
assert!(!is_insensitive(false, true, &one(r"Foo\pL")));
assert!(!is_insensitive(false, true, &one(r"\d+"))); assert!(!is_insensitive(false, true, &one("(")));
assert!(is_insensitive(true, false, &one("FOO")));
assert!(!is_insensitive(false, false, &one("foo")));
}
}