use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use spg_storage::Value;
use super::{EvalError, text_arg};
const PARSE_DEPTH_LIMIT: u32 = 100;
const REPEAT_MAX: u32 = 0x0000_FFFF;
const MATCH_DEPTH_LIMIT: u32 = 500;
const MATCH_STEP_LIMIT: u64 = 10_000_000;
const MATCHALL_SAFE_LEN: u64 = 2_000_000;
#[derive(Debug, Clone)]
enum ReNode {
Literal(char),
AnyChar,
Class {
members: Vec<ClassMember>,
negated: bool,
},
Start,
End,
WordBoundary(WordBoundaryKind),
Quant {
inner: Box<ReNode>,
min: usize,
max: Option<usize>,
greedy: bool,
},
Concat(Vec<ReNode>),
Alt(Vec<ReNode>),
Lookahead { negative: bool, inner: Box<ReNode> },
Group { idx: usize, inner: Box<ReNode> },
Backref { idx: usize, ci: bool },
}
#[derive(Debug, Clone)]
enum ClassMember {
Single(char),
Range(char, char),
NotInSet(Vec<ClassMember>),
}
#[derive(Debug, Clone, Copy)]
enum WordBoundaryKind {
Boundary,
NonBoundary,
BegWord,
EndWord,
}
fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn re_compile(pat: &str) -> Result<ReNode, EvalError> {
let all: Vec<char> = pat.chars().collect();
let mut fold = false;
let mut extended = false;
let mut start = 0;
if all.len() >= 3 && all[0] == '(' && all[1] == '?' {
if let Some(close) = all[2..].iter().position(|&c| c == ')') {
let flags = &all[2..2 + close];
if !flags.is_empty() && flags.iter().all(|c| "bceimnpqstwx".contains(*c)) {
fold = flags.contains(&'i');
extended = flags.contains(&'x');
start = 2 + close + 1;
}
}
}
let body: Vec<char> = if extended {
strip_regex_extended_whitespace(&all[start..])
} else {
all[start..].to_vec()
};
let mut p = 0;
let mut ng = 1usize;
let mut n = re_parse_alt(&body, &mut p, 0, &mut ng)?;
if p != body.len() {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regex compile: trailing chars at pos {p} in {pat:?}"),
});
}
if fold {
fold_case(&mut n);
}
Ok(n)
}
fn strip_regex_extended_whitespace(chars: &[char]) -> Vec<char> {
let mut out = Vec::with_capacity(chars.len());
let mut in_class = false;
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '\\' && i + 1 < chars.len() {
out.push(c);
out.push(chars[i + 1]);
i += 2;
continue;
}
if in_class {
out.push(c);
if c == ']' {
in_class = false;
}
i += 1;
continue;
}
match c {
'[' => {
in_class = true;
out.push(c);
}
' ' | '\t' | '\n' | '\r' | '\x0c' => {} '#' => {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
continue;
}
_ => out.push(c),
}
i += 1;
}
out
}
fn re_parse_alt(
chars: &[char],
p: &mut usize,
depth: u32,
ng: &mut usize,
) -> Result<ReNode, EvalError> {
if depth > PARSE_DEPTH_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
let mut branches = alloc::vec![re_parse_concat(chars, p, depth, ng)?];
while *p < chars.len() && chars[*p] == '|' {
*p += 1;
branches.push(re_parse_concat(chars, p, depth, ng)?);
}
if branches.len() == 1 {
Ok(branches.pop().unwrap())
} else {
Ok(ReNode::Alt(branches))
}
}
fn re_parse_concat(
chars: &[char],
p: &mut usize,
depth: u32,
ng: &mut usize,
) -> Result<ReNode, EvalError> {
let mut items: Vec<ReNode> = Vec::new();
while *p < chars.len() {
let c = chars[*p];
if c == '|' || c == ')' {
break;
}
let atom = re_parse_atom(chars, p, depth, ng)?;
let quantified = if *p < chars.len() {
match chars[*p] {
'*' => {
*p += 1;
let greedy = !consume_lazy_suffix(chars, p);
ReNode::Quant {
inner: Box::new(atom),
min: 0,
max: None,
greedy,
}
}
'+' => {
*p += 1;
let greedy = !consume_lazy_suffix(chars, p);
ReNode::Quant {
inner: Box::new(atom),
min: 1,
max: None,
greedy,
}
}
'?' => {
*p += 1;
let greedy = !consume_lazy_suffix(chars, p);
ReNode::Quant {
inner: Box::new(atom),
min: 0,
max: Some(1),
greedy,
}
}
'{' => {
match re_parse_bound(chars, p)? {
Some((min, max)) => {
let greedy = !consume_lazy_suffix(chars, p);
ReNode::Quant {
inner: Box::new(atom),
min,
max,
greedy,
}
}
None => atom,
}
}
_ => atom,
}
} else {
atom
};
items.push(quantified);
}
if items.len() == 1 {
Ok(items.pop().unwrap())
} else {
Ok(ReNode::Concat(items))
}
}
fn re_parse_atom(
chars: &[char],
p: &mut usize,
depth: u32,
ng: &mut usize,
) -> Result<ReNode, EvalError> {
let c = chars[*p];
match c {
'(' => {
*p += 1;
let mut lookahead: Option<bool> = None;
let mut capturing = true;
if *p + 1 < chars.len() && chars[*p] == '?' {
match chars[*p + 1] {
':' => {
capturing = false;
*p += 2;
}
'=' => {
lookahead = Some(false);
capturing = false;
*p += 2;
}
'!' => {
lookahead = Some(true);
capturing = false;
*p += 2;
}
c => {
let msg = if c.is_ascii_alphabetic() {
"invalid regular expression: invalid embedded option"
} else {
"invalid regular expression: quantifier operand invalid"
};
return Err(EvalError::TypeMismatch { detail: msg.into() });
}
}
}
let group_idx = if capturing {
let idx = *ng;
*ng += 1;
Some(idx)
} else {
None
};
let inner = re_parse_alt(chars, p, depth + 1, ng)?;
if *p >= chars.len() || chars[*p] != ')' {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: parentheses () not balanced".into(),
});
}
*p += 1;
match lookahead {
Some(negative) => Ok(ReNode::Lookahead {
negative,
inner: Box::new(inner),
}),
None => match group_idx {
Some(idx) => Ok(ReNode::Group {
idx,
inner: Box::new(inner),
}),
None => Ok(inner),
},
}
}
'[' => re_parse_class(chars, p),
'.' => {
*p += 1;
Ok(ReNode::AnyChar)
}
'^' => {
*p += 1;
Ok(ReNode::Start)
}
'$' => {
*p += 1;
Ok(ReNode::End)
}
'\\' => {
*p += 1;
if *p >= chars.len() {
return Err(EvalError::TypeMismatch {
detail: "regex compile: dangling backslash".into(),
});
}
let esc = chars[*p];
*p += 1;
match esc {
'd' => Ok(ReNode::Class {
members: alloc::vec![ClassMember::Range('0', '9')],
negated: false,
}),
'D' => Ok(ReNode::Class {
members: alloc::vec![ClassMember::Range('0', '9')],
negated: true,
}),
'w' => Ok(ReNode::Class {
members: alloc::vec![
ClassMember::Range('a', 'z'),
ClassMember::Range('A', 'Z'),
ClassMember::Range('0', '9'),
ClassMember::Single('_'),
],
negated: false,
}),
'W' => Ok(ReNode::Class {
members: alloc::vec![
ClassMember::Range('a', 'z'),
ClassMember::Range('A', 'Z'),
ClassMember::Range('0', '9'),
ClassMember::Single('_'),
],
negated: true,
}),
's' => Ok(ReNode::Class {
members: shortcut_members('s'),
negated: false,
}),
'S' => Ok(ReNode::Class {
members: shortcut_members('s'),
negated: true,
}),
'y' => Ok(ReNode::WordBoundary(WordBoundaryKind::Boundary)),
'Y' => Ok(ReNode::WordBoundary(WordBoundaryKind::NonBoundary)),
'm' => Ok(ReNode::WordBoundary(WordBoundaryKind::BegWord)),
'M' => Ok(ReNode::WordBoundary(WordBoundaryKind::EndWord)),
'A' => Ok(ReNode::Start),
'Z' => Ok(ReNode::End),
'a' => Ok(ReNode::Literal('\u{07}')), 'e' => Ok(ReNode::Literal('\u{1b}')), 'f' => Ok(ReNode::Literal('\u{0c}')), 'n' => Ok(ReNode::Literal('\n')),
'r' => Ok(ReNode::Literal('\r')),
't' => Ok(ReNode::Literal('\t')),
'v' => Ok(ReNode::Literal('\u{0b}')), 'b' => Ok(ReNode::Literal('\u{08}')), 'B' => Ok(ReNode::Literal('\\')), 'x' | 'u' => {
let want = if esc == 'x' { 2 } else { 4 };
let mut hex = alloc::string::String::new();
while hex.len() < want && *p < chars.len() && chars[*p].is_ascii_hexdigit() {
hex.push(chars[*p]);
*p += 1;
}
if hex.is_empty() {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regex compile: `\\{esc}` needs hex digits"),
});
}
let code =
u32::from_str_radix(&hex, 16).map_err(|_| EvalError::TypeMismatch {
detail: "regex compile: bad numeric escape".into(),
})?;
Ok(ReNode::Literal(char::from_u32(code).unwrap_or('\u{fffd}')))
}
d @ '1'..='9' => {
let mut n = (d as usize) - ('0' as usize);
while *p < chars.len() && chars[*p].is_ascii_digit() {
n = n * 10 + ((chars[*p] as usize) - ('0' as usize));
*p += 1;
}
if n == 0 || n >= *ng {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: invalid backreference number"
.into(),
});
}
Ok(ReNode::Backref { idx: n, ci: false })
}
other => Ok(ReNode::Literal(other)),
}
}
other => {
*p += 1;
Ok(ReNode::Literal(other))
}
}
}
fn re_parse_class(chars: &[char], p: &mut usize) -> Result<ReNode, EvalError> {
debug_assert_eq!(chars.get(*p), Some(&'['));
*p += 1;
let mut negated = false;
if *p < chars.len() && chars[*p] == '^' {
negated = true;
*p += 1;
}
let mut members: Vec<ClassMember> = Vec::new();
let mut first = true;
while *p < chars.len() {
let c = chars[*p];
if c == ']' && !first {
*p += 1; return Ok(ReNode::Class { members, negated });
}
first = false;
if c == '[' && chars.get(*p + 1) == Some(&':') {
let mut q = *p + 2;
let name_start = q;
while q < chars.len() && chars[q] != ':' {
q += 1;
}
if q + 1 >= chars.len() || chars[q + 1] != ']' {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: invalid character class".into(),
});
}
let name: String = chars[name_start..q].iter().collect();
members.extend(posix_class_members(&name)?);
*p = q + 2; continue;
}
if c == '\\' && *p + 1 < chars.len() {
let esc = chars[*p + 1];
*p += 2;
match esc {
'd' | 'w' | 's' => members.extend(shortcut_members(esc)),
'D' => members.push(ClassMember::NotInSet(shortcut_members('d'))),
'W' => members.push(ClassMember::NotInSet(shortcut_members('w'))),
'S' => members.push(ClassMember::NotInSet(shortcut_members('s'))),
't' => members.push(ClassMember::Single('\t')),
'n' => members.push(ClassMember::Single('\n')),
'r' => members.push(ClassMember::Single('\r')),
'f' => members.push(ClassMember::Single('\u{0c}')),
'v' => members.push(ClassMember::Single('\u{0b}')),
'b' => members.push(ClassMember::Single('\u{08}')), other => members.push(ClassMember::Single(other)),
}
continue;
}
let start = c;
*p += 1;
if *p + 1 < chars.len() && chars[*p] == '-' && chars[*p + 1] != ']' {
let end = chars[*p + 1];
*p += 2;
if end < start {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: invalid character range".into(),
});
}
members.push(ClassMember::Range(start, end));
} else {
members.push(ClassMember::Single(start));
}
}
Err(EvalError::TypeMismatch {
detail: "invalid regular expression: brackets [] not balanced".into(),
})
}
fn re_parse_bound(
chars: &[char],
p: &mut usize,
) -> Result<Option<(usize, Option<usize>)>, EvalError> {
debug_assert_eq!(chars.get(*p), Some(&'{'));
let mut q = *p + 1;
let (min, min_digits) = re_scan_count(chars, &mut q);
if min_digits == 0 {
return Ok(None);
}
let mut max = Some(min);
if q < chars.len() && chars[q] == ',' {
q += 1;
let (m, m_digits) = re_scan_count(chars, &mut q);
max = if m_digits == 0 { None } else { Some(m) };
}
if q >= chars.len() || chars[q] != '}' {
return Ok(None);
}
let repeat_max = REPEAT_MAX as usize;
if min > repeat_max || matches!(max, Some(mx) if mx > repeat_max) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid regular expression: regular expression is too complex \
(repetition count exceeds {REPEAT_MAX})"
),
});
}
if let Some(mx) = max {
if mx < min {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: {m,n} quantifier with n < m".into(),
});
}
}
*p = q + 1; Ok(Some((min, max)))
}
fn re_scan_count(chars: &[char], p: &mut usize) -> (usize, usize) {
let ceiling = REPEAT_MAX as usize + 1;
let mut val: usize = 0;
let mut digits = 0usize;
while *p < chars.len() && chars[*p].is_ascii_digit() {
let d = (chars[*p] as u8 - b'0') as usize;
val = val.saturating_mul(10).saturating_add(d).min(ceiling);
digits += 1;
*p += 1;
}
(val, digits)
}
fn consume_lazy_suffix(chars: &[char], p: &mut usize) -> bool {
if *p < chars.len() && chars[*p] == '?' {
*p += 1;
true
} else {
false
}
}
fn class_matches(member: &ClassMember, c: char) -> bool {
match member {
ClassMember::Single(s) => *s == c,
ClassMember::Range(a, b) => c >= *a && c <= *b,
ClassMember::NotInSet(subs) => !subs.iter().any(|m| class_matches(m, c)),
}
}
fn shortcut_members(kind: char) -> Vec<ClassMember> {
match kind {
'd' | 'D' => alloc::vec![ClassMember::Range('0', '9')],
'w' | 'W' => alloc::vec![
ClassMember::Range('a', 'z'),
ClassMember::Range('A', 'Z'),
ClassMember::Range('0', '9'),
ClassMember::Single('_'),
],
's' | 'S' => alloc::vec![
ClassMember::Single(' '),
ClassMember::Single('\t'),
ClassMember::Single('\n'),
ClassMember::Single('\r'),
ClassMember::Single('\u{0b}'), ClassMember::Single('\u{0c}'), ],
_ => Vec::new(),
}
}
fn posix_class_members(name: &str) -> Result<Vec<ClassMember>, EvalError> {
let members = match name {
"alpha" => alloc::vec![ClassMember::Range('a', 'z'), ClassMember::Range('A', 'Z')],
"digit" => alloc::vec![ClassMember::Range('0', '9')],
"alnum" => alloc::vec![
ClassMember::Range('a', 'z'),
ClassMember::Range('A', 'Z'),
ClassMember::Range('0', '9'),
],
"upper" => alloc::vec![ClassMember::Range('A', 'Z')],
"lower" => alloc::vec![ClassMember::Range('a', 'z')],
"xdigit" => alloc::vec![
ClassMember::Range('0', '9'),
ClassMember::Range('a', 'f'),
ClassMember::Range('A', 'F'),
],
"word" => alloc::vec![
ClassMember::Range('a', 'z'),
ClassMember::Range('A', 'Z'),
ClassMember::Range('0', '9'),
ClassMember::Single('_'),
],
"space" => alloc::vec![
ClassMember::Single(' '),
ClassMember::Single('\t'),
ClassMember::Single('\n'),
ClassMember::Single('\r'),
ClassMember::Single('\u{0b}'),
ClassMember::Single('\u{0c}'),
],
"blank" => alloc::vec![ClassMember::Single(' '), ClassMember::Single('\t')],
"cntrl" => alloc::vec![
ClassMember::Range('\u{00}', '\u{1f}'),
ClassMember::Single('\u{7f}'),
],
"print" => alloc::vec![ClassMember::Range('\u{20}', '\u{7e}')],
"graph" => alloc::vec![ClassMember::Range('\u{21}', '\u{7e}')],
"punct" => alloc::vec![
ClassMember::Range('\u{21}', '\u{2f}'),
ClassMember::Range('\u{3a}', '\u{40}'),
ClassMember::Range('\u{5b}', '\u{60}'),
ClassMember::Range('\u{7b}', '\u{7e}'),
],
_ => {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: invalid character class".into(),
});
}
};
Ok(members)
}
fn re_match_at(
node: &ReNode,
s: &[char],
pos: usize,
depth: u32,
steps: &mut u64,
) -> Result<Option<usize>, EvalError> {
if depth > MATCH_DEPTH_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
*steps += 1;
if *steps > MATCH_STEP_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
let d = depth + 1;
match node {
ReNode::Literal(c) => Ok(if s.get(pos).copied() == Some(*c) {
Some(pos + 1)
} else {
None
}),
ReNode::AnyChar => Ok(if pos < s.len() { Some(pos + 1) } else { None }),
ReNode::Class { members, negated } => match s.get(pos) {
Some(&c) => {
let hit = members.iter().any(|m| class_matches(m, c));
Ok(if hit ^ negated { Some(pos + 1) } else { None })
}
None => Ok(None),
},
ReNode::Start => Ok(if pos == 0 { Some(pos) } else { None }),
ReNode::End => Ok(if pos == s.len() { Some(pos) } else { None }),
ReNode::WordBoundary(kind) => {
let before = pos > 0 && is_word_char(s[pos - 1]);
let after = pos < s.len() && is_word_char(s[pos]);
let ok = match kind {
WordBoundaryKind::Boundary => before != after,
WordBoundaryKind::NonBoundary => before == after,
WordBoundaryKind::BegWord => !before && after,
WordBoundaryKind::EndWord => before && !after,
};
Ok(if ok { Some(pos) } else { None })
}
ReNode::Concat(items) => re_match_seq(items, s, pos, d, steps),
ReNode::Alt(branches) => {
for b in branches {
if let Some(p) = re_match_at(b, s, pos, d, steps)? {
return Ok(Some(p));
}
}
Ok(None)
}
ReNode::Quant {
inner,
min,
max,
greedy,
} => {
let mut count = 0usize;
let mut p = pos;
loop {
if !*greedy && count >= *min {
break;
}
if let Some(cap) = max {
if count >= *cap {
break;
}
}
match re_match_at(inner, s, p, d, steps)? {
Some(np) if np > p => {
p = np;
count += 1;
}
_ => break,
}
}
if count < *min {
return Ok(None);
}
Ok(Some(p))
}
ReNode::Lookahead { negative, inner } => {
let hit = re_match_at(inner, s, pos, d, steps)?.is_some();
Ok(if hit != *negative { Some(pos) } else { None })
}
ReNode::Group { inner, .. } => re_match_at(inner, s, pos, d, steps),
ReNode::Backref { .. } => Ok(None),
}
}
fn re_match_seq(
items: &[ReNode],
s: &[char],
pos: usize,
depth: u32,
steps: &mut u64,
) -> Result<Option<usize>, EvalError> {
if depth > MATCH_DEPTH_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
*steps += 1;
if *steps > MATCH_STEP_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
let d = depth + 1;
let Some((first, rest)) = items.split_first() else {
return Ok(Some(pos));
};
match first {
ReNode::Quant {
inner,
min,
max,
greedy,
} => {
let mut ends = alloc::vec![pos];
let mut p = pos;
let mut count = 0usize;
loop {
if let Some(cap) = max {
if count >= *cap {
break;
}
}
match re_match_at(inner, s, p, d, steps)? {
Some(np) if np > p => {
p = np;
count += 1;
ends.push(p);
}
_ => break,
}
}
let n = ends.len(); for i in 0..n {
let reps = if *greedy { n - 1 - i } else { i };
if reps < *min {
if *greedy {
break;
}
continue;
}
if let Some(e) = re_match_seq(rest, s, ends[reps], d, steps)? {
return Ok(Some(e));
}
}
Ok(None)
}
ReNode::Alt(branches) => {
for b in branches {
if let Some(p) = re_match_at(b, s, pos, d, steps)? {
if let Some(e) = re_match_seq(rest, s, p, d, steps)? {
return Ok(Some(e));
}
}
}
Ok(None)
}
ReNode::Concat(nested) => {
let mut combined: alloc::vec::Vec<ReNode> =
alloc::vec::Vec::with_capacity(nested.len() + rest.len());
combined.extend(nested.iter().cloned());
combined.extend(rest.iter().cloned());
re_match_seq(&combined, s, pos, d, steps)
}
other => match re_match_at(other, s, pos, d, steps)? {
Some(p) => re_match_seq(rest, s, p, d, steps),
None => Ok(None),
},
}
}
fn has_backref(node: &ReNode) -> bool {
match node {
ReNode::Backref { .. } => true,
ReNode::Group { inner, .. }
| ReNode::Quant { inner, .. }
| ReNode::Lookahead { inner, .. } => has_backref(inner),
ReNode::Concat(items) | ReNode::Alt(items) => items.iter().any(has_backref),
_ => false,
}
}
fn re_find(node: &ReNode, s: &[char], from: usize) -> Result<Option<(usize, usize)>, EvalError> {
if has_backref(node) {
return Ok(re_find_caps(node, s, from, max_group(node))?.map(|(span, _caps)| span));
}
let mut steps: u64 = 0;
let mut start = from;
loop {
if let Some(end) = re_match_at(node, s, start, 0, &mut steps)? {
return Ok(Some((start, end)));
}
if start >= s.len() {
return Ok(None);
}
start += 1;
}
}
fn max_group(node: &ReNode) -> usize {
match node {
ReNode::Group { idx, inner } => (*idx).max(max_group(inner)),
ReNode::Concat(items) | ReNode::Alt(items) => {
items.iter().map(max_group).max().unwrap_or(0)
}
ReNode::Quant { inner, .. } | ReNode::Lookahead { inner, .. } => max_group(inner),
ReNode::Backref { idx, .. } => *idx,
_ => 0,
}
}
const CAP_MATCH_DEPTH_LIMIT: u32 = 300;
type Caps = alloc::vec::Vec<Option<(usize, usize)>>;
type MatchWithCaps = ((usize, usize), Caps);
type CapJournal = alloc::vec::Vec<(usize, Option<(usize, usize)>)>;
fn cap_set(caps: &mut Caps, journal: &mut CapJournal, idx: usize, val: (usize, usize)) {
if idx < caps.len() {
journal.push((idx, caps[idx]));
caps[idx] = Some(val);
}
}
fn cap_undo(caps: &mut Caps, journal: &mut CapJournal, mark: usize) {
while journal.len() > mark {
let (idx, old) = journal.pop().unwrap();
caps[idx] = old;
}
}
fn re_match_at_caps(
node: &ReNode,
s: &[char],
pos: usize,
depth: u32,
steps: &mut u64,
caps: &mut Caps,
journal: &mut CapJournal,
) -> Result<Option<usize>, EvalError> {
if depth > CAP_MATCH_DEPTH_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
*steps += 1;
if *steps > MATCH_STEP_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
let d = depth + 1;
match node {
ReNode::Literal(c) => Ok((s.get(pos).copied() == Some(*c)).then_some(pos + 1)),
ReNode::AnyChar => Ok((pos < s.len()).then_some(pos + 1)),
ReNode::Class { members, negated } => match s.get(pos) {
Some(&c) => {
let hit = members.iter().any(|m| class_matches(m, c));
Ok((hit ^ negated).then_some(pos + 1))
}
None => Ok(None),
},
ReNode::Start => Ok((pos == 0).then_some(pos)),
ReNode::End => Ok((pos == s.len()).then_some(pos)),
ReNode::WordBoundary(kind) => {
let before = pos > 0 && is_word_char(s[pos - 1]);
let after = pos < s.len() && is_word_char(s[pos]);
let ok = match kind {
WordBoundaryKind::Boundary => before != after,
WordBoundaryKind::NonBoundary => before == after,
WordBoundaryKind::BegWord => !before && after,
WordBoundaryKind::EndWord => before && !after,
};
Ok(ok.then_some(pos))
}
ReNode::Concat(items) => re_match_seq_caps(items, s, pos, d, steps, caps, journal),
ReNode::Alt(branches) => {
for b in branches {
let mark = journal.len();
if let Some(p) = re_match_at_caps(b, s, pos, d, steps, caps, journal)? {
return Ok(Some(p));
}
cap_undo(caps, journal, mark);
}
Ok(None)
}
ReNode::Quant {
inner,
min,
max,
greedy,
} => {
let mut count = 0usize;
let mut p = pos;
loop {
if !*greedy && count >= *min {
break;
}
if let Some(cap) = max {
if count >= *cap {
break;
}
}
let mark = journal.len();
match re_match_at_caps(inner, s, p, d, steps, caps, journal)? {
Some(np) if np > p => {
p = np;
count += 1;
}
_ => {
cap_undo(caps, journal, mark);
break;
}
}
}
if count < *min {
return Ok(None);
}
Ok(Some(p))
}
ReNode::Lookahead { negative, inner } => {
let mark = journal.len();
let hit = re_match_at_caps(inner, s, pos, d, steps, caps, journal)?.is_some();
cap_undo(caps, journal, mark);
Ok((hit != *negative).then_some(pos))
}
ReNode::Group { idx, inner } => {
let start = pos;
match re_match_at_caps(inner, s, pos, d, steps, caps, journal)? {
Some(end) => {
cap_set(caps, journal, *idx, (start, end));
Ok(Some(end))
}
None => Ok(None),
}
}
ReNode::Backref { idx, ci } => match caps.get(*idx).copied().flatten() {
Some((cs, ce)) => {
let need_len = ce - cs;
let end = pos + need_len;
if end <= s.len()
&& (0..need_len).all(|k| {
let (a, b) = (s[pos + k], s[cs + k]);
if *ci {
a.eq_ignore_ascii_case(&b)
} else {
a == b
}
})
{
Ok(Some(end))
} else {
Ok(None)
}
}
None => Ok(Some(pos)),
},
}
}
fn re_match_seq_caps(
items: &[ReNode],
s: &[char],
pos: usize,
depth: u32,
steps: &mut u64,
caps: &mut Caps,
journal: &mut CapJournal,
) -> Result<Option<usize>, EvalError> {
if depth > CAP_MATCH_DEPTH_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
*steps += 1;
if *steps > MATCH_STEP_LIMIT {
return Err(EvalError::TypeMismatch {
detail: "invalid regular expression: regular expression is too complex".into(),
});
}
let d = depth + 1;
let Some((first, rest)) = items.split_first() else {
return Ok(Some(pos));
};
match first {
ReNode::Group { idx, inner } if matches!(**inner, ReNode::Quant { .. }) => {
let ReNode::Quant {
inner: qinner,
min,
max,
greedy,
} = &**inner
else {
unreachable!()
};
let mut ends = alloc::vec![pos];
let mut marks = alloc::vec![journal.len()];
let mut p = pos;
let mut count = 0usize;
loop {
if let Some(cap) = max {
if count >= *cap {
break;
}
}
let mark = journal.len();
match re_match_at_caps(qinner, s, p, d, steps, caps, journal)? {
Some(np) if np > p => {
p = np;
count += 1;
ends.push(p);
marks.push(mark);
}
_ => {
cap_undo(caps, journal, mark);
break;
}
}
}
let n = ends.len();
for i in 0..n {
let reps = if *greedy { n - 1 - i } else { i };
if reps < *min {
if *greedy {
break;
}
continue;
}
if reps + 1 < n {
cap_undo(caps, journal, marks[reps + 1]);
}
cap_set(caps, journal, *idx, (pos, ends[reps]));
let tail_mark = journal.len();
if let Some(e) = re_match_seq_caps(rest, s, ends[reps], d, steps, caps, journal)? {
return Ok(Some(e));
}
cap_undo(caps, journal, tail_mark);
}
Ok(None)
}
ReNode::Quant {
inner,
min,
max,
greedy,
} => {
let mut ends = alloc::vec![pos];
let mut marks = alloc::vec![journal.len()];
let mut p = pos;
let mut count = 0usize;
loop {
if let Some(cap) = max {
if count >= *cap {
break;
}
}
let mark = journal.len();
match re_match_at_caps(inner, s, p, d, steps, caps, journal)? {
Some(np) if np > p => {
p = np;
count += 1;
ends.push(p);
marks.push(mark);
}
_ => {
cap_undo(caps, journal, mark);
break;
}
}
}
let n = ends.len();
for i in 0..n {
let reps = if *greedy { n - 1 - i } else { i };
if reps < *min {
if *greedy {
break;
}
continue;
}
if reps + 1 < n {
cap_undo(caps, journal, marks[reps + 1]);
}
let tail_mark = journal.len();
if let Some(e) = re_match_seq_caps(rest, s, ends[reps], d, steps, caps, journal)? {
return Ok(Some(e));
}
cap_undo(caps, journal, tail_mark);
}
Ok(None)
}
ReNode::Alt(branches) => {
for b in branches {
let mark = journal.len();
if let Some(p) = re_match_at_caps(b, s, pos, d, steps, caps, journal)? {
if let Some(e) = re_match_seq_caps(rest, s, p, d, steps, caps, journal)? {
return Ok(Some(e));
}
}
cap_undo(caps, journal, mark);
}
Ok(None)
}
ReNode::Concat(nested) => {
let mut combined: alloc::vec::Vec<ReNode> =
alloc::vec::Vec::with_capacity(nested.len() + rest.len());
combined.extend(nested.iter().cloned());
combined.extend(rest.iter().cloned());
re_match_seq_caps(&combined, s, pos, d, steps, caps, journal)
}
other => {
let mark = journal.len();
match re_match_at_caps(other, s, pos, d, steps, caps, journal)? {
Some(p) => {
if let Some(e) = re_match_seq_caps(rest, s, p, d, steps, caps, journal)? {
return Ok(Some(e));
}
cap_undo(caps, journal, mark);
Ok(None)
}
None => Ok(None),
}
}
}
}
fn re_find_caps(
node: &ReNode,
s: &[char],
from: usize,
ngroups: usize,
) -> Result<Option<MatchWithCaps>, EvalError> {
let mut steps: u64 = 0;
let mut start = from;
loop {
let mut caps: Caps = alloc::vec![None; ngroups + 1];
let mut journal: CapJournal = alloc::vec::Vec::new();
if let Some(end) = re_match_at_caps(node, s, start, 0, &mut steps, &mut caps, &mut journal)?
{
return Ok(Some(((start, end), caps)));
}
if start >= s.len() {
return Ok(None);
}
start += 1;
}
}
fn matchall_length_bounds(node: &ReNode) -> Option<(usize, Option<usize>)> {
let ReNode::Concat(items) = node else {
return None;
};
if items.len() < 2
|| !matches!(items.first(), Some(ReNode::Start))
|| !matches!(items.last(), Some(ReNode::End))
{
return None;
}
let mut min_sum: usize = 0;
let mut max_sum: Option<usize> = Some(0);
let mut flexible = 0u32;
for atom in &items[1..items.len() - 1] {
let (amin, amax) = match atom {
ReNode::AnyChar => (1usize, Some(1usize)),
ReNode::Quant {
inner, min, max, ..
} if matches!(**inner, ReNode::AnyChar) => (*min, *max),
_ => return None,
};
if amax != Some(amin) {
flexible += 1;
}
min_sum = min_sum.saturating_add(amin);
max_sum = match (max_sum, amax) {
(Some(a), Some(b)) => Some(a.saturating_add(b)),
_ => None,
};
}
if flexible > 1 {
return None;
}
Some((min_sum, max_sum))
}
fn flags_have_i(args: &[Value<'_>], idx: usize) -> Result<bool, EvalError> {
match args.get(idx) {
Some(v) => Ok(text_arg(v)?.map_or(false, |f| f.contains('i'))),
None => Ok(false),
}
}
pub(crate) fn similar_to_regex(pat: &str, esc: Option<&str>) -> Result<String, EvalError> {
similar_to_regex_mode(pat, esc, false)
}
fn similar_to_regex_mode(
pat: &str,
esc: Option<&str>,
for_substring: bool,
) -> Result<String, EvalError> {
let esc_char: Option<char> = match esc {
None => Some('\\'),
Some("") => None,
Some(e) => {
let mut it = e.chars();
let c = it.next();
if it.next().is_some() {
return Err(EvalError::TypeMismatch {
detail: "invalid escape string".into(),
});
}
c
}
};
let mut out = String::with_capacity(pat.len() * 3 + 8);
out.push_str("^(?:");
let mut nquotes = 0u8;
let mut afterescape = false;
let mut bracket_depth = 0i32;
let mut charclass_pos = 0i32;
for c in pat.chars() {
if afterescape {
if c == '"' && bracket_depth < 1 {
match nquotes {
0 => out.push_str(if for_substring { ")(" } else { "){1,1}?(" }),
1 => out.push_str(if for_substring { ")(?:" } else { "){1,1}(?:" }),
_ => {
return Err(EvalError::TypeMismatch {
detail: "SQL regular expression may not contain more than \
two escape-double-quote separators"
.into(),
});
}
}
nquotes += 1;
} else {
out.push('\\');
out.push(c);
charclass_pos = 3;
}
afterescape = false;
} else if Some(c) == esc_char {
afterescape = true;
} else if bracket_depth > 0 {
if c == '\\' {
out.push('\\');
}
out.push(c);
if c == ']' && charclass_pos > 2 {
bracket_depth -= 1;
} else if c == '[' {
bracket_depth += 1;
charclass_pos = 3;
} else if c == '^' {
charclass_pos += 1;
} else {
charclass_pos = 3;
}
} else if c == '[' {
out.push('[');
bracket_depth = 1;
charclass_pos = 1;
} else if c == '%' {
out.push_str(if for_substring && nquotes == 0 {
".*?"
} else {
".*"
});
} else if c == '_' {
out.push('.');
} else if c == '(' {
out.push_str("(?:");
} else if matches!(c, '\\' | '.' | '^' | '$') {
out.push('\\');
out.push(c);
} else {
out.push(c);
}
}
out.push_str(")$");
Ok(out)
}
pub(super) fn similar_to_match(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if !matches!(args.len(), 2 | 3) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("SIMILAR TO takes 2-3 args, got {}", args.len()),
});
}
let (Some(text), Some(pat)) = (text_arg(&args[0])?, text_arg(&args[1])?) else {
return Ok(Value::Null);
};
let esc = match args.get(2) {
None => None,
Some(Value::Null) => return Ok(Value::Null),
Some(v) => text_arg(v)?,
};
let re = similar_to_regex_mode(&pat, esc.as_deref(), true)?;
Ok(Value::Bool(regex_is_match(&re, &text)?))
}
pub(super) fn substring_similar(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 3 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("substring(similar) takes 3 args, got {}", args.len()),
});
}
let (Some(text), Some(pat)) = (text_arg(&args[0])?, text_arg(&args[1])?) else {
return Ok(Value::Null);
};
let Some(esc) = text_arg(&args[2])? else {
return Ok(Value::Null);
};
let re = similar_to_regex_mode(&pat, Some(esc.as_str()), true)?;
let node = re_compile(&re)?;
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
match re_find_caps(&node, &chars, 0, ngroups)? {
Some(((s_pos, e_pos), caps)) => {
let span = if ngroups >= 1 {
match caps.get(1).copied().flatten() {
Some(sp) => sp,
None => return Ok(Value::Null),
}
} else {
(s_pos, e_pos)
};
Ok(Value::text(
chars[span.0..span.1].iter().collect::<String>(),
))
}
None => Ok(Value::Null),
}
}
pub(crate) fn regex_is_match(pat: &str, text: &str) -> Result<bool, EvalError> {
let node = re_compile(pat)?;
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
Ok(re_find_caps(&node, &chars, 0, ngroups)?.is_some())
}
pub(super) fn regexp_matches(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
let (text, pat, all_matches) = match args.len() {
2 => (text_arg(&args[0])?, text_arg(&args[1])?, false),
3 => {
let flags = text_arg(&args[2])?.unwrap_or_default();
(
text_arg(&args[0])?,
text_arg(&args[1])?,
flags.contains('g'),
)
}
n => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_matches() takes 2 or 3 args, got {n}"),
});
}
};
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
let mut node = re_compile(&pat)?;
if flags_have_i(args, 2)? {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
let mut out: Vec<Option<String>> = Vec::new();
let mut from = 0usize;
while let Some(((s_pos, e_pos), caps)) = re_find_caps(&node, &chars, from, ngroups)? {
if ngroups == 0 {
out.push(Some(chars[s_pos..e_pos].iter().collect()));
} else {
for g in 1..=ngroups {
out.push(caps[g].map(|(a, b)| chars[a..b].iter().collect()));
}
}
if !all_matches {
break;
}
from = if e_pos > s_pos { e_pos } else { e_pos + 1 };
if from > chars.len() {
break;
}
}
Ok(Value::TextArray(out))
}
pub(crate) fn regexp_matches_rows(args: &[Value<'_>]) -> Result<Vec<Value<'static>>, EvalError> {
let (text, pat, all_matches) = match args.len() {
2 => (text_arg(&args[0])?, text_arg(&args[1])?, false),
3 => {
let flags = text_arg(&args[2])?.unwrap_or_default();
(
text_arg(&args[0])?,
text_arg(&args[1])?,
flags.contains('g'),
)
}
n => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_matches() takes 2 or 3 args, got {n}"),
});
}
};
let (Some(text), Some(pat)) = (text, pat) else {
return Ok(Vec::new());
};
let mut node = re_compile(&pat)?;
if flags_have_i(args, 2)? {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
let mut rows: Vec<Value<'static>> = Vec::new();
let mut from = 0usize;
while let Some(((s_pos, e_pos), caps)) = re_find_caps(&node, &chars, from, ngroups)? {
let groups: Vec<Option<String>> = if ngroups == 0 {
alloc::vec![Some(chars[s_pos..e_pos].iter().collect())]
} else {
(1..=ngroups)
.map(|g| caps[g].map(|(a, b)| chars[a..b].iter().collect()))
.collect()
};
rows.push(Value::TextArray(groups));
if !all_matches {
break;
}
from = if e_pos > s_pos { e_pos } else { e_pos + 1 };
if from > chars.len() {
break;
}
}
Ok(rows)
}
pub(super) fn regexp_match(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
let (text, pat) = match args.len() {
2 | 3 => (text_arg(&args[0])?, text_arg(&args[1])?),
n => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_match() takes 2 or 3 args, got {n}"),
});
}
};
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
let mut node = re_compile(&pat)?;
if flags_have_i(args, 2)? {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
match re_find_caps(&node, &chars, 0, ngroups)? {
Some(((s_pos, e_pos), caps)) => {
if ngroups == 0 {
Ok(Value::TextArray(alloc::vec![Some(
chars[s_pos..e_pos].iter().collect(),
)]))
} else {
Ok(Value::TextArray(
(1..=ngroups)
.map(|g| caps[g].map(|(a, b)| chars[a..b].iter().collect()))
.collect(),
))
}
}
None => Ok(Value::Null),
}
}
pub(super) fn regexp_replace(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() < 3 || args.len() > 6 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_replace() takes 3-6 args, got {}", args.len()),
});
}
let text = text_arg(&args[0])?;
let pat = text_arg(&args[1])?;
let repl = text_arg(&args[2])?;
fn int_arg(v: &Value<'_>) -> Result<Option<i64>, EvalError> {
match v {
Value::Null => Ok(None),
Value::SmallInt(n) => Ok(Some(i64::from(*n))),
Value::Int(n) => Ok(Some(i64::from(*n))),
Value::BigInt(n) => Ok(Some(*n)),
_ => Err(EvalError::TypeMismatch {
detail: "regexp_replace(): integer arg required".into(),
}),
}
}
let is_int = |v: &Value<'_>| matches!(v, Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_));
let (start_1based, nth, flags): (i64, Option<i64>, String) = match args.len() {
3 => (1, None, String::new()),
4 if is_int(&args[3]) => match int_arg(&args[3])? {
None => return Ok(Value::Null),
Some(st) => (st, None, String::new()),
},
4 => (1, None, text_arg(&args[3])?.unwrap_or_default()),
5 => match (int_arg(&args[3])?, int_arg(&args[4])?) {
(Some(st), Some(n)) => (st, Some(n), String::new()),
_ => return Ok(Value::Null),
},
_ => match (int_arg(&args[3])?, int_arg(&args[4])?) {
(Some(st), Some(n)) => (st, Some(n), text_arg(&args[5])?.unwrap_or_default()),
_ => return Ok(Value::Null),
},
};
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
let Some(repl) = repl else {
return Ok(Value::Null);
};
if start_1based < 1 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"start\": {start_1based}"),
});
}
if let Some(n) = nth {
if n < 0 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"n\": {n}"),
});
}
}
let global = nth == Some(0) || (nth.is_none() && flags.contains('g'));
let nth_target = nth.filter(|n| *n >= 1);
let mut node = re_compile(&pat)?;
if flags.contains('i') {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
let mut out = String::with_capacity(text.len());
let start_idx = ((start_1based - 1) as usize).min(chars.len());
out.extend(chars[..start_idx].iter());
let mut from = start_idx;
let mut hits = 0i64;
loop {
match re_find_caps(&node, &chars, from, ngroups)? {
Some(((s_pos, e_pos), caps)) => {
hits += 1;
let replace_this = match nth_target {
Some(n) => hits == n,
None => true,
};
out.extend(chars[from..s_pos].iter());
if replace_this {
expand_replacement(&repl, &chars, (s_pos, e_pos), &caps, &mut out);
} else {
out.extend(chars[s_pos..e_pos].iter());
}
let step = if e_pos > s_pos { e_pos } else { e_pos + 1 };
from = step;
let done = if let Some(n) = nth_target {
hits == n
} else {
!global
};
if done {
if from <= chars.len() {
out.extend(chars[from..].iter());
}
return Ok(Value::text(out));
}
if from > chars.len() {
break;
}
}
None => {
out.extend(chars[from..].iter());
break;
}
}
}
Ok(Value::text(out))
}
fn expand_replacement(
repl: &str,
chars: &[char],
whole: (usize, usize),
caps: &Caps,
out: &mut String,
) {
let rep: Vec<char> = repl.chars().collect();
let mut i = 0;
while i < rep.len() {
if rep[i] == '\\' && i + 1 < rep.len() {
let c = rep[i + 1];
if let Some(d) = c.to_digit(10) {
let g = d as usize;
if g == 0 {
out.extend(chars[whole.0..whole.1].iter());
} else if let Some(Some((a, b))) = caps.get(g) {
out.extend(chars[*a..*b].iter());
}
} else if c == '&' {
out.extend(chars[whole.0..whole.1].iter());
} else {
out.push(c);
}
i += 2;
} else {
out.push(rep[i]);
i += 1;
}
}
}
pub(super) fn substring_pattern(text: &str, pat: &str) -> Result<Value<'static>, EvalError> {
let node = re_compile(pat)?;
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
match re_find_caps(&node, &chars, 0, ngroups)? {
Some(((s_pos, e_pos), caps)) => {
if ngroups == 0 {
Ok(Value::text(chars[s_pos..e_pos].iter().collect::<String>()))
} else {
match caps.get(1).copied().flatten() {
Some((a, b)) => Ok(Value::text(chars[a..b].iter().collect::<String>())),
None => Ok(Value::Null),
}
}
}
None => Ok(Value::Null),
}
}
pub(super) fn regexp_split_to_array(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 2 && args.len() != 3 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_split_to_array() takes 2-3 args, got {}", args.len()),
});
}
let text = text_arg(&args[0])?;
let pat = text_arg(&args[1])?;
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
let mut node = re_compile(&pat)?;
if flags_have_i(args, 2)? {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let mut out: Vec<Option<String>> = Vec::new();
let mut piece_start = 0usize;
let mut from = 0usize;
loop {
match re_find(&node, &chars, from)? {
Some((s_pos, e_pos)) => {
let piece: String = chars[piece_start..s_pos].iter().collect();
out.push(Some(piece));
let step = if e_pos > s_pos { e_pos } else { e_pos + 1 };
from = step;
piece_start = step;
if from > chars.len() {
break;
}
}
None => {
let tail: String = chars[piece_start..].iter().collect();
out.push(Some(tail));
break;
}
}
}
Ok(Value::TextArray(out))
}
pub(super) fn regexp_instr(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() < 2 || args.len() > 7 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_instr() takes 2-7 args, got {}", args.len()),
});
}
let text = text_arg(&args[0])?;
let pat = text_arg(&args[1])?;
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
fn int_arg(v: &Value<'_>) -> Result<Option<i64>, EvalError> {
match v {
Value::Null => Ok(None),
Value::SmallInt(n) => Ok(Some(i64::from(*n))),
Value::Int(n) => Ok(Some(i64::from(*n))),
Value::BigInt(n) => Ok(Some(*n)),
_ => Err(EvalError::TypeMismatch {
detail: "regexp_instr(): integer arg required".into(),
}),
}
}
let start_1based = if args.len() >= 3 {
match int_arg(&args[2])? {
None => return Ok(Value::Null),
Some(n) => n,
}
} else {
1
};
let nth = if args.len() >= 4 {
match int_arg(&args[3])? {
None => return Ok(Value::Null),
Some(n) => n,
}
} else {
1
};
let endoption = if args.len() >= 5 {
match int_arg(&args[4])? {
None => return Ok(Value::Null),
Some(n) => n,
}
} else {
0
};
if start_1based < 1 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"start\": {start_1based}"),
});
}
if nth < 1 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"n\": {nth}"),
});
}
if !(0..=1).contains(&endoption) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"endoption\": {endoption}"),
});
}
let mut node = re_compile(&pat)?;
if flags_have_i(args, 5)? {
fold_case(&mut node);
}
let subexpr = if args.len() >= 7 {
match int_arg(&args[6])? {
None => return Ok(Value::Null),
Some(n) => n,
}
} else {
0
};
if subexpr < 0 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"subexpr\": {subexpr}"),
});
}
let chars: Vec<char> = text.chars().collect();
let ngroups = max_group(&node);
let mut from = (start_1based - 1) as usize;
let mut hits = 0i64;
while let Some(((s_pos, e_pos), caps)) = re_find_caps(&node, &chars, from, ngroups)? {
hits += 1;
if hits == nth {
let span = if subexpr > 0 {
match caps.get(subexpr as usize).copied().flatten() {
Some(sp) => sp,
None => return Ok(Value::Int(0)),
}
} else {
(s_pos, e_pos)
};
let idx = if endoption == 1 { span.1 } else { span.0 };
return Ok(Value::Int((idx + 1) as i32));
}
let step = if e_pos > s_pos { e_pos } else { e_pos + 1 };
from = step;
if from > chars.len() {
break;
}
}
Ok(Value::Int(0))
}
pub(super) fn regexp_substr(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() < 2 || args.len() > 5 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_substr() takes 2-5 args, got {}", args.len()),
});
}
let text = text_arg(&args[0])?;
let pat = text_arg(&args[1])?;
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
fn int_arg(v: &Value<'_>) -> Result<Option<i64>, EvalError> {
match v {
Value::Null => Ok(None),
Value::SmallInt(n) => Ok(Some(i64::from(*n))),
Value::Int(n) => Ok(Some(i64::from(*n))),
Value::BigInt(n) => Ok(Some(*n)),
_ => Err(EvalError::TypeMismatch {
detail: "regexp_substr(): integer arg required".into(),
}),
}
}
let start_1based = if args.len() >= 3 {
match int_arg(&args[2])? {
None => return Ok(Value::Null),
Some(n) => n,
}
} else {
1
};
let nth = if args.len() >= 4 {
match int_arg(&args[3])? {
None => return Ok(Value::Null),
Some(n) => n,
}
} else {
1
};
if start_1based < 1 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"start\": {start_1based}"),
});
}
if nth < 1 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid value for parameter \"n\": {nth}"),
});
}
let mut node = re_compile(&pat)?;
if flags_have_i(args, 4)? {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let mut from = (start_1based - 1) as usize;
let mut hits = 0i64;
while let Some((s_pos, e_pos)) = re_find(&node, &chars, from)? {
hits += 1;
if hits == nth {
let substr: String = chars[s_pos..e_pos].iter().collect();
return Ok(Value::text(substr));
}
let step = if e_pos > s_pos { e_pos } else { e_pos + 1 };
from = step;
if from > chars.len() {
break;
}
}
Ok(Value::Null)
}
#[derive(Debug, Clone)]
pub(crate) struct CompiledRe {
node: ReNode,
matchall: Option<(usize, Option<usize>)>,
}
pub(crate) fn compile_re(pat: &str, case_insensitive: bool) -> Result<CompiledRe, EvalError> {
let mut node = re_compile(pat)?;
if case_insensitive {
fold_case(&mut node);
}
let matchall = matchall_length_bounds(&node);
Ok(CompiledRe { node, matchall })
}
pub(crate) fn compiled_is_match(re: &CompiledRe, text: &str) -> Result<bool, EvalError> {
let chars: Vec<char> = text.chars().collect();
if let Some((min, max)) = re.matchall {
let len = chars.len();
if (len as u64) <= MATCHALL_SAFE_LEN {
return Ok(min <= len && max.is_none_or(|mx| len <= mx));
}
}
Ok(re_find(&re.node, &chars, 0)?.is_some())
}
pub(super) fn regexp_like(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() < 2 || args.len() > 3 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_like() takes 2 or 3 args, got {}", args.len()),
});
}
let text = text_arg(&args[0])?;
let pat = text_arg(&args[1])?;
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
let case_insensitive = match args.get(2) {
Some(v) => match text_arg(v)? {
Some(flags) => flags.contains('i'),
None => return Ok(Value::Null),
},
None => false,
};
let mut node = re_compile(&pat)?;
if case_insensitive {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
if let Some((min, max)) = matchall_length_bounds(&node) {
let len = chars.len();
if (len as u64) <= MATCHALL_SAFE_LEN {
let matched = min <= len && max.map_or(true, |mx| len <= mx);
return Ok(Value::Bool(matched));
}
}
Ok(Value::Bool(re_find(&node, &chars, 0)?.is_some()))
}
fn fold_case(node: &mut ReNode) {
match node {
ReNode::Literal(c) if c.is_ascii_alphabetic() => {
*node = ReNode::Class {
members: alloc::vec![
ClassMember::Single(c.to_ascii_lowercase()),
ClassMember::Single(c.to_ascii_uppercase()),
],
negated: false,
};
}
ReNode::Class { members, .. } => {
let mut extra: Vec<ClassMember> = Vec::new();
for m in members.iter() {
match m {
ClassMember::Single(c) if c.is_ascii_alphabetic() => {
extra.push(ClassMember::Single(c.to_ascii_lowercase()));
extra.push(ClassMember::Single(c.to_ascii_uppercase()));
}
ClassMember::Range(a, b)
if a.is_ascii_alphabetic() && b.is_ascii_alphabetic() =>
{
extra.push(ClassMember::Range(
a.to_ascii_lowercase(),
b.to_ascii_lowercase(),
));
extra.push(ClassMember::Range(
a.to_ascii_uppercase(),
b.to_ascii_uppercase(),
));
}
_ => {}
}
}
members.extend(extra);
}
ReNode::Quant { inner, .. }
| ReNode::Lookahead { inner, .. }
| ReNode::Group { inner, .. } => fold_case(inner),
ReNode::Concat(items) | ReNode::Alt(items) => {
for it in items.iter_mut() {
fold_case(it);
}
}
ReNode::Backref { ci, .. } => *ci = true,
_ => {}
}
}
pub(super) fn regexp_count(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() < 2 || args.len() > 4 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("regexp_count() takes 2-4 args, got {}", args.len()),
});
}
let text = text_arg(&args[0])?;
let pat = text_arg(&args[1])?;
let Some(text) = text else {
return Ok(Value::Null);
};
let Some(pat) = pat else {
return Ok(Value::Null);
};
let start_1based = if args.len() >= 3 {
match &args[2] {
Value::Null => return Ok(Value::Null),
Value::Int(n) => *n as i64,
Value::BigInt(n) => *n,
_ => {
return Err(EvalError::TypeMismatch {
detail: "regexp_count(): start must be integer".into(),
});
}
}
} else {
1
};
if start_1based < 1 {
return Err(EvalError::TypeMismatch {
detail: "regexp_count(): start must be >= 1".into(),
});
}
let mut node = re_compile(&pat)?;
if flags_have_i(args, 3)? {
fold_case(&mut node);
}
let chars: Vec<char> = text.chars().collect();
let mut count: i64 = 0;
let mut from = (start_1based - 1) as usize;
while let Some((s_pos, e_pos)) = re_find(&node, &chars, from)? {
count += 1;
let step = if e_pos > s_pos { e_pos } else { e_pos + 1 };
from = step;
if from > chars.len() {
break;
}
}
Ok(Value::BigInt(count))
}
#[cfg(test)]
mod redos_tests {
extern crate std;
use alloc::string::String;
use alloc::vec::Vec;
use std::thread;
fn chars(s: &str) -> Vec<char> {
s.chars().collect()
}
fn repeat_char(c: char, n: usize) -> String {
core::iter::repeat_n(c, n).collect()
}
fn repeat_char_str(s: &str, n: usize) -> String {
core::iter::repeat_n(s, n).collect()
}
#[test]
fn redos_deep_nested_groups_parse_error() {
let pat = repeat_char('(', 5000); let res = super::re_compile(&pat);
assert!(res.is_err(), "deeply-nested groups must be a clean error");
}
#[test]
fn redos_deep_match_returns_err_not_overflow() {
let handle = thread::Builder::new()
.stack_size(1024 * 1024)
.spawn(|| {
let pat = repeat_char('a', 6000);
let node = super::re_compile(&pat).expect("flat literal compiles");
let hay = chars(&repeat_char('a', 6000));
super::re_find(&node, &hay, 0)
})
.expect("spawn");
let res = handle.join().expect("match thread must not overflow/panic");
assert!(
res.is_err(),
"over-deep match must abort with a clean error"
);
}
#[test]
fn redos_repeat_bound_over_cap_rejected() {
assert!(super::re_compile("a{0,70000}").is_err());
assert!(super::re_compile("a{70000}").is_err());
assert!(super::re_compile("a{999999999999999999999}").is_err());
assert!(super::re_compile("a{5,2}").is_err());
assert!(super::re_compile("a{0,65535}").is_ok());
}
#[test]
fn redos_normal_bounds_match_correctly() {
let node = super::re_compile("^a{1,5}$").expect("compiles");
assert_eq!(
super::re_find(&node, &chars("aaa"), 0).unwrap(),
Some((0, 3))
);
assert_eq!(
super::re_find(&node, &chars("aaaaa"), 0).unwrap(),
Some((0, 5))
);
assert_eq!(super::re_find(&node, &chars(""), 0).unwrap(), None);
assert_eq!(super::re_find(&node, &chars("aaaaaa"), 0).unwrap(), None);
let exact = super::re_compile("^a{3}$").expect("compiles");
assert!(super::re_find(&exact, &chars("aaa"), 0).unwrap().is_some());
assert!(super::re_find(&exact, &chars("aa"), 0).unwrap().is_none());
let openb = super::re_compile("^a{2,}$").expect("compiles");
assert!(super::re_find(&openb, &chars("aaaa"), 0).unwrap().is_some());
assert!(super::re_find(&openb, &chars("a"), 0).unwrap().is_none());
}
#[test]
fn redos_stray_brace_is_literal() {
let node = super::re_compile("a{foo").expect("stray brace compiles as literal");
assert_eq!(
super::re_find(&node, &chars("a{foo"), 0).unwrap(),
Some((0, 5))
);
}
#[test]
fn redos_moderate_nesting_ok() {
let pat = alloc::format!("{}a{}", repeat_char('(', 50), repeat_char(')', 50));
assert!(super::re_compile(&pat).is_ok());
}
#[test]
fn redos_catastrophic_backtracking_returns_err_fast() {
use std::time::Instant;
let pat = alloc::format!("{}b", repeat_char_str("a*", 10));
let node = super::re_compile(&pat).expect("compiles");
let hay = chars(&repeat_char('a', 50));
let t0 = Instant::now();
let res = super::re_find(&node, &hay, 0);
let elapsed = t0.elapsed();
assert!(
res.is_err(),
"catastrophic backtracking must abort with a clean budget error, got {:?}",
res
);
assert!(
elapsed.as_secs() < 10,
"budget-exceeded abort must be fast, took {:?}",
elapsed
);
}
#[test]
fn redos_normal_long_input_still_matches() {
let node = super::re_compile("^a+b$").expect("compiles");
let hay = chars(&alloc::format!("{}b", repeat_char('a', 10_000)));
assert_eq!(
super::re_find(&node, &hay, 0).unwrap(),
Some((0, 10_001)),
"a legitimate long match must succeed — budget must not trip"
);
let miss = chars(&repeat_char('a', 10_000));
assert_eq!(super::re_find(&node, &miss, 0).unwrap(), None);
}
}
#[cfg(test)]
mod matchall_tests {
use alloc::string::String;
use alloc::vec::Vec;
use spg_storage::Value;
fn chars(s: &str) -> Vec<char> {
s.chars().collect()
}
fn repeat_char(c: char, n: usize) -> String {
core::iter::repeat_n(c, n).collect()
}
#[test]
fn matchall_detects_dot_repetition_shapes() {
fn bounds(pat: &str) -> Option<(usize, Option<usize>)> {
super::matchall_length_bounds(&super::re_compile(pat).unwrap())
}
assert_eq!(bounds("^.*$"), Some((0, None)));
assert_eq!(bounds("^.+$"), Some((1, None)));
assert_eq!(bounds("^.$"), Some((1, Some(1))));
assert_eq!(bounds("^.{2,4}$"), Some((2, Some(4))));
assert_eq!(bounds("^.{3}$"), Some((3, Some(3))));
assert_eq!(bounds("^.{2,}$"), Some((2, None)));
assert_eq!(bounds("^..*$"), Some((1, None)));
assert_eq!(bounds("^..$"), Some((2, Some(2))));
assert_eq!(bounds("^$"), Some((0, Some(0))));
}
#[test]
fn matchall_rejects_non_dot_or_unanchored() {
fn bounds(pat: &str) -> Option<(usize, Option<usize>)> {
super::matchall_length_bounds(&super::re_compile(pat).unwrap())
}
assert_eq!(bounds("^a.*b$"), None);
assert_eq!(bounds(".*"), None);
assert_eq!(bounds("^.*"), None);
assert_eq!(bounds(".*$"), None);
assert_eq!(bounds("^.*.*$"), None);
assert_eq!(bounds("^.{2,4}.{1,3}$"), None);
assert_eq!(bounds("^[abc]*$"), None);
}
#[test]
fn matchall_star_matches_any_length() {
let node = super::re_compile("^.*$").unwrap();
let (min, max) = super::matchall_length_bounds(&node).unwrap();
for s in ["", "a", "abc", "hello world"] {
let cs = chars(s);
let len = cs.len();
let fast = min <= len && max.map_or(true, |mx| len <= mx) && !cs.contains(&'\n');
assert!(fast, "`.*` must match {s:?}");
}
}
#[test]
fn matchall_bounded_matches_window_only() {
let node = super::re_compile("^.{2,4}$").unwrap();
let (min, max) = super::matchall_length_bounds(&node).unwrap();
let verdict = |s: &str| {
let cs = chars(s);
let len = cs.len();
min <= len && max.map_or(true, |mx| len <= mx) && !cs.contains(&'\n')
};
assert!(!verdict("a")); assert!(verdict("ab")); assert!(verdict("abc")); assert!(verdict("abcd")); assert!(!verdict("abcde")); }
#[test]
fn matchall_non_dot_pattern_uses_backtracker_correctly() {
let node = super::re_compile("^a.*b$").unwrap();
assert_eq!(super::matchall_length_bounds(&node), None);
assert!(super::re_find(&node, &chars("axxxb"), 0).unwrap().is_some());
assert!(super::re_find(&node, &chars("axxxc"), 0).unwrap().is_none());
}
#[test]
fn matchall_fast_path_agrees_with_backtracker() {
let pats = [
"^.*$", "^.+$", "^.$", "^.{2,4}$", "^.{3}$", "^.{2,}$", "^..*$", "^..$", "^$",
];
let inputs = [
"", "a", "ab", "abc", "abcd", "abcde", "a\nb", "\n", "ab\n", "\n\n", "hello",
];
for pat in pats {
let node = super::re_compile(pat).unwrap();
let (min, max) =
super::matchall_length_bounds(&node).expect("pattern must be fast-pathed");
for s in inputs {
let cs = chars(s);
let len = cs.len();
let fast = min <= len && max.map_or(true, |mx| len <= mx);
let slow = super::re_find(&node, &cs, 0).unwrap().is_some();
assert_eq!(
fast, slow,
"fast/slow disagree for pat {pat:?} input {s:?}: fast={fast} slow={slow}"
);
}
}
}
#[test]
fn matchall_regexp_like_end_to_end() {
let like = |text: &str, pat: &str| -> bool {
match super::regexp_like(&[Value::text(text), Value::text(pat)]).unwrap() {
Value::Bool(b) => b,
other => panic!("regexp_like returned {other:?}"),
}
};
assert!(like("", "^.*$"));
assert!(like("anything at all", "^.*$"));
assert!(like("two\nlines", "^.*$"));
assert!(!like("a", "^.{2,4}$"));
assert!(like("abc", "^.{2,4}$"));
assert!(!like("abcde", "^.{2,4}$"));
assert!(like("axxxb", "^a.*b$"));
assert!(!like("axxxc", "^a.*b$"));
}
#[test]
fn word_boundary_assertions_match_pg18() {
let like = |text: &str, pat: &str| -> bool {
match super::regexp_like(&[Value::text(text), Value::text(pat)]).unwrap() {
Value::Bool(b) => b,
other => panic!("regexp_like returned {other:?}"),
}
};
let cases: &[(&str, &str, bool)] = &[
("foobar", r"\yfoo\y", false), ("foo bar", r"\yfoo\y", true), ("a.b", r"\ya\y", true), ("hello world", r"\yworld\y", true),
("foobar", r"\yfoo", true), ("foobar", r"bar\y", true), ("foobar", r"foo\ybar", false), ("foo_bar", r"foo\ybar", false), ("foo bar", r"\mbar", true),
("foobar", r"\mfoo", true),
("foo bar", r"foo\M", true),
("foobar", r"foo\M", false),
("foobar", r"oo\Yba", true), ("foo bar", r"foo\Y bar", false), ("foo bar", "foo\\bbar", false), ("a\u{08}c", "a\\bc", true), ("foobar", r"foo\Bbar", false), ("a\\c", r"a\Bc", true), ];
for &(text, pat, expected) in cases {
assert_eq!(
like(text, pat),
expected,
"SPG disagrees with PG18 for {text:?} ~ {pat:?} (PG18={expected})"
);
}
}
}
#[cfg(test)]
mod pg18_differential_tests {
extern crate std;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_storage::Value;
fn like(text: &str, pat: &str, ci: bool) -> bool {
let mut args = alloc::vec![Value::text(text), Value::text(pat)];
if ci {
args.push(Value::text("i"));
}
match super::regexp_like(&args).unwrap() {
Value::Bool(b) => b,
other => panic!("regexp_like returned {other:?}"),
}
}
fn span(text: &str, pat: &str) -> Option<String> {
match super::regexp_match(&[Value::text(text), Value::text(pat)]).unwrap() {
Value::Null => None,
Value::TextArray(v) => v.into_iter().next().flatten(),
other => panic!("regexp_match returned {other:?}"),
}
}
#[test]
fn pg18_differential_corpus() {
let mut fails: Vec<String> = Vec::new();
let bool_cases: &[(&str, &str, bool)] = &[
("ab12", "^[[:alpha:]]+", true),
("__", "[[:alnum:]]", false),
("a", "[[:alnum:]]", true),
(" ", "[[:space:]]", true),
("A", "[[:upper:]]", true),
("A", "[[:lower:]]", false),
("a", "[[:lower:]]", true),
("!", "[[:punct:]]", true),
("@", "[[:punct:]]", true),
("_", "[[:punct:]]", true),
(" ", "[[:punct:]]", false),
("f", "[[:xdigit:]]", true),
("g", "[[:xdigit:]]", false),
("_", "[[:word:]]", true),
(" ", "[[:word:]]", false),
("\u{0b}", "[[:space:]]", true), ("\u{0c}", "[[:space:]]", true), (" ", "[[:blank:]]", true),
("\t", "[[:blank:]]", true),
("\n", "[[:blank:]]", false),
("\u{01}", "[[:cntrl:]]", true),
("a", "[[:cntrl:]]", false),
(" ", "[[:print:]]", true),
(" ", "[[:graph:]]", false),
("a", "[[:graph:]]", true),
("a", "[^[:digit:]]", true), ("5", "[^[:digit:]]", false),
("foobar", r"\Afoo", true),
("xfoo", r"\Afoo", false),
("foobar", r"bar\Z", true),
("barx", r"bar\Z", false),
("aAb", r"a\Ab", false), ("\u{0b}", r"\s", true),
("\u{0c}", r"\s", true),
("\u{0b}", r"\S", false),
("5", r"[\d]", true),
("a", r"[\d]", false),
("_", r"[\w]", true),
("a", r"[\D]", true),
("5", r"[\D]", false),
("A", r"[\dA]", true),
("\t", r"[\t]", true),
("\t", r"[\s]", true),
("x", r"[\S]", true),
("\t", r"[\S]", false),
("5", r"[\w.]", true),
("]", "[]a]", true),
("a", "[]a]", true),
("b", "[^]a]", true),
("]", "[^]a]", false),
(".", "[.]", true),
("a", "[.]", false),
("[", "[[]", true),
("a", "[a-]", true),
("-", "[a-]", true),
("-", "[-a]", true),
("m", "[a-z]", true),
("5", "[^a-z]", true),
("color", "colou?r", true),
("abc", "^abc$", true),
("cat", "cat|dog", true),
("a.b", r"a\.b", true),
("axb", r"a\.b", false),
("7", r"\d", true),
("a", r"\D", true),
("_", r"\w", true),
];
for &(text, pat, want) in bool_cases {
let got = like(text, pat, false);
if got != want {
fails.push(alloc::format!(
"BOOL {text:?} ~ {pat:?}: PG18={want} SPG={got}"
));
}
}
if !like("HELLO", "hello", true) {
fails.push("BOOL(ci) 'HELLO' ~* 'hello': PG18=true SPG=false".to_string());
}
let span_cases: &[(&str, &str, Option<&str>)] = &[
("ab12cd", "[[:digit:]]+", Some("12")),
("a1!", "[[:alpha:][:digit:]]+", Some("a1")),
("a5", r"[\d]+", Some("5")),
("aXbXb", "a.*b", Some("aXbXb")),
("aaab", "a+", Some("aaa")),
("aaaa", "a{2,3}", Some("aaa")),
("axbxb", "a.*?b", Some("axb")), ("aaa", "a+?", Some("a")), ("<a><b>", "<.*?>", Some("<a>")),
("abcabc", "a.*?c", Some("abc")), ("a", "a??", Some("")), ("xaa", "xa??", Some("x")), ("abc", ".*?", Some("")), ("abc", ".+?", Some("a")), ("aaaa", "a{2,5}?", Some("aa")), ("aaaab", "a{2,5}?b", Some("aaaab")), ("abab", "(ab)+?", Some("ab")), ];
for &(text, pat, want) in span_cases {
let got = span(text, pat);
let want_owned = want.map(|s| s.to_string());
if got != want_owned {
fails.push(alloc::format!(
"SPAN {text:?} ~ {pat:?}: PG18={want:?} SPG={got:?}"
));
}
}
for pat in ["[[:notaclass:]]", "[[:^upper:]]"] {
if super::re_compile(pat).is_ok() {
fails.push(alloc::format!(
"COMPILE {pat:?}: PG18=error SPG=compiled-ok"
));
}
}
assert!(
fails.is_empty(),
"SPG diverges from PG18 on {} case(s):\n {}",
fails.len(),
fails.join("\n ")
);
}
#[test]
fn pg18_known_deferred_divergences() {
assert_eq!(span("ab", "a|ab"), Some("a".to_string()));
assert!(like("abab", r"(ab)\1", false));
}
}