use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::convert::TryFrom;
use regex_syntax::hir::{Capture, Dot, Hir, HirKind, Look, Repetition};
use bit_set::BitSet;
use crate::{push_quoted, Assertion, BytesMode, Expr, RegexOptions};
pub(crate) struct HirCtx {
unicode: bool,
utf8: bool,
next_group: u32,
fragments: BTreeMap<String, Option<Hir>>,
preserve_zero_rep_captures: bool,
zero_rep_groups: BitSet,
}
impl HirCtx {
pub(crate) fn new(unicode: bool, utf8: bool) -> Self {
HirCtx {
unicode,
utf8,
next_group: 1,
fragments: BTreeMap::new(),
preserve_zero_rep_captures: false,
zero_rep_groups: BitSet::new(),
}
}
pub(crate) fn utf8(&self) -> bool {
self.utf8
}
pub(crate) fn enable_preserve_zero_rep_captures(&mut self) {
self.preserve_zero_rep_captures = true;
}
pub(crate) fn into_zero_rep_groups(self) -> BitSet {
self.zero_rep_groups
}
}
impl From<&RegexOptions> for HirCtx {
fn from(options: &RegexOptions) -> Self {
let unicode =
options.syntaxc.get_unicode() && !matches!(options.bytes_mode, BytesMode::Ascii);
let utf8 = matches!(options.bytes_mode, BytesMode::Unicode);
Self::new(unicode, utf8)
}
}
pub(crate) fn expr_to_hir(expr: &Expr, ctx: &mut HirCtx) -> Option<Hir> {
Some(match *expr {
Expr::Empty | Expr::DefineGroup { .. } => Hir::empty(),
Expr::Any { newline, crlf } => {
let dot = match (newline, crlf, ctx.unicode, ctx.utf8) {
(true, _, true, _) => Dot::AnyChar,
(true, _, false, false) => Dot::AnyByte,
(false, false, true, _) => Dot::AnyCharExceptLF,
(false, false, false, false) => Dot::AnyByteExceptLF,
(false, true, true, _) => Dot::AnyCharExceptCRLF,
(false, true, false, false) => Dot::AnyByteExceptCRLF,
(_, _, false, true) => return None,
};
Hir::dot(dot)
}
Expr::Literal { ref val, casei } => {
if !casei {
Hir::literal(val.as_bytes())
} else {
let mut cooked = String::with_capacity(val.len() + 5);
cooked.push_str("(?i:");
push_quoted(&mut cooked, val);
cooked.push(')');
parse_fragment(&cooked, ctx)?
}
}
Expr::LiteralBytes { ref bytes, .. } => {
if ctx.utf8 {
let s: String = bytes
.iter()
.map(|&b| char::from_u32(b as u32).unwrap())
.collect();
Hir::literal(s.as_bytes())
} else {
Hir::literal(bytes.as_slice())
}
}
Expr::Assertion(assertion) => Hir::look(match assertion {
Assertion::StartText => Look::Start,
Assertion::EndText => Look::End,
Assertion::StartLine { crlf: false }
| Assertion::StartLineOniguruma { crlf: false } => Look::StartLF,
Assertion::EndLine { crlf: false } => Look::EndLF,
Assertion::StartLine { crlf: true } | Assertion::StartLineOniguruma { crlf: true } => {
Look::StartCRLF
}
Assertion::EndLine { crlf: true } => Look::EndCRLF,
Assertion::WordBoundary => {
if ctx.unicode {
Look::WordUnicode
} else {
Look::WordAscii
}
}
Assertion::NotWordBoundary => {
if ctx.unicode {
Look::WordUnicodeNegate
} else {
Look::WordAsciiNegate
}
}
Assertion::LeftWordBoundary => {
if ctx.unicode {
Look::WordStartUnicode
} else {
Look::WordStartAscii
}
}
Assertion::RightWordBoundary => {
if ctx.unicode {
Look::WordEndUnicode
} else {
Look::WordEndAscii
}
}
Assertion::LeftWordHalfBoundary => {
if ctx.unicode {
Look::WordStartHalfUnicode
} else {
Look::WordStartHalfAscii
}
}
Assertion::RightWordHalfBoundary => {
if ctx.unicode {
Look::WordEndHalfUnicode
} else {
Look::WordEndHalfAscii
}
}
_ => return None,
}),
Expr::Concat(ref children) => {
let subs = children
.iter()
.map(|child| expr_to_hir(child, ctx))
.collect::<Option<Vec<_>>>()?;
Hir::concat(subs)
}
Expr::Alt(ref children) => {
let subs = children
.iter()
.map(|child| expr_to_hir(child, ctx))
.collect::<Option<Vec<_>>>()?;
Hir::alternation(subs)
}
Expr::Group(ref child) => {
let index = ctx.next_group;
ctx.next_group += 1;
let sub = expr_to_hir(child, ctx)?;
Hir::capture(Capture {
index,
name: None,
sub: Box::new(sub),
})
}
Expr::Repeat {
ref child,
lo,
hi,
greedy,
} => {
if lo == 0 && hi == 0 && ctx.preserve_zero_rep_captures {
let sub = expr_to_hir(child, ctx)?;
return Some(make_zero_rep_empty_with_captures(
sub,
&mut ctx.zero_rep_groups,
));
}
let min = u32::try_from(lo).ok()?;
let max = if hi == usize::MAX {
None
} else {
Some(u32::try_from(hi).ok()?)
};
let sub = expr_to_hir(child, ctx)?;
Hir::repetition(Repetition {
min,
max,
greedy,
sub: Box::new(sub),
})
}
Expr::Delegate { ref inner, casei } => {
if casei {
let mut cooked = String::with_capacity(inner.len() + 5);
cooked.push_str("(?i:");
cooked.push_str(inner);
cooked.push(')');
parse_fragment(&cooked, ctx)?
} else {
parse_fragment(inner, ctx)?
}
}
_ => return None,
})
}
fn make_zero_rep_empty_with_captures(hir: Hir, zero_rep_groups: &mut BitSet) -> Hir {
match hir.into_kind() {
HirKind::Capture(capture) => {
zero_rep_groups.insert(capture.index as usize);
let sub = make_zero_rep_empty_with_captures(*capture.sub, zero_rep_groups);
Hir::capture(Capture {
index: capture.index,
name: capture.name,
sub: Box::new(sub),
})
}
HirKind::Concat(subs) => {
let new_subs: Vec<Hir> = subs
.into_iter()
.map(|s| make_zero_rep_empty_with_captures(s, zero_rep_groups))
.collect();
Hir::concat(new_subs)
}
HirKind::Alternation(subs) => {
let new_subs: Vec<Hir> = subs
.into_iter()
.map(|s| make_zero_rep_empty_with_captures(s, zero_rep_groups))
.collect();
Hir::alternation(new_subs)
}
HirKind::Repetition(rep) => {
let sub = make_zero_rep_empty_with_captures(*rep.sub, zero_rep_groups);
Hir::repetition(Repetition {
min: rep.min,
max: rep.max,
greedy: rep.greedy,
sub: Box::new(sub),
})
}
_ => Hir::empty(),
}
}
pub(crate) fn parse_fragment(fragment: &str, ctx: &mut HirCtx) -> Option<Hir> {
let memoize = fragment.starts_with("(?i:");
if memoize {
if let Some(hir) = ctx.fragments.get(fragment) {
return hir.clone();
}
}
let hir = regex_syntax::ParserBuilder::new()
.utf8(ctx.utf8)
.unicode(ctx.unicode)
.build()
.parse(fragment)
.ok();
if memoize {
ctx.fragments.insert(fragment.into(), hir.clone());
}
hir
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyze::{analyze, AnalyzeContext};
use crate::{BytesMode, RegexOptions};
use alloc::string::ToString;
const PATTERNS: &[&str] = &[
"a",
"abc",
"a.c",
".",
"(?s).",
"(?s:.)x",
"^abc$",
"(?m)^abc$",
"(?m:^)foo",
"(?Rm)^x$",
"(?i)abc",
"(?i)aBc(?-i)d",
"(?i)δΔ",
"αβγ",
"a+b*c?",
"a{2,5}",
"a{3}",
"a{2,}?",
"a+?b??",
"(a)(b(c))",
"(?:ab|cd)e",
"a|b|",
"(a|)",
"()",
"[a-z]+",
r"\d\w\s",
"[^a-c]{2,3}",
r"\p{L}+",
"(?i)[a-k]x",
r"\x61\n\t",
"(a+)(?:b|c)*",
r"\.\*\+#",
r"(?i)ſ",
r"\bword\b",
r"(a)\1",
r"\b(?:at)\b",
r"\B(?:foo)\B",
r"\b(?:foo|bar)\b",
];
fn check_oracle(bytes_mode: BytesMode) {
for pattern in PATTERNS {
let options = RegexOptions {
bytes_mode,
..RegexOptions::default()
};
let Ok(tree) = crate::Expr::parse_tree_with_flags(pattern, options.compute_flags())
else {
continue;
};
let Ok(info) = analyze(&tree, AnalyzeContext::default()) else {
continue;
};
if info.hard {
continue;
}
let mut cooked = String::new();
tree.expr.to_str(&mut cooked, 0);
let unicode =
options.syntaxc.get_unicode() && !matches!(options.bytes_mode, BytesMode::Ascii);
let utf8 = matches!(options.bytes_mode, BytesMode::Unicode);
let expected = regex_syntax::ParserBuilder::new()
.utf8(utf8)
.unicode(unicode)
.build()
.parse(&cooked);
let got = expr_to_hir(&tree.expr, &mut HirCtx::new(unicode, utf8));
match expected {
Ok(hir) => assert_eq!(
Some(hir),
got,
"Hir mismatch for {:?} (cooked {:?}) in {:?} mode",
pattern,
cooked,
bytes_mode
),
Err(_) => assert_eq!(
None, got,
"expected fallback for {:?} (cooked {:?}) in {:?} mode: the parser \
rejects it, so the translator must not accept it",
pattern, cooked, bytes_mode
),
}
}
}
#[test]
fn oracle_unicode_mode() {
check_oracle(BytesMode::Unicode);
}
#[test]
fn oracle_unicode_bytes_mode() {
check_oracle(BytesMode::UnicodeBytes);
}
#[test]
fn oracle_ascii_mode() {
check_oracle(BytesMode::Ascii);
}
#[test]
fn group_numbering_matches_textual_order() {
let tree = crate::Expr::parse_tree("(a)((b)(c))").unwrap();
let hir = expr_to_hir(&tree.expr, &mut HirCtx::new(true, true)).unwrap();
let cooked = {
let mut s = String::new();
tree.expr.to_str(&mut s, 0);
s
};
let expected = regex_syntax::ParserBuilder::new().build().parse(&cooked);
assert_eq!(expected.unwrap().to_string(), hir.to_string());
}
}