use std::cell::RefCell;
use super::nfa::{Program, State, StateId};
use super::parser::AnchorKind;
pub fn is_match(prog: &Program, input: &str) -> bool {
SCRATCH.with(|cell| {
let mut scratch = cell.borrow_mut();
scratch.run(prog, input, Mode::WholeString)
})
}
pub fn find_match(prog: &Program, input: &str) -> bool {
SCRATCH.with(|cell| {
let mut scratch = cell.borrow_mut();
scratch.run(prog, input, Mode::Find)
})
}
pub fn leftmost_match_at_start(
prog: &Program,
slice: &str,
abs_char_pos: usize,
abs_total_chars: usize,
) -> Option<usize> {
SCRATCH.with(|cell| {
let mut scratch = cell.borrow_mut();
scratch.run_leftmost(prog, slice, abs_char_pos, abs_total_chars)
})
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum Mode {
WholeString,
Find,
}
thread_local! {
static SCRATCH: RefCell<Scratch> = const { RefCell::new(Scratch::new()) };
}
struct Scratch {
current: Vec<StateId>,
next: Vec<StateId>,
marks: Vec<u32>,
cur_mark: u32,
}
impl Scratch {
const fn new() -> Self {
Self {
current: Vec::new(),
next: Vec::new(),
marks: Vec::new(),
cur_mark: 0,
}
}
fn run(&mut self, prog: &Program, input: &str, mode: Mode) -> bool {
self.reset(prog.states.len());
let total = input.chars().count();
let mut pos: usize = 0;
let seed_mark = self.cur_mark;
add(&mut self.current, &mut self.marks, seed_mark,
prog, prog.start, pos, total);
if mode == Mode::Find && contains_match(prog, &self.current) {
return true;
}
for c in input.chars() {
self.cur_mark = self.cur_mark.wrapping_add(1);
let next_mark = self.cur_mark;
self.next.clear();
for i in 0..self.current.len() {
let sid = self.current[i];
if let State::Char { class, next } = prog.states[sid as usize] {
if prog.classes[class as usize].contains(c) {
add(&mut self.next, &mut self.marks, next_mark,
prog, next, pos + 1, total);
}
}
}
pos += 1;
std::mem::swap(&mut self.current, &mut self.next);
if mode == Mode::Find {
add(&mut self.current, &mut self.marks, next_mark,
prog, prog.start, pos, total);
}
if mode == Mode::Find && contains_match(prog, &self.current) {
return true;
}
if self.current.is_empty() {
if mode == Mode::Find { continue; }
return false;
}
}
contains_match(prog, &self.current)
}
fn run_leftmost(
&mut self, prog: &Program, slice: &str,
abs_start: usize, abs_total: usize,
) -> Option<usize> {
self.reset(prog.states.len());
let mut pos: usize = abs_start;
let mut byte_pos: usize = 0;
let mut matched: Option<usize> = None;
let seed_mark = self.cur_mark;
add(&mut self.current, &mut self.marks, seed_mark,
prog, prog.start, pos, abs_total);
let mut chars = slice.chars();
loop {
self.cur_mark = self.cur_mark.wrapping_add(1);
let next_mark = self.cur_mark;
self.next.clear();
let c = chars.next();
for i in 0..self.current.len() {
let sid = self.current[i];
match prog.states[sid as usize] {
State::Match => {
matched = Some(byte_pos);
break;
}
State::Char { class, next } => {
if let Some(ch) = c {
if prog.classes[class as usize].contains(ch) {
add(&mut self.next, &mut self.marks, next_mark,
prog, next, pos + 1, abs_total);
}
}
}
_ => {}
}
}
match c {
None => break, Some(ch) => {
pos += 1;
byte_pos += ch.len_utf8();
std::mem::swap(&mut self.current, &mut self.next);
if self.current.is_empty() { break; }
}
}
}
matched
}
fn reset(&mut self, state_count: usize) {
let need_resize = self.marks.len() != state_count;
let need_wrap_reset = self.cur_mark >= u32::MAX - 2;
if need_resize {
self.marks.clear();
self.marks.resize(state_count, 0);
self.cur_mark = 1;
} else if need_wrap_reset {
self.marks.iter_mut().for_each(|m| *m = 0);
self.cur_mark = 1;
} else {
self.cur_mark = self.cur_mark.wrapping_add(1);
}
self.current.clear();
self.next.clear();
}
}
fn contains_match(prog: &Program, list: &[StateId]) -> bool {
list.iter().any(|&s| matches!(prog.states[s as usize], State::Match))
}
fn add(
list: &mut Vec<StateId>,
marks: &mut [u32],
mark: u32,
prog: &Program,
sid: StateId,
pos: usize,
total: usize,
) {
if marks[sid as usize] == mark {
return;
}
marks[sid as usize] = mark;
match prog.states[sid as usize] {
State::Split(a, b) => {
add(list, marks, mark, prog, a, pos, total);
add(list, marks, mark, prog, b, pos, total);
}
State::Assert { kind, next } => {
let ok = match kind {
AnchorKind::Start => pos == 0,
AnchorKind::End => pos == total,
};
if ok {
add(list, marks, mark, prog, next, pos, total);
}
}
_ => list.push(sid),
}
}
#[cfg(test)]
mod tests {
use super::super::nfa::compile;
use super::super::parser::{parse, parse_with, Dialect};
fn matches(pat: &str, s: &str) -> bool {
let prog = compile(&parse(pat).unwrap()).unwrap();
super::is_match(&prog, s)
}
fn finds(pat: &str, s: &str) -> bool {
let prog = compile(&parse_with(pat, Dialect::Xpath).unwrap()).unwrap();
super::find_match(&prog, s)
}
#[test]
fn empty_pattern_matches_empty_string() {
assert!(matches("", ""));
assert!(!matches("", "x"));
}
#[test]
fn literal() {
assert!(matches("abc", "abc"));
assert!(!matches("abc", "ab"));
assert!(!matches("abc", "abcd"));
assert!(!matches("abc", "xbc"));
}
#[test]
fn alternation() {
assert!(matches("a|b|c", "a"));
assert!(matches("a|b|c", "b"));
assert!(matches("a|b|c", "c"));
assert!(!matches("a|b|c", "d"));
}
#[test]
fn star() {
assert!(matches("a*", ""));
assert!(matches("a*", "a"));
assert!(matches("a*", "aaaa"));
assert!(!matches("a*", "ab"));
}
#[test]
fn plus() {
assert!(!matches("a+", ""));
assert!(matches("a+", "a"));
assert!(matches("a+", "aaaa"));
}
#[test]
fn optional() {
assert!(matches("a?", ""));
assert!(matches("a?", "a"));
assert!(!matches("a?", "aa"));
}
#[test]
fn counted_exact() {
assert!(matches("a{3}", "aaa"));
assert!(!matches("a{3}", "aa"));
assert!(!matches("a{3}", "aaaa"));
}
#[test]
fn counted_range() {
assert!(!matches("a{2,4}", "a"));
assert!(matches("a{2,4}", "aa"));
assert!(matches("a{2,4}", "aaa"));
assert!(matches("a{2,4}", "aaaa"));
assert!(!matches("a{2,4}", "aaaaa"));
}
#[test]
fn counted_unbounded() {
assert!(matches("a{2,}", "aa"));
assert!(matches("a{2,}", "aaaaaa"));
assert!(!matches("a{2,}", "a"));
}
#[test]
fn class_subtraction_matches_correctly() {
assert!(matches("[a-z-[aeiou]]+", "bcdfg"));
assert!(!matches("[a-z-[aeiou]]+", "abcdf"));
}
#[test]
fn xsd_whitespace_is_not_unicode_whitespace() {
assert!(matches(r"\s+", " \t\n"));
assert!(!matches(r"\s+", "\u{A0}"));
}
#[test]
fn zip_code_pattern() {
assert!(matches(r"\d{5}(-\d{4})?", "12345"));
assert!(matches(r"\d{5}(-\d{4})?", "12345-6789"));
assert!(!matches(r"\d{5}(-\d{4})?", "1234"));
assert!(!matches(r"\d{5}(-\d{4})?", "12345-678"));
}
#[test]
fn dot_excludes_newline() {
assert!(matches(".+", "abc"));
assert!(!matches(".+", "a\nb"));
}
#[test]
fn linear_time_on_adversarial_pattern() {
let pat = "(a|a)*b";
let prog = compile(&parse(pat).unwrap()).unwrap();
let s: String = "a".repeat(30);
let t0 = std::time::Instant::now();
let m = super::is_match(&prog, &s);
assert!(!m);
assert!(t0.elapsed().as_millis() < 50, "took {:?}", t0.elapsed());
}
#[test]
fn unicode_property_letter() {
assert!(matches(r"\p{L}+", "héllo"));
assert!(matches(r"\p{L}+", "中文"));
assert!(!matches(r"\p{L}+", "h3llo"));
}
#[test]
fn block_basic_latin() {
assert!(matches(r"\p{IsBasicLatin}+", "hello"));
assert!(!matches(r"\p{IsBasicLatin}+", "héllo"));
}
#[test]
fn xpath_find_substring() {
assert!(finds("bar", "foo bar baz"));
assert!(finds(r"\d+", "abc 123 def"));
assert!(!finds(r"\d+", "abcdef"));
}
#[test]
fn xpath_start_anchor() {
assert!(finds("^foo", "foo bar"));
assert!(!finds("^foo", "bar foo"));
}
#[test]
fn xpath_end_anchor() {
assert!(finds("bar$", "foo bar"));
assert!(!finds("bar$", "bar foo"));
}
#[test]
fn xpath_both_anchors_force_whole_string() {
assert!(finds("^foo$", "foo"));
assert!(!finds("^foo$", "foo bar"));
assert!(!finds("^foo$", "x foo"));
}
#[test]
fn xpath_empty_pattern_finds_everywhere() {
assert!(finds("", ""));
assert!(finds("", "anything"));
}
#[test]
fn xpath_anchored_empty_against_nonempty() {
assert!(finds("^$", ""));
assert!(!finds("^$", "x"));
}
fn lm(pat: &str, s: &str) -> Option<usize> {
let prog = compile(&parse_with(pat, Dialect::Xpath).unwrap()).unwrap();
let total = s.chars().count();
super::leftmost_match_at_start(&prog, s, 0, total)
}
#[test]
fn leftmost_first_alternation_prefers_earlier_branch() {
assert_eq!(lm("a|ana", "anana"), Some(1));
assert_eq!(lm("ana|a", "anana"), Some(3)); assert_eq!(lm("a|ab", "ab"), Some(1));
}
#[test]
fn leftmost_first_greedy_quantifier_takes_longest_run() {
assert_eq!(lm("a*", "aaa"), Some(3));
assert_eq!(lm("a*", "aaab"), Some(3));
assert_eq!(lm("a*", "b"), Some(0)); assert_eq!(lm("(an)*a", "anana"), Some(5));
}
#[test]
fn xpath_literal_caret_dollar_in_xsd_mode() {
assert!(matches("^abc$", "^abc$"));
assert!(!matches("^abc$", "abc"));
}
}