const MAX_PATTERN_LEN: usize = 4096;
const MAX_PARSE_DEPTH: usize = 128;
const MAX_REPEAT: u32 = 1024;
const MAX_PROG: usize = 200_000;
use std::sync::Arc;
#[derive(Debug, Clone)]
enum Node {
Char(char),
Any,
Class {
neg: bool,
ranges: Arc<[(char, char)]>,
},
Start,
End,
Seq(Vec<Node>),
Alt(Vec<Node>),
Rep {
node: Box<Node>,
min: u32,
max: Option<u32>,
},
}
#[derive(Debug, Clone)]
enum Inst {
Char(char),
Any,
Class {
neg: bool,
ranges: Arc<[(char, char)]>,
},
Start,
End,
Split(usize, usize),
Jmp(usize),
Match,
}
#[derive(Debug, Clone)]
pub struct Regex {
prog: Vec<Inst>,
}
impl Regex {
pub fn new(pattern: &str) -> Option<Regex> {
if pattern.len() > MAX_PATTERN_LEN {
return None;
}
let cs: Vec<char> = pattern.chars().collect();
let mut i = 0;
let ast = parse_alt(&cs, &mut i, 0)?;
if i != cs.len() {
return None; }
let mut prog = Vec::new();
emit(&ast, &mut prog)?;
prog.push(Inst::Match);
Some(Regex { prog })
}
pub fn is_match(&self, text: &str) -> bool {
let s: Vec<char> = text.chars().collect();
let prog = &self.prog;
let len = s.len();
let n = prog.len();
let mut seen = vec![0u32; n];
let mut gen = 0u32;
let mut clist: Vec<usize> = Vec::new();
let mut nlist: Vec<usize> = Vec::new();
let mut stack: Vec<usize> = Vec::new();
gen += 1;
add_thread(&mut clist, &mut stack, &mut seen, gen, prog, 0, 0, len);
#[allow(clippy::needless_range_loop)]
for pos in 0..=len {
if clist.iter().any(|&pc| matches!(prog[pc], Inst::Match)) {
return true;
}
if pos == len {
break;
}
let c = s[pos];
nlist.clear();
gen += 1;
for &pc in &clist {
let hit = match &prog[pc] {
Inst::Char(ch) => *ch == c,
Inst::Any => true,
Inst::Class { neg, ranges } => class_match(*neg, ranges, c),
_ => false,
};
if hit {
add_thread(&mut nlist, &mut stack, &mut seen, gen, prog, pc + 1, pos + 1, len);
}
}
add_thread(&mut nlist, &mut stack, &mut seen, gen, prog, 0, pos + 1, len);
std::mem::swap(&mut clist, &mut nlist);
}
false
}
}
#[allow(clippy::too_many_arguments)]
fn add_thread(
list: &mut Vec<usize>,
stack: &mut Vec<usize>,
seen: &mut [u32],
gen: u32,
prog: &[Inst],
pc: usize,
pos: usize,
len: usize,
) {
stack.clear();
stack.push(pc);
while let Some(pc) = stack.pop() {
if seen[pc] == gen {
continue;
}
seen[pc] = gen;
match &prog[pc] {
Inst::Jmp(x) => stack.push(*x),
Inst::Split(a, b) => {
stack.push(*b);
stack.push(*a);
}
Inst::Start => {
if pos == 0 {
stack.push(pc + 1);
}
}
Inst::End => {
if pos == len {
stack.push(pc + 1);
}
}
_ => list.push(pc),
}
}
}
fn parse_alt(cs: &[char], i: &mut usize, depth: usize) -> Option<Node> {
if depth > MAX_PARSE_DEPTH {
return None;
}
let mut branches = vec![parse_seq(cs, i, depth)?];
while cs.get(*i) == Some(&'|') {
*i += 1;
branches.push(parse_seq(cs, i, depth)?);
}
Some(if branches.len() == 1 {
branches.pop().unwrap()
} else {
Node::Alt(branches)
})
}
fn parse_seq(cs: &[char], i: &mut usize, depth: usize) -> Option<Node> {
let mut nodes = Vec::new();
while *i < cs.len() && cs[*i] != '|' && cs[*i] != ')' {
let atom = parse_atom(cs, i, depth)?;
nodes.push(parse_postfix(cs, i, atom)?);
}
Some(Node::Seq(nodes))
}
fn parse_postfix(cs: &[char], i: &mut usize, atom: Node) -> Option<Node> {
let (min, max) = match cs.get(*i) {
Some('*') => (0, None),
Some('+') => (1, None),
Some('?') => (0, Some(1)),
Some('{') => {
let close = cs[*i..].iter().position(|&c| c == '}')? + *i;
let body: String = cs[*i + 1..close].iter().collect();
*i = close; let (lo, hi) = if let Some((a, b)) = body.split_once(',') {
let lo: u32 = a.parse().ok()?;
let hi = if b.is_empty() {
None
} else {
Some(b.parse().ok()?)
};
(lo, hi)
} else {
let n: u32 = body.parse().ok()?;
(n, Some(n))
};
if let Some(hi) = hi {
if lo > hi {
return None;
}
}
*i += 1;
return Some(Node::Rep {
node: Box::new(atom),
min: lo,
max: hi,
});
}
_ => return Some(atom),
};
*i += 1;
Some(Node::Rep {
node: Box::new(atom),
min,
max,
})
}
fn parse_atom(cs: &[char], i: &mut usize, depth: usize) -> Option<Node> {
let c = *cs.get(*i)?;
*i += 1;
match c {
'(' => {
let inner = parse_alt(cs, i, depth + 1)?;
if cs.get(*i) != Some(&')') {
return None;
}
*i += 1;
Some(inner)
}
'[' => parse_class(cs, i),
'.' => Some(Node::Any),
'^' => Some(Node::Start),
'$' => Some(Node::End),
'\\' => {
let e = *cs.get(*i)?;
*i += 1;
match e {
'd' => Some(Node::Class {
neg: false,
ranges: Arc::from(vec![('0', '9')]),
}),
e if e.is_ascii_alphanumeric() => None,
_ => Some(Node::Char(e)), }
}
'*' | '+' | '?' | '{' | ')' => None, _ => Some(Node::Char(c)),
}
}
fn parse_class(cs: &[char], i: &mut usize) -> Option<Node> {
let neg = cs.get(*i) == Some(&'^');
if neg {
*i += 1;
}
let mut ranges = Vec::new();
let mut first = true;
loop {
let c = *cs.get(*i)?;
if c == ']' && !first {
*i += 1;
return Some(Node::Class {
neg,
ranges: ranges.into(),
});
}
first = false;
let lo = if c == '\\' {
*i += 1;
let e = *cs.get(*i)?;
*i += 1;
if e == 'd' {
ranges.push(('0', '9'));
continue;
}
if e.is_ascii_alphanumeric() {
return None;
}
e
} else {
*i += 1;
c
};
if cs.get(*i) == Some(&'-') && cs.get(*i + 1).is_some_and(|&n| n != ']') {
*i += 1;
let mut hi = *cs.get(*i)?;
if hi == '\\' {
*i += 1;
hi = *cs.get(*i)?;
if hi.is_ascii_alphanumeric() {
return None;
}
}
*i += 1;
ranges.push((lo, hi));
} else {
ranges.push((lo, lo));
}
}
}
fn class_match(neg: bool, ranges: &[(char, char)], c: char) -> bool {
let hit = ranges.iter().any(|&(lo, hi)| lo <= c && c <= hi);
hit != neg
}
fn emit(node: &Node, prog: &mut Vec<Inst>) -> Option<()> {
if prog.len() > MAX_PROG {
return None;
}
match node {
Node::Char(c) => prog.push(Inst::Char(*c)),
Node::Any => prog.push(Inst::Any),
Node::Class { neg, ranges } => prog.push(Inst::Class {
neg: *neg,
ranges: ranges.clone(),
}),
Node::Start => prog.push(Inst::Start),
Node::End => prog.push(Inst::End),
Node::Seq(v) => {
for n in v {
emit(n, prog)?;
}
}
Node::Alt(v) => emit_alt(v, prog)?,
Node::Rep { node, min, max } => emit_rep(node, *min, *max, prog)?,
}
Some(())
}
fn emit_alt(branches: &[Node], prog: &mut Vec<Inst>) -> Option<()> {
let mut jmps = Vec::new();
let last = branches.len() - 1;
for (i, b) in branches.iter().enumerate() {
if i < last {
let sp = prog.len();
prog.push(Inst::Split(0, 0));
emit(b, prog)?;
jmps.push(prog.len());
prog.push(Inst::Jmp(0));
prog[sp] = Inst::Split(sp + 1, prog.len());
} else {
emit(b, prog)?;
}
}
let end = prog.len();
for j in jmps {
prog[j] = Inst::Jmp(end);
}
Some(())
}
fn emit_rep(node: &Node, min: u32, max: Option<u32>, prog: &mut Vec<Inst>) -> Option<()> {
match max {
None => {
if min == 0 {
let l1 = prog.len();
let sp = prog.len();
prog.push(Inst::Split(0, 0));
let l2 = prog.len();
emit(node, prog)?;
prog.push(Inst::Jmp(l1));
let l3 = prog.len();
prog[sp] = Inst::Split(l2, l3);
} else {
if min > MAX_REPEAT {
return None;
}
for _ in 0..min {
emit(node, prog)?;
}
emit_rep(node, 0, None, prog)?;
}
}
Some(mx) => {
if min > mx || mx > MAX_REPEAT {
return None;
}
for _ in 0..min {
emit(node, prog)?;
}
let mut splits = Vec::new();
for _ in 0..(mx - min) {
let sp = prog.len();
prog.push(Inst::Split(0, 0));
splits.push(sp);
emit(node, prog)?;
}
let end = prog.len();
for sp in splits {
prog[sp] = Inst::Split(sp + 1, end);
}
}
}
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escaped_alphanumerics_are_outside_the_dialect() {
for p in [r"^(a)\1$", r"\w+", r"a\s", r"\bword"] {
assert!(Regex::new(p).is_none(), "{p} must be rejected");
}
for p in [r"[\w]+", r"[\s]", r"[a-\d]", r"[0-\d]"] {
assert!(Regex::new(p).is_none(), "{p} must be rejected");
}
assert!(Regex::new(r"a\.b\/c\\d").is_some());
assert!(Regex::new(r"\d{2,4}").is_some());
assert!(Regex::new(r"[\d]").is_some());
assert!(Regex::new(r"[\.\-]").is_some());
assert!(Regex::new(r"[a-z\/]").is_some());
}
#[test]
fn transposed_and_pathological_quantifiers_rejected() {
assert!(Regex::new("a{3,1}").is_none());
assert!(Regex::new(r"^\d{4,2}$").is_none());
assert!(Regex::new("a{2,4}").is_some());
assert!(Regex::new("a{3}").is_some());
assert!(Regex::new("a{2,}").is_some());
assert!(Regex::new("a{3,3}").is_some());
}
#[test]
fn deeply_nested_or_huge_patterns_reject_without_overflow() {
assert!(Regex::new(&"(".repeat(100_000)).is_none());
assert!(Regex::new(&"(a)".repeat(60)).is_some());
}
#[test]
fn fat_class_under_repetition_compiles_cheaply() {
let p = format!("([{}]{{1024}}){{100}}", "a".repeat(1000));
assert!(Regex::new(&p).is_some());
}
#[test]
fn flat_alternation_with_thousands_of_branches() {
let p = vec!["a"; 2000].join("|");
let re = Regex::new(&p).unwrap();
assert!(re.is_match("a"));
assert!(!re.is_match("b"));
}
#[test]
fn long_inputs_match_without_overflow_or_blowup() {
assert!(Regex::new("^[0-9]+$").unwrap().is_match(&"9".repeat(500_000)));
assert!(Regex::new("^[A-Za-z0-9+/]*={0,2}$")
.unwrap()
.is_match(&"a".repeat(200)));
let mut bad = "9".repeat(500_000);
bad.push('a');
assert!(!Regex::new("^[0-9]+$").unwrap().is_match(&bad));
}
#[test]
fn no_catastrophic_backtracking() {
let re = Regex::new("^(a+)+$").unwrap();
let t = format!("{}X", "a".repeat(40));
assert!(!re.is_match(&t));
assert!(re.is_match("aaaa"));
}
fn ok(p: &str, t: &str) -> bool {
Regex::new(p).unwrap().is_match(t)
}
#[test]
fn core_patterns() {
assert!(ok(r"^-?[0-9]+$", "8080"));
assert!(ok(r"^-?[0-9]+$", "-3"));
assert!(!ok(r"^-?[0-9]+$", "abc"));
assert!(!ok(r"^-?[0-9]+$", "12a"));
assert!(ok(r"^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$", "0.5"));
assert!(ok(r"^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$", "1e-9"));
assert!(!ok(r"^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$", "x"));
assert!(ok(r"^$", ""));
assert!(!ok(r"^$", "a"));
assert!(ok(r"^[A-Za-z0-9+\/]*={0,2}$", "aGk="));
assert!(ok(r"^(\d{1,3}\.){3}\d{1,3}$", "192.168.1.1"));
assert!(!ok(r"^(\d{1,3}\.){3}\d{1,3}$", "192.168.1"));
assert!(ok(r"[a-zA-Z0-9.-]+", "example.com"));
}
}