#![allow(dead_code)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::match_like_matches_macro)]
#![allow(clippy::type_complexity)]
#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::redundant_closure)]
#![allow(clippy::len_without_is_empty)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::comparison_chain)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::manual_strip)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::string_slice)]
#![allow(clippy::needless_late_init)]
#![allow(clippy::manual_is_ascii_check)]
#![allow(clippy::sliced_string_as_bytes)]
mod advanced; mod engine; pub mod optimization; mod parser;
use aho_corasick::AhoCorasick;
use memchr::memmem;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use advanced::{Lookaround, LookaroundType};
use engine::DFA;
use parser::{
is_sequence_pattern, parse_escape, parse_quantified_pattern, parse_sequence,
starts_with_escape, BoundaryType, CharClass, Flags, Group, QuantifiedPattern, Sequence,
};
pub use advanced::{CaptureGroup, Captures};
pub use optimization::{literal, prefilter};
#[derive(Debug, Clone)]
pub struct Pattern {
matcher: Matcher,
prefilter: Option<(
optimization::prefilter::Prefilter,
optimization::literal::LiteralKind,
)>,
fast_path: Option<optimization::fast_path::FastPath>, #[allow(dead_code)]
flags: Flags, }
pub type ReXile = Pattern;
#[inline]
fn safe_slice(text: &str, start: usize) -> Option<&str> {
text.get(start..)
}
#[inline]
fn safe_slice_range(text: &str, start: usize, end: usize) -> Option<&str> {
text.get(start..end)
}
#[inline]
#[allow(dead_code)]
fn char_boundaries(text: &str, start_pos: usize) -> impl Iterator<Item = usize> + '_ {
(start_pos..=text.len()).filter(|&i| text.is_char_boundary(i))
}
impl Pattern {
pub fn new(pattern: &str) -> Result<Self, PatternError> {
let (flags, effective_pattern) =
if let Some((parsed_flags, rest)) = Flags::parse_from_pattern(pattern) {
(parsed_flags, rest)
} else {
(Flags::new(), pattern)
};
let has_start_anchor = effective_pattern.starts_with('^');
let has_end_anchor =
effective_pattern.ends_with('$') && !effective_pattern.ends_with("\\$");
let inner_pattern = {
let mut p = effective_pattern;
if has_start_anchor {
p = p.strip_prefix('^').unwrap_or(p);
}
if has_end_anchor {
p = p.strip_suffix('$').unwrap_or(p);
}
p
};
let has_captures = inner_pattern.contains('(')
&& !inner_pattern.contains("(?:")
&& !inner_pattern.contains("(?=")
&& !inner_pattern.contains("(?!")
&& !inner_pattern.contains("(?<=")
&& !inner_pattern.contains("(?<!");
let inner_ast = if has_captures {
parse_pattern_with_captures_with_flags(inner_pattern, &flags)?
} else {
parse_pattern_with_flags(inner_pattern, &flags)?
};
let ast = if has_start_anchor || has_end_anchor {
Ast::AnchoredPattern {
inner: Box::new(inner_ast),
start: has_start_anchor,
end: has_end_anchor,
}
} else {
inner_ast
};
let mut matcher = compile_ast(&ast)?;
if flags.case_insensitive && !matches!(matcher, Matcher::CaseInsensitive(_)) {
matcher = Matcher::CaseInsensitive(Box::new(matcher));
}
let fast_path = if flags.multiline || flags.dot_matches_newline {
None
} else {
if let Matcher::PatternWithCaptures { ref elements, .. } = matcher {
if let Some(dfa) = engine::capture_dfa::compile_capture_pattern(elements) {
Some(optimization::fast_path::FastPath::CaptureDFA(
std::sync::Arc::new(dfa),
))
} else {
let fast_path_pattern = if flags.case_insensitive {
pattern
} else {
effective_pattern
};
optimization::fast_path::detect_fast_path(fast_path_pattern)
}
} else {
let fast_path_pattern = if flags.case_insensitive {
pattern
} else {
effective_pattern
};
optimization::fast_path::detect_fast_path(fast_path_pattern)
}
};
let literals = optimization::literal::extract_from_pattern(effective_pattern);
let has_groups = effective_pattern.contains("(?:")
|| (effective_pattern.contains('(') && !effective_pattern.contains("(?"));
let prefilter = if !literals.is_empty()
&& literals.kind == optimization::literal::LiteralKind::Prefix
&& !has_groups
&& !flags.multiline
&& !flags.dot_matches_newline
{
let pf = optimization::prefilter::Prefilter::from_literals(&literals);
if pf.is_available() {
Some((pf, literals.kind))
} else {
None
}
} else {
None
};
Ok(Pattern {
matcher,
prefilter,
fast_path,
flags,
})
}
pub fn is_match(&self, text: &str) -> bool {
if let Some(ref fp) = self.fast_path {
return fp.find(text).is_some();
}
if let Some((ref prefilter, literal_kind)) = self.prefilter {
return self.is_match_with_prefilter(text, prefilter, literal_kind);
}
self.matcher.is_match(text)
}
fn is_match_with_prefilter(
&self,
text: &str,
prefilter: &prefilter::Prefilter,
literal_kind: literal::LiteralKind,
) -> bool {
let bytes = text.as_bytes();
let max_lookback = match literal_kind {
literal::LiteralKind::Prefix => 10, literal::LiteralKind::Inner => 30, literal::LiteralKind::Suffix => 50, literal::LiteralKind::None => return self.matcher.is_match(text),
};
for candidate_pos in prefilter.candidates(bytes) {
let lookback = candidate_pos.min(max_lookback);
for offset in 0..=lookback {
let start_pos = candidate_pos - offset;
if self
.matcher
.is_match(safe_slice(text, start_pos).unwrap_or(""))
{
return true;
}
}
}
false
}
pub fn find(&self, text: &str) -> Option<(usize, usize)> {
if let Some(ref fp) = self.fast_path {
return fp.find(text);
}
if let Some((ref prefilter, literal_kind)) = self.prefilter {
return self.find_with_prefilter(text, prefilter, literal_kind);
}
self.matcher.find(text)
}
fn find_with_prefilter(
&self,
text: &str,
prefilter: &prefilter::Prefilter,
literal_kind: literal::LiteralKind,
) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut earliest_match: Option<(usize, usize)> = None;
let max_lookback = match literal_kind {
literal::LiteralKind::Prefix => 10,
literal::LiteralKind::Inner => 30,
literal::LiteralKind::Suffix => 50,
literal::LiteralKind::None => return self.matcher.find(text),
};
for candidate_pos in prefilter.candidates(bytes) {
if let Some((start, _)) = earliest_match {
if start < candidate_pos {
return earliest_match;
}
}
let lookback = candidate_pos.min(max_lookback);
for offset in 0..=lookback {
let start_pos = candidate_pos - offset;
if let Some((match_start, match_end)) =
self.matcher.find(safe_slice(text, start_pos).unwrap_or(""))
{
let abs_start = start_pos + match_start;
let abs_end = start_pos + match_end;
if earliest_match.is_none() || abs_start < earliest_match.unwrap().0 {
earliest_match = Some((abs_start, abs_end));
}
break;
}
}
}
earliest_match
}
pub fn find_all(&self, text: &str) -> Vec<(usize, usize)> {
if let Some(ref fp) = self.fast_path {
return fp.find_all(text);
}
match &self.matcher {
Matcher::Literal(lit) => {
memmem::find_iter(text.as_bytes(), lit.as_bytes())
.map(|pos| (pos, pos + lit.len()))
.collect()
}
Matcher::MultiLiteral(ac) => {
ac.find_iter(text)
.map(|mat| (mat.start(), mat.end()))
.collect()
}
Matcher::Sequence(seq) => {
seq.find_all(text)
}
Matcher::Quantified(qp) => qp.find_all(text),
Matcher::AnchoredLiteral { .. }
| Matcher::AnchoredGroup { .. }
| Matcher::AnchoredPattern { .. } => self.matcher.find_all(text),
_ => {
self.find_iter(text).map(|m| (m.start(), m.end())).collect()
}
}
}
pub fn find_iter<'a>(&'a self, text: &'a str) -> FindIter<'a> {
FindIter {
matcher: &self.matcher,
fast_path: &self.fast_path,
text,
pos: 0,
}
}
pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
if let Matcher::PatternWithCaptures {
elements,
total_groups,
} = &self.matcher
{
for start_pos in 0..=text.len() {
if let Some((end_pos, capture_list)) =
Matcher::match_elements_with_backtrack_and_captures(text, start_pos, elements)
{
if end_pos > start_pos || elements.is_empty() {
let mut caps = Captures::new(text, (start_pos, end_pos), *total_groups);
for (group_num, cap_start, cap_end) in capture_list {
caps.set(group_num, cap_start, cap_end);
}
return Some(caps);
}
}
}
None
} else if let Matcher::Capture(inner_matcher, group_index) = &self.matcher {
let total_groups =
if let Matcher::PatternWithCaptures { total_groups, .. } = **inner_matcher {
total_groups
} else {
*group_index };
if let Some((start, end)) = inner_matcher.find(text) {
let mut caps = Captures::new(text, (start, end), total_groups);
caps.set(*group_index, start, end);
let nested = inner_matcher.extract_nested_captures(text, start);
for (group_num, cap_start, cap_end) in nested {
caps.set(group_num, cap_start, cap_end);
}
Some(caps)
} else {
None
}
} else if let Matcher::AnchoredPattern { inner, start, end } = &self.matcher {
if let Matcher::PatternWithCaptures {
elements,
total_groups,
} = inner.as_ref()
{
let check_anchor = |match_start: usize, match_end: usize| -> bool {
let start_ok = !*start || match_start == 0;
let end_ok = !*end || match_end == text.len();
start_ok && end_ok
};
for start_pos in 0..=text.len() {
if *start && start_pos != 0 {
continue;
}
if let Some((end_pos, capture_list)) =
Matcher::match_elements_with_backtrack_and_captures(
text, start_pos, elements,
)
{
if (end_pos > start_pos || elements.is_empty())
&& check_anchor(start_pos, end_pos)
{
let mut caps = Captures::new(text, (start_pos, end_pos), *total_groups);
for (group_num, cap_start, cap_end) in capture_list {
caps.set(group_num, cap_start, cap_end);
}
return Some(caps);
}
}
}
None
} else {
self.find(text).map(|(match_start, match_end)| {
Captures::new(text, (match_start, match_end), 0)
})
}
} else {
self.find(text)
.map(|(start, end)| Captures::new(text, (start, end), 0))
}
}
pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CapturesIter<'r, 't> {
CapturesIter {
pattern: self,
text,
pos: 0,
}
}
pub fn replace(&self, text: &str, replacement: &str) -> String {
let has_captures = replacement.contains('$');
if !has_captures {
if let Some((start, end)) = self.find(text) {
let mut result = String::new();
result.push_str(&text[..start]);
result.push_str(replacement);
result.push_str(&text[end..]);
result
} else {
text.to_string()
}
} else {
if let Some(caps) = self.captures(text) {
let match_start = caps.pos(0).unwrap().0;
let match_end = caps.pos(0).unwrap().1;
let mut result = String::new();
result.push_str(&text[..match_start]);
let mut chars = replacement.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '$' {
if let Some(&next_ch) = chars.peek() {
if next_ch.is_ascii_digit() {
chars.next(); let group_num = next_ch.to_digit(10).unwrap() as usize;
if let Some(group_text) = caps.get(group_num) {
result.push_str(group_text);
}
} else {
result.push('$');
}
} else {
result.push('$');
}
} else {
result.push(ch);
}
}
result.push_str(&text[match_end..]);
result
} else {
text.to_string()
}
}
}
pub fn replace_all(&self, text: &str, replacement: &str) -> String {
let has_captures = replacement.contains('$');
if !has_captures {
let mut result = String::new();
let mut last_end = 0;
for (start, end) in self.find_all(text) {
result.push_str(&text[last_end..start]);
result.push_str(replacement);
last_end = end;
}
result.push_str(&text[last_end..]);
return result;
}
let mut result = String::new();
let mut last_end = 0;
for caps in self.captures_iter(text) {
let _full_match = caps.get(0).unwrap();
let match_start = caps.pos(0).unwrap().0;
let match_end = caps.pos(0).unwrap().1;
result.push_str(&text[last_end..match_start]);
let mut chars = replacement.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '$' {
if let Some(&next_ch) = chars.peek() {
if next_ch.is_ascii_digit() {
chars.next(); let group_num = next_ch.to_digit(10).unwrap() as usize;
if let Some(group_text) = caps.get(group_num) {
result.push_str(group_text);
}
} else {
result.push('$');
}
} else {
result.push('$');
}
} else {
result.push(ch);
}
}
last_end = match_end;
}
result.push_str(&text[last_end..]);
result
}
pub fn split<'r, 't>(&'r self, text: &'t str) -> SplitIter<'r, 't> {
SplitIter {
pattern: self,
text,
pos: 0,
finished: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Match<'t> {
text: &'t str,
start: usize,
end: usize,
}
impl<'t> Match<'t> {
#[inline]
pub fn new(text: &'t str, start: usize, end: usize) -> Self {
Self { text, start, end }
}
#[inline]
pub fn start(&self) -> usize {
self.start
}
#[inline]
pub fn end(&self) -> usize {
self.end
}
#[inline]
pub fn as_str(&self) -> &'t str {
&self.text[self.start..self.end]
}
#[inline]
pub fn range(&self) -> std::ops::Range<usize> {
self.start..self.end
}
#[inline]
pub fn len(&self) -> usize {
self.end - self.start
}
#[inline]
pub fn is_empty(&self) -> bool {
self.start == self.end
}
}
pub struct FindIter<'a> {
matcher: &'a Matcher,
fast_path: &'a Option<optimization::fast_path::FastPath>,
text: &'a str,
pos: usize,
}
impl<'a> Iterator for FindIter<'a> {
type Item = Match<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.text.len() {
return None;
}
if let Some(ref fast_path) = self.fast_path {
if let Some((start, end)) = fast_path.find_at(self.text, self.pos) {
self.pos = end.max(self.pos + 1);
return Some(Match::new(self.text, start, end));
} else {
return None;
}
}
let remaining = &self.text[self.pos..];
if let Some((rel_start, rel_end)) = self.matcher.find(remaining) {
let abs_start = self.pos + rel_start;
let abs_end = self.pos + rel_end;
self.pos = abs_end.max(self.pos + 1);
Some(Match::new(self.text, abs_start, abs_end))
} else {
None
}
}
}
pub struct CapturesIter<'r, 't> {
pattern: &'r Pattern,
text: &'t str,
pos: usize,
}
impl<'r, 't> Iterator for CapturesIter<'r, 't> {
type Item = Captures<'t>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.text.len() {
return None;
}
if let Matcher::PatternWithCaptures {
elements,
total_groups,
} = &self.pattern.matcher
{
let remaining = &self.text[self.pos..];
let char_indices: Vec<usize> = remaining.char_indices().map(|(i, _)| i).collect();
let search_positions: Vec<usize> = if char_indices.is_empty() {
vec![0]
} else {
char_indices
.into_iter()
.chain(std::iter::once(remaining.len()))
.collect()
};
for &start_offset in &search_positions {
if start_offset >= remaining.len() {
break;
}
let mut pos = start_offset;
let mut capture_positions: Vec<(usize, usize)> = Vec::new();
let mut all_matched = true;
for element in elements {
let (matcher, group_num_opt) = match element {
CompiledCaptureElement::Capture(m, num) => (m, Some(*num)),
CompiledCaptureElement::NonCapture(m) => (m, None),
};
if let Some((rel_start, rel_end)) = matcher.find(&remaining[pos..]) {
if rel_start != 0 {
all_matched = false;
break;
}
let abs_start = pos;
let abs_end = pos + rel_end;
if let Some(group_num) = group_num_opt {
while capture_positions.len() < group_num {
capture_positions.push((0, 0));
}
capture_positions[group_num - 1] = (abs_start, abs_end);
}
pos = abs_end;
} else {
all_matched = false;
break;
}
}
if all_matched {
let abs_start = self.pos + start_offset;
let abs_end = self.pos + pos;
self.pos = abs_end.max(self.pos + 1);
let mut caps = Captures::new(self.text, (abs_start, abs_end), *total_groups);
for (i, &(start, end)) in capture_positions.iter().enumerate() {
caps.set(i + 1, self.pos - pos + start, self.pos - pos + end);
}
return Some(caps);
}
}
None
} else {
let remaining = &self.text[self.pos..];
if let Some((rel_start, rel_end)) = self.pattern.matcher.find(remaining) {
let abs_start = self.pos + rel_start;
let abs_end = self.pos + rel_end;
self.pos = abs_end.max(self.pos + 1);
Some(Captures::new(self.text, (abs_start, abs_end), 0))
} else {
None
}
}
}
}
pub struct SplitIter<'r, 't> {
pattern: &'r Pattern,
text: &'t str,
pos: usize,
finished: bool,
}
impl<'r, 't> Iterator for SplitIter<'r, 't> {
type Item = &'t str;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
let remaining = &self.text[self.pos..];
if let Some((rel_start, rel_end)) = self.pattern.matcher.find(remaining) {
let abs_start = self.pos + rel_start;
let abs_end = self.pos + rel_end;
let result = &self.text[self.pos..abs_start];
self.pos = abs_end;
Some(result)
} else {
self.finished = true;
Some(&self.text[self.pos..])
}
}
}
static CACHE: OnceLock<Mutex<HashMap<String, Pattern>>> = OnceLock::new();
fn get_cache() -> &'static Mutex<HashMap<String, Pattern>> {
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn get_pattern(pattern: &str) -> Result<Pattern, PatternError> {
let mut cache = get_cache().lock().unwrap();
if let Some(p) = cache.get(pattern) {
return Ok(p.clone());
}
let compiled = Pattern::new(pattern)?;
cache.insert(pattern.to_string(), compiled.clone());
Ok(compiled)
}
pub fn is_match(pattern: &str, text: &str) -> Result<bool, PatternError> {
Ok(get_pattern(pattern)?.is_match(text))
}
pub fn find(pattern: &str, text: &str) -> Result<Option<(usize, usize)>, PatternError> {
Ok(get_pattern(pattern)?.find(text))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternError {
ParseError(String),
UnsupportedFeature(String),
}
impl std::fmt::Display for PatternError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PatternError::ParseError(msg) => write!(f, "Parse error: {}", msg),
PatternError::UnsupportedFeature(msg) => write!(f, "Unsupported: {}", msg),
}
}
}
impl std::error::Error for PatternError {}
#[derive(Debug, Clone, PartialEq)]
enum Ast {
Literal(String),
Dot, DotAll, Alternation(Vec<String>),
Anchored {
literal: String,
start: bool,
end: bool,
},
AnchoredGroup {
group: Group,
start: bool,
end: bool,
},
AnchoredPattern {
inner: Box<Ast>,
start: bool,
end: bool,
},
CharClass(CharClass),
Quantified(QuantifiedPattern),
Sequence(Sequence),
SequenceWithFlags(Sequence, Flags), Group(Group),
Boundary(BoundaryType), Lookaround(Lookaround), Capture(Box<Ast>, usize), QuantifiedCapture(Box<Ast>, parser::quantifier::Quantifier), CombinedWithLookaround {
prefix: Box<Ast>,
lookaround: Lookaround,
}, LookbehindWithSuffix {
lookbehind: Lookaround,
suffix: Box<Ast>,
}, PatternWithCaptures {
elements: Vec<CaptureElement>,
total_groups: usize,
}, AlternationWithCaptures {
branches: Vec<Ast>,
total_groups: usize,
}, Backreference(usize), CaseInsensitive(Box<Ast>), }
fn parse_pattern_with_groups(pattern: &str) -> Result<Ast, PatternError> {
if pattern.matches('(').count() > 1 && !pattern.contains('|') {
let mut combined_literals = Vec::new();
let mut pos = 0;
let mut all_parsed = true;
while pos < pattern.len() && pattern[pos..].starts_with('(') {
match parser::group::parse_group(&pattern[pos..]) {
Ok((group, bytes_consumed)) => {
match &group.content {
parser::group::GroupContent::Single(s) => {
combined_literals.push(s.clone());
}
parser::group::GroupContent::Sequence(seq) => {
let mut literal = String::new();
let mut is_simple = true;
for elem in &seq.elements {
match elem {
crate::parser::sequence::SequenceElement::Char(ch) => {
literal.push(*ch);
}
crate::parser::sequence::SequenceElement::Literal(lit) => {
literal.push_str(lit);
}
_ => {
is_simple = false;
break;
}
}
}
if is_simple {
combined_literals.push(literal);
} else {
all_parsed = false;
break;
}
}
parser::group::GroupContent::Alternation(_)
| parser::group::GroupContent::ParsedAlternation(_) => {
all_parsed = false;
break;
}
}
pos += bytes_consumed;
}
Err(_) => {
all_parsed = false;
break;
}
}
}
if all_parsed && pos == pattern.len() && !combined_literals.is_empty() {
use crate::parser::sequence::{Sequence, SequenceElement};
let mut elements = Vec::new();
for literal in combined_literals {
elements.push(SequenceElement::Literal(literal));
}
let seq = Sequence::new(elements);
return Ok(Ast::Sequence(seq));
}
}
if pattern.starts_with("^(") || pattern.ends_with(")$") {
let has_start = pattern.starts_with('^');
let has_end = pattern.ends_with('$');
let mut inner = pattern;
if has_start {
inner = &inner[1..]; }
if has_end {
inner = &inner[..inner.len() - 1]; }
if inner.starts_with('(') {
if let Ok((group, bytes_consumed)) = parser::group::parse_group(inner) {
if bytes_consumed == inner.len() {
let group_literal = match &group.content {
parser::group::GroupContent::Single(s) => Some(s.clone()),
parser::group::GroupContent::Sequence(seq) => {
let mut literal = String::new();
let mut is_simple = true;
for elem in &seq.elements {
match elem {
crate::parser::sequence::SequenceElement::Char(ch) => {
literal.push(*ch);
}
crate::parser::sequence::SequenceElement::Literal(lit) => {
literal.push_str(lit);
}
_ => {
is_simple = false;
break;
}
}
}
if is_simple {
Some(literal)
} else {
None
}
}
parser::group::GroupContent::Alternation(_)
| parser::group::GroupContent::ParsedAlternation(_) => {
None
}
};
if let Some(lit) = group_literal {
return Ok(Ast::Anchored {
literal: lit,
start: has_start,
end: has_end,
});
} else {
return Ok(Ast::AnchoredGroup {
group,
start: has_start,
end: has_end,
});
}
}
}
}
}
if pattern.starts_with('(') {
if let Ok((group, bytes_consumed)) = parser::group::parse_group(pattern) {
if bytes_consumed == pattern.len() {
return Ok(Ast::Group(group));
}
if bytes_consumed < pattern.len() {
let suffix = &pattern[bytes_consumed..];
match &group.content {
parser::group::GroupContent::Alternation(parts) => {
let expanded: Vec<String> =
parts.iter().map(|p| format!("{}{}", p, suffix)).collect();
return Ok(Ast::Alternation(expanded));
}
parser::group::GroupContent::Sequence(seq) => {
use crate::parser::sequence::{Sequence, SequenceElement};
let mut new_elements = seq.elements.clone();
for ch in suffix.chars() {
new_elements.push(SequenceElement::Char(ch));
}
let combined_seq = Sequence::new(new_elements);
return Ok(Ast::Sequence(combined_seq));
}
parser::group::GroupContent::Single(s) => {
let combined = format!("{}{}", s, suffix);
return Ok(Ast::Literal(combined));
}
parser::group::GroupContent::ParsedAlternation(_) => {
}
}
}
}
}
if let Some(group_start) = pattern.find('(') {
if group_start > 0 {
let prefix = &pattern[..group_start];
if prefix != "^" && prefix != "$" {
let group_part = &pattern[group_start..];
if let Ok((group, bytes_consumed)) = parser::group::parse_group(group_part) {
if bytes_consumed == group_part.len() {
match &group.content {
parser::group::GroupContent::Alternation(parts) => {
let expanded: Vec<String> =
parts.iter().map(|p| format!("{}{}", prefix, p)).collect();
return Ok(Ast::Alternation(expanded));
}
_ => {
return Ok(Ast::Group(group));
}
}
}
}
}
}
}
Err(PatternError::ParseError(
"Complex group pattern not fully supported".to_string(),
))
}
fn parse_pattern(pattern: &str) -> Result<Ast, PatternError> {
parse_pattern_with_depth(pattern, 0)
}
const MAX_RECURSION_DEPTH: usize = 100;
fn parse_pattern_with_depth(pattern: &str, depth: usize) -> Result<Ast, PatternError> {
if depth > MAX_RECURSION_DEPTH {
return Err(PatternError::ParseError(
"Pattern too complex: recursion depth exceeded".to_string(),
));
}
if pattern.is_empty() {
return Ok(Ast::Literal(String::new()));
}
if pattern.starts_with("(?=")
|| pattern.starts_with("(?!")
|| pattern.starts_with("(?<=")
|| pattern.starts_with("(?<!")
{
return parse_lookaround(pattern, depth);
}
if pattern.contains("(?=")
|| pattern.contains("(?!")
|| pattern.contains("(?<=")
|| pattern.contains("(?<!")
{
if let Ok(ast) = parse_combined_with_lookaround(pattern, depth) {
return Ok(ast);
}
}
if pattern.starts_with('(') && !pattern.starts_with("(?") {
if let Some(close_idx) = find_matching_paren(pattern, 0) {
if close_idx == pattern.len() - 1 {
let inner = &pattern[1..close_idx];
if !contains_unescaped_paren(inner) || inner.starts_with("(?") {
let inner_ast = parse_pattern_with_depth(inner, depth + 1)?;
return Ok(Ast::Capture(Box::new(inner_ast), 1)); }
}
}
}
let is_quantified_group = pattern.starts_with('(')
&& if let Some(close_idx) = find_matching_paren(pattern, 0) {
close_idx == pattern.len() - 2
&& (pattern.ends_with('?') || pattern.ends_with('*') || pattern.ends_with('+'))
} else {
false
};
let is_bounded_quantified_group = pattern.starts_with('(')
&& if let Some(close_idx) = find_matching_paren(pattern, 0) {
close_idx < pattern.len() - 1 && pattern[close_idx + 1..].starts_with('{')
} else {
false
};
if contains_unescaped_paren(pattern)
&& !pattern.starts_with('^')
&& !pattern.ends_with('$')
&& !is_quantified_group
&& !is_bounded_quantified_group
&& !pattern.contains("(?=")
&& !pattern.contains("(?!")
&& !pattern.contains("(?<=")
&& !pattern.contains("(?<!")
{
if let Ok(ast) = parse_pattern_with_captures(pattern) {
return Ok(ast);
}
}
if contains_unescaped_paren(pattern) {
if let Ok(ast) = parse_pattern_with_groups(pattern) {
return Ok(ast);
}
}
let has_start_anchor = pattern.starts_with('^');
let has_end_anchor = pattern.ends_with('$');
if has_start_anchor || has_end_anchor {
let mut literal = pattern;
if has_start_anchor {
literal = literal.strip_prefix('^').unwrap();
}
if has_end_anchor {
literal = literal.strip_suffix('$').unwrap();
}
return Ok(Ast::Anchored {
literal: literal.to_string(),
start: has_start_anchor,
end: has_end_anchor,
});
}
if pattern.contains('|') && !pattern.contains('[') {
let parts: Vec<String> = pattern.split('|').map(|s| s.to_string()).collect();
return Ok(Ast::Alternation(parts));
}
if is_sequence_pattern(pattern) {
match parse_sequence(pattern) {
Ok(seq) => return Ok(Ast::Sequence(seq)),
Err(_) => {
}
}
}
if starts_with_escape(pattern) {
match parse_escape(pattern) {
Ok((seq, bytes_consumed)) => {
if bytes_consumed == pattern.len() {
if let Some(boundary_type) = seq.to_boundary() {
return Ok(Ast::Boundary(boundary_type));
}
if let Some(cc) = seq.to_char_class() {
return Ok(Ast::CharClass(cc));
}
if let Some(ch) = seq.to_char() {
return Ok(Ast::Literal(ch.to_string()));
}
}
let remaining = &pattern[bytes_consumed..];
if !remaining.is_empty() {
if let Some(q_char) = remaining.chars().next() {
if q_char == '*' || q_char == '+' || q_char == '?' || q_char == '{' {
if let Ok(qp) = parse_quantified_pattern(pattern) {
return Ok(Ast::Quantified(qp));
}
}
}
}
}
Err(e) => return Err(PatternError::ParseError(e)),
}
}
let has_quantifier = pattern.ends_with('*')
|| pattern.ends_with('+')
|| pattern.ends_with('?')
|| (pattern.contains('{') && pattern.ends_with('}'));
if has_quantifier {
match parse_quantified_pattern(pattern) {
Ok(qp) => return Ok(Ast::Quantified(qp)),
Err(_) => {
if pattern.contains('{') {
return Err(PatternError::ParseError("Invalid quantifier".to_string()));
}
}
}
}
if pattern.contains('{') {
return Err(PatternError::ParseError("Invalid quantifier".to_string()));
}
if pattern.starts_with('[') && pattern.contains(']') {
let end_idx = pattern.find(']').unwrap();
if end_idx == pattern.len() - 1 {
let class_content = &pattern[1..end_idx];
let char_class = CharClass::parse(class_content).map_err(PatternError::ParseError)?;
return Ok(Ast::CharClass(char_class));
}
} else if pattern.starts_with('[') {
return Err(PatternError::ParseError(
"Unclosed character class".to_string(),
));
}
if pattern == "." {
return Ok(Ast::Dot);
}
if pattern.contains('.') {
use crate::parser::sequence::{Sequence, SequenceElement};
let mut elements = Vec::new();
for ch in pattern.chars() {
if ch == '.' {
elements.push(SequenceElement::Dot);
} else {
elements.push(SequenceElement::Char(ch));
}
}
return Ok(Ast::Sequence(Sequence::new(elements)));
}
Ok(Ast::Literal(pattern.to_string()))
}
fn parse_pattern_with_flags(pattern: &str, flags: &Flags) -> Result<Ast, PatternError> {
if flags.dot_matches_newline {
let ast = parse_pattern_dotall(pattern, flags)?;
if flags.case_insensitive {
return Ok(Ast::CaseInsensitive(Box::new(ast)));
}
return Ok(ast);
}
let ast = parse_pattern(pattern)?;
if flags.case_insensitive {
return Ok(Ast::CaseInsensitive(Box::new(ast)));
}
Ok(ast)
}
fn parse_pattern_dotall(pattern: &str, flags: &Flags) -> Result<Ast, PatternError> {
if pattern.is_empty() {
return Ok(Ast::Literal(String::new()));
}
if pattern == "." {
return Ok(Ast::DotAll);
}
if pattern.contains('.') {
if is_sequence_pattern(pattern) {
match parse_sequence(pattern) {
Ok(seq) => return Ok(Ast::SequenceWithFlags(seq, *flags)),
Err(_) => {
}
}
}
use crate::parser::sequence::{Sequence, SequenceElement};
let mut elements = Vec::new();
for ch in pattern.chars() {
if ch == '.' {
elements.push(SequenceElement::Dot);
} else {
elements.push(SequenceElement::Char(ch));
}
}
return Ok(Ast::SequenceWithFlags(Sequence::new(elements), *flags));
}
parse_pattern(pattern)
}
fn parse_pattern_with_captures_with_flags(
pattern: &str,
flags: &Flags,
) -> Result<Ast, PatternError> {
let ast = parse_pattern_with_captures(pattern)?;
if flags.case_insensitive {
return Ok(Ast::CaseInsensitive(Box::new(ast)));
}
Ok(ast)
}
#[derive(Debug, Clone)]
enum Matcher {
Literal(String),
MultiLiteral(AhoCorasick),
AnchoredLiteral {
literal: String,
start: bool,
end: bool,
},
AnchoredGroup {
group: Group,
start: bool,
end: bool,
},
AnchoredPattern {
inner: Box<Matcher>,
start: bool,
end: bool,
},
CharClass(CharClass),
Quantified(QuantifiedPattern),
Sequence(Sequence),
SequenceWithFlags(Sequence, Flags), Group(Group),
DigitRun, WordRun, Boundary(BoundaryType), Lookaround(Box<Lookaround>, Box<Matcher>), Capture(Box<Matcher>, usize), QuantifiedCapture(Box<Matcher>, parser::quantifier::Quantifier), CombinedWithLookaround {
prefix: Box<Matcher>,
lookaround: Box<Lookaround>,
lookaround_matcher: Box<Matcher>,
}, LookbehindWithSuffix {
lookbehind: Box<Lookaround>,
lookbehind_matcher: Box<Matcher>,
suffix: Box<Matcher>,
}, PatternWithCaptures {
elements: Vec<CompiledCaptureElement>,
total_groups: usize,
}, AlternationWithCaptures {
branches: Vec<Matcher>,
#[allow(dead_code)]
total_groups: usize,
}, Backreference(usize), DFA(DFA), LazyDFA(engine::lazy_dfa::LazyDFA), CaseInsensitive(Box<Matcher>), }
#[derive(Debug, Clone)]
enum CompiledCaptureElement {
Capture(Matcher, usize), NonCapture(Matcher), }
impl Matcher {
fn is_match(&self, text: &str) -> bool {
match self {
Matcher::Literal(lit) => memmem::find(text.as_bytes(), lit.as_bytes()).is_some(),
Matcher::MultiLiteral(ac) => ac.is_match(text),
Matcher::AnchoredLiteral {
literal,
start,
end,
} => match (start, end) {
(true, true) => text == literal,
(true, false) => text.starts_with(literal),
(false, true) => text.ends_with(literal),
_ => unreachable!(),
},
Matcher::AnchoredGroup { group, start, end } => {
match (start, end) {
(true, true) => {
group
.match_at(text, 0)
.map(|len| len == text.len())
.unwrap_or(false)
}
(true, false) => {
group.match_at(text, 0).is_some()
}
(false, true) => {
if let Some((_start_pos, end_pos)) = group.find(text) {
end_pos == text.len()
} else {
false
}
}
_ => unreachable!(),
}
}
Matcher::AnchoredPattern { inner, start, end } => {
match (start, end) {
(true, true) => {
if let Some((match_start, match_end)) = inner.find(text) {
match_start == 0 && match_end == text.len()
} else {
false
}
}
(true, false) => {
if let Some((match_start, _)) = inner.find(text) {
match_start == 0
} else {
false
}
}
(false, true) => {
if let Some((_, match_end)) = inner.find(text) {
match_end == text.len()
} else {
false
}
}
_ => unreachable!(),
}
}
Matcher::CharClass(cc) => {
cc.find_first(text).is_some()
}
Matcher::Quantified(qp) => {
if let crate::parser::quantifier::QuantifiedElement::CharClass(cc) = &qp.element {
if let Some(bitmap) = cc.get_ascii_bitmap() {
let negated = cc.negated;
let min = qp.quantifier.min_matches();
let bytes = text.as_bytes();
if min <= 1 {
for &byte in bytes {
if byte < 128 {
let idx = byte as usize;
let bit_set = (bitmap[idx / 64] & (1u64 << (idx % 64))) != 0;
if bit_set != negated {
return true;
}
}
}
return false;
} else {
let mut run = 0usize;
for &byte in bytes {
if byte < 128 {
let idx = byte as usize;
let bit_set = (bitmap[idx / 64] & (1u64 << (idx % 64))) != 0;
if bit_set != negated {
run += 1;
if run >= min {
return true;
}
continue;
}
}
run = 0;
}
return false;
}
}
}
qp.is_match(text)
}
Matcher::Sequence(seq) => seq.is_match(text), Matcher::Group(group) => group.is_match(text), Matcher::DigitRun => Self::digit_run_is_match(text), Matcher::WordRun => Self::word_run_is_match(text), Matcher::Boundary(boundary_type) => boundary_type.find_first(text).is_some(),
Matcher::Lookaround(lookaround, inner_matcher) => {
for pos in 0..=text.len() {
if lookaround.matches_at(text, pos, inner_matcher) {
return true;
}
}
false
}
Matcher::Capture(inner_matcher, _group_index) => {
inner_matcher.is_match(text)
}
Matcher::QuantifiedCapture(inner_matcher, quantifier) => {
Self::quantified_is_match(text, inner_matcher, quantifier)
}
Matcher::CombinedWithLookaround {
prefix,
lookaround,
lookaround_matcher,
} => {
if let Some((_start, end)) = prefix.find(text) {
lookaround.matches_at(text, end, lookaround_matcher)
} else {
false
}
}
Matcher::LookbehindWithSuffix {
lookbehind,
lookbehind_matcher,
suffix,
} => {
if let Some((start, _end)) = suffix.find(text) {
lookbehind.matches_at(text, start, lookbehind_matcher)
} else {
false
}
}
Matcher::PatternWithCaptures { .. } => {
self.find(text).is_some()
}
Matcher::Backreference(_) => {
false
}
Matcher::DFA(dfa) => {
dfa.is_match(text)
}
Matcher::LazyDFA(lazy_dfa) => {
let mut dfa = lazy_dfa.clone();
dfa.find(text).is_some()
}
Matcher::SequenceWithFlags(seq, flags) => {
seq.is_match_with_flags(text, flags)
}
Matcher::AlternationWithCaptures { branches, .. } => {
for branch in branches {
if branch.is_match(text) {
return true;
}
}
false
}
Matcher::CaseInsensitive(inner) => {
if let Matcher::Literal(needle) = inner.as_ref() {
let needle_bytes = needle.as_bytes();
let text_bytes = text.as_bytes();
let needle_is_ascii = needle_bytes.iter().all(|&b| b < 128);
if needle_is_ascii
&& !needle_bytes.is_empty()
&& needle_bytes.len() <= text_bytes.len()
{
let first_lower = needle_bytes[0];
let first_upper = if first_lower >= b'a' && first_lower <= b'z' {
first_lower - 32
} else {
first_lower
};
for i in 0..=(text_bytes.len() - needle_bytes.len()) {
let b = text_bytes[i];
if b == first_lower || b == first_upper {
let mut matched = true;
for j in 1..needle_bytes.len() {
let tb = text_bytes[i + j];
let nb = needle_bytes[j];
if tb >= 128 || nb >= 128 {
matched = false;
break;
}
if tb != nb && (tb ^ 32) != nb {
matched = false;
break;
}
}
if matched {
return true;
}
}
}
return false;
}
}
if let Matcher::MultiLiteral(ac) = inner.as_ref() {
let bytes = text.as_bytes();
let len = bytes.len();
if len <= 256 {
let mut buf = [0u8; 256];
let mut all_ascii = true;
for i in 0..len {
let b = bytes[i];
if b >= 128 {
all_ascii = false;
break;
}
buf[i] = if b >= b'A' && b <= b'Z' { b + 32 } else { b };
}
if all_ascii {
let lower = unsafe { std::str::from_utf8_unchecked(&buf[..len]) };
return ac.is_match(lower);
}
}
}
let bytes = text.as_bytes();
let len = bytes.len();
if len <= 256 {
let mut buf = [0u8; 256];
let mut all_ascii = true;
for i in 0..len {
let b = bytes[i];
if b >= 128 {
all_ascii = false;
break;
}
buf[i] = if b >= b'A' && b <= b'Z' { b + 32 } else { b };
}
if all_ascii {
let lower = unsafe { std::str::from_utf8_unchecked(&buf[..len]) };
return inner.is_match(lower);
}
}
let lower_text = text.to_lowercase();
inner.is_match(&lower_text)
}
}
}
fn extract_nested_captures(&self, text: &str, start_pos: usize) -> Vec<(usize, usize, usize)> {
let mut captures = Vec::new();
match self {
Matcher::PatternWithCaptures { elements, .. } => {
let mut pos = start_pos;
for element in elements {
match element {
CompiledCaptureElement::Capture(inner_matcher, group_num) => {
if let Some((rel_start, rel_end)) =
inner_matcher.find(safe_slice(text, pos).unwrap_or(""))
{
if rel_start == 0 {
let abs_start = pos;
let abs_end = pos + rel_end;
captures.push((*group_num, abs_start, abs_end));
let nested =
inner_matcher.extract_nested_captures(text, abs_start);
captures.extend(nested);
pos = abs_end;
} else {
break;
}
} else {
break;
}
}
CompiledCaptureElement::NonCapture(inner_matcher) => {
if let Some((rel_start, rel_end)) =
inner_matcher.find(safe_slice(text, pos).unwrap_or(""))
{
if rel_start == 0 {
let abs_start = pos;
let nested =
inner_matcher.extract_nested_captures(text, abs_start);
captures.extend(nested);
pos += rel_end;
} else {
break;
}
} else {
break;
}
}
}
}
}
Matcher::Capture(inner_matcher, group_num) => {
if let Some((rel_start, rel_end)) =
inner_matcher.find(safe_slice(text, start_pos).unwrap_or(""))
{
let abs_start = start_pos + rel_start;
let abs_end = start_pos + rel_end;
captures.push((*group_num, abs_start, abs_end));
let nested = inner_matcher.extract_nested_captures(text, abs_start);
captures.extend(nested);
}
}
Matcher::AlternationWithCaptures { branches, .. } => {
for branch in branches {
if let Some((rel_start, _rel_end)) =
branch.find(safe_slice(text, start_pos).unwrap_or(""))
{
if rel_start == 0 {
let abs_start = start_pos;
let nested = branch.extract_nested_captures(text, abs_start);
captures.extend(nested);
break; }
}
}
}
_ => {
}
}
captures
}
fn match_pattern_with_backreferences(
text: &str,
start_pos: usize,
elements: &[CompiledCaptureElement],
) -> Option<usize> {
let mut pos = start_pos;
let mut capture_positions: Vec<(usize, usize)> = Vec::new();
for element in elements {
match element {
CompiledCaptureElement::Capture(m, num) => {
if let Some((rel_start, rel_end)) = m.find(safe_slice(text, pos).unwrap_or(""))
{
if rel_start != 0 {
return None; }
let abs_start = pos;
let abs_end = pos + rel_end;
while capture_positions.len() < *num {
capture_positions.push((0, 0));
}
capture_positions[*num - 1] = (abs_start, abs_end);
pos = abs_end;
} else {
return None;
}
}
CompiledCaptureElement::NonCapture(m) => {
if let Matcher::Backreference(ref_num) = m {
if *ref_num > 0 && *ref_num <= capture_positions.len() {
let (cap_start, cap_end) = capture_positions[*ref_num - 1];
let captured_text = &text[cap_start..cap_end];
if text[pos..].starts_with(captured_text) {
pos += captured_text.len();
} else {
return None;
}
} else {
return None;
}
} else {
if let Some((rel_start, rel_end)) =
m.find(safe_slice(text, pos).unwrap_or(""))
{
if rel_start != 0 {
return None;
}
pos += rel_end;
} else {
return None;
}
}
}
}
}
Some(pos)
}
#[inline(always)]
fn digit_run_is_match(text: &str) -> bool {
let bytes = text.as_bytes();
if bytes.is_empty() {
return false;
}
bytes.iter().any(|&b| b.is_ascii_digit())
}
#[inline(always)]
fn word_run_is_match(text: &str) -> bool {
let bytes = text.as_bytes();
if bytes.is_empty() {
return false;
}
bytes.iter().any(|&b| {
b.is_ascii_lowercase() || b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'
})
}
fn quantified_is_match(
text: &str,
inner_matcher: &Matcher,
quantifier: &parser::quantifier::Quantifier,
) -> bool {
Self::quantified_find(text, inner_matcher, quantifier).is_some()
}
fn quantified_find(
text: &str,
inner_matcher: &Matcher,
quantifier: &parser::quantifier::Quantifier,
) -> Option<(usize, usize)> {
let (min, max) = quantifier_bounds(quantifier);
if text.is_empty() {
return if min == 0 { Some((0, 0)) } else { None };
}
for start_pos in 0..text.len() {
let mut pos = start_pos;
let mut count = 0;
while count < max && pos < text.len() {
if let Some((rel_start, rel_end)) =
inner_matcher.find(safe_slice(text, pos).unwrap_or(""))
{
if rel_start != 0 {
break;
}
if rel_end == 0 {
break; }
pos += rel_end;
count += 1;
} else {
break;
}
}
if count >= min {
return Some((start_pos, pos));
}
}
None
}
fn contains_quantified(matcher: &Matcher) -> bool {
match matcher {
Matcher::Quantified(_) | Matcher::QuantifiedCapture(_, _) => true,
Matcher::Capture(inner, _) => Self::contains_quantified(inner),
Matcher::PatternWithCaptures { elements, .. } => {
elements.iter().any(|elem| match elem {
CompiledCaptureElement::Capture(m, _)
| CompiledCaptureElement::NonCapture(m) => {
match m {
Matcher::AlternationWithCaptures { .. } => false,
_ => Self::contains_quantified(m),
}
}
})
}
Matcher::AlternationWithCaptures { .. } => false,
_ => false,
}
}
fn match_elements_with_backtrack_and_captures(
text: &str,
start_pos: usize,
elements: &[CompiledCaptureElement],
) -> Option<(usize, Vec<(usize, usize, usize)>)> {
if elements.is_empty() {
return Some((start_pos, Vec::new()));
}
let first_element = &elements[0];
let needs_backtracking = if elements.len() <= 1 {
false
} else {
match first_element {
CompiledCaptureElement::Capture(m, _) | CompiledCaptureElement::NonCapture(m) => {
Self::contains_quantified(m)
}
}
};
if needs_backtracking {
let remaining_text = safe_slice(text, start_pos).unwrap_or("");
let remaining_len = remaining_text.len();
for try_len in (0..=remaining_len).rev() {
let next_pos = start_pos + try_len;
if let Some((final_pos, mut remaining_caps)) =
Self::match_elements_with_backtrack_and_captures(text, next_pos, &elements[1..])
{
if try_len == 0 {
match first_element {
CompiledCaptureElement::Capture(m, num) => {
if let Some((rel_start, rel_end)) = m.find("") {
if rel_start == 0 && rel_end == 0 {
let mut caps = vec![(*num, start_pos, start_pos)];
caps.append(&mut remaining_caps);
return Some((final_pos, caps));
}
}
}
CompiledCaptureElement::NonCapture(m) => {
if let Some((rel_start, rel_end)) = m.find("") {
if rel_start == 0 && rel_end == 0 {
return Some((final_pos, remaining_caps));
}
}
}
}
} else {
let substring = safe_slice_range(text, start_pos, next_pos).unwrap_or("");
match first_element {
CompiledCaptureElement::Capture(m, num) => {
if let Some((rel_start, rel_end)) = m.find(substring) {
if rel_start == 0 && rel_end == substring.len() {
let mut caps = vec![(*num, start_pos, next_pos)];
caps.append(&mut remaining_caps);
return Some((final_pos, caps));
}
}
}
CompiledCaptureElement::NonCapture(m) => {
if let Some((rel_start, rel_end)) = m.find(substring) {
if rel_start == 0 && rel_end == substring.len() {
let nested_caps =
m.extract_nested_captures(text, start_pos);
let mut all_caps = nested_caps;
all_caps.extend(remaining_caps);
return Some((final_pos, all_caps));
}
}
}
}
}
}
}
None
} else {
match first_element {
CompiledCaptureElement::Capture(m, num) => {
if let Some((rel_start, rel_end)) =
m.find(safe_slice(text, start_pos).unwrap_or(""))
{
if rel_start == 0 {
let next_pos = start_pos + rel_end;
if let Some((final_pos, mut remaining_caps)) =
Self::match_elements_with_backtrack_and_captures(
text,
next_pos,
&elements[1..],
)
{
let mut caps = vec![(*num, start_pos, next_pos)];
caps.append(&mut remaining_caps);
return Some((final_pos, caps));
}
}
}
None
}
CompiledCaptureElement::NonCapture(m) => {
if let Some((rel_start, rel_end)) =
m.find(safe_slice(text, start_pos).unwrap_or(""))
{
if rel_start == 0 {
let next_pos = start_pos + rel_end;
if let Some((final_pos, remaining_caps)) =
Self::match_elements_with_backtrack_and_captures(
text,
next_pos,
&elements[1..],
)
{
let nested_caps = m.extract_nested_captures(text, start_pos);
let mut all_caps = nested_caps;
all_caps.extend(remaining_caps);
return Some((final_pos, all_caps));
}
}
}
None
}
}
}
}
fn match_elements_with_backtrack(
text: &str,
start_pos: usize,
elements: &[CompiledCaptureElement],
) -> Option<usize> {
if elements.is_empty() {
return Some(start_pos);
}
let first_element = &elements[0];
let first_matcher = match first_element {
CompiledCaptureElement::Capture(m, _) => m,
CompiledCaptureElement::NonCapture(m) => m,
};
let needs_backtracking = if elements.len() <= 1 {
false } else {
Self::contains_quantified(first_matcher)
};
if needs_backtracking {
let remaining_text = safe_slice(text, start_pos).unwrap_or("");
let remaining_len = remaining_text.len();
for try_len in (0..=remaining_len).rev() {
let next_pos = start_pos + try_len;
if let Some(final_pos) =
Self::match_elements_with_backtrack(text, next_pos, &elements[1..])
{
let substring = safe_slice_range(text, start_pos, next_pos).unwrap_or("");
if try_len == 0 {
if let Some((rel_start, rel_end)) = first_matcher.find("") {
if rel_start == 0 && rel_end == 0 {
return Some(final_pos);
}
}
} else {
if let Some((rel_start, rel_end)) = first_matcher.find(substring) {
if rel_start == 0 && rel_end == substring.len() {
return Some(final_pos);
}
}
}
}
}
None
} else {
if let Matcher::AlternationWithCaptures { branches, .. } = first_matcher {
for branch in branches {
if let Some((rel_start, rel_end)) =
branch.find(safe_slice(text, start_pos).unwrap_or(""))
{
if rel_start == 0 {
let next_pos = start_pos + rel_end;
if let Some(final_pos) =
Self::match_elements_with_backtrack(text, next_pos, &elements[1..])
{
return Some(final_pos);
}
}
}
}
return None;
}
if let Some((rel_start, rel_end)) =
first_matcher.find(safe_slice(text, start_pos).unwrap_or(""))
{
if rel_start == 0 {
let next_pos = start_pos + rel_end;
return Self::match_elements_with_backtrack(text, next_pos, &elements[1..]);
}
}
None
}
}
fn quantified_find_all(
text: &str,
inner_matcher: &Matcher,
quantifier: &parser::quantifier::Quantifier,
) -> Vec<(usize, usize)> {
let mut matches = Vec::new();
let mut search_pos = 0;
while search_pos < text.len() {
if let Some((start, end)) =
Self::quantified_find(&text[search_pos..], inner_matcher, quantifier)
{
matches.push((search_pos + start, search_pos + end));
search_pos += start + 1; if start == end {
search_pos += 1; }
} else {
break;
}
}
matches
}
fn find(&self, text: &str) -> Option<(usize, usize)> {
match self {
Matcher::Literal(lit) => {
let pos = memmem::find(text.as_bytes(), lit.as_bytes())?;
Some((pos, pos + lit.len()))
}
Matcher::MultiLiteral(ac) => {
let mat = ac.find(text)?;
Some((mat.start(), mat.end()))
}
Matcher::AnchoredLiteral {
literal,
start,
end,
} => match (start, end) {
(true, true) => (text == literal).then_some((0, text.len())),
(true, false) => text.starts_with(literal).then_some((0, literal.len())),
(false, true) => text
.ends_with(literal)
.then(|| (text.len() - literal.len(), text.len())),
_ => unreachable!(),
},
Matcher::AnchoredGroup { group, start, end } => {
match (start, end) {
(true, true) => {
group.match_at(text, 0).and_then(|len| {
if len == text.len() {
Some((0, len))
} else {
None
}
})
}
(true, false) => {
group.match_at(text, 0).map(|len| (0, len))
}
(false, true) => {
group.find(text).and_then(|(start_pos, end_pos)| {
if end_pos == text.len() {
Some((start_pos, end_pos))
} else {
None
}
})
}
_ => unreachable!(),
}
}
Matcher::AnchoredPattern { inner, start, end } => {
match (start, end) {
(true, true) => {
inner.find(text).and_then(|(match_start, match_end)| {
if match_start == 0 && match_end == text.len() {
Some((0, text.len()))
} else {
None
}
})
}
(true, false) => {
inner.find(text).and_then(|(match_start, match_end)| {
if match_start == 0 {
Some((0, match_end))
} else {
None
}
})
}
(false, true) => {
inner.find(text).and_then(|(match_start, match_end)| {
if match_end == text.len() {
Some((match_start, match_end))
} else {
None
}
})
}
_ => unreachable!(),
}
}
Matcher::CharClass(cc) => {
for (idx, ch) in text.char_indices() {
if cc.matches(ch) {
return Some((idx, idx + ch.len_utf8()));
}
}
None
}
Matcher::Quantified(qp) => qp.find(text),
Matcher::Sequence(seq) => seq.find(text),
Matcher::Group(group) => group.find(text),
Matcher::DigitRun => Self::digit_run_find(text), Matcher::WordRun => Self::word_run_find(text), Matcher::Boundary(boundary_type) => {
boundary_type.find_first(text).map(|pos| (pos, pos))
}
Matcher::Lookaround(lookaround, inner_matcher) => {
for pos in 0..=text.len() {
if lookaround.matches_at(text, pos, inner_matcher) {
return Some((pos, pos)); }
}
None
}
Matcher::Capture(inner_matcher, _group_index) => {
inner_matcher.find(text)
}
Matcher::QuantifiedCapture(inner_matcher, quantifier) => {
Self::quantified_find(text, inner_matcher, quantifier)
}
Matcher::CombinedWithLookaround {
prefix,
lookaround,
lookaround_matcher,
} => {
let mut search_pos = 0;
while search_pos < text.len() {
let remaining = &text[search_pos..];
if let Some((rel_start, rel_end)) = prefix.find(remaining) {
let abs_start = search_pos + rel_start;
let abs_end = search_pos + rel_end;
if lookaround.matches_at(text, abs_end, lookaround_matcher) {
return Some((abs_start, abs_end));
}
search_pos = abs_start + 1;
} else {
break;
}
}
None
}
Matcher::LookbehindWithSuffix {
lookbehind,
lookbehind_matcher,
suffix,
} => {
let mut search_pos = 0;
while search_pos < text.len() {
let remaining = &text[search_pos..];
if let Some((rel_start, rel_end)) = suffix.find(remaining) {
let abs_start = search_pos + rel_start;
let abs_end = search_pos + rel_end;
if lookbehind.matches_at(text, abs_start, lookbehind_matcher) {
return Some((abs_start, abs_end));
}
search_pos = abs_start + 1;
} else {
break;
}
}
None
}
Matcher::PatternWithCaptures { elements, .. } => {
if elements.len() == 1 {
let matcher = match &elements[0] {
CompiledCaptureElement::Capture(m, _) => m,
CompiledCaptureElement::NonCapture(m) => m,
};
return matcher.find(text);
}
let has_backrefs = elements.iter().any(|elem| {
matches!(
elem,
CompiledCaptureElement::NonCapture(Matcher::Backreference(_))
)
});
if has_backrefs {
for start_pos in 0..=text.len() {
if let Some(end_pos) =
Self::match_pattern_with_backreferences(text, start_pos, elements)
{
if end_pos > start_pos || elements.is_empty() {
return Some((start_pos, end_pos));
}
}
}
return None;
}
if let Some(dfa) = crate::engine::capture_dfa::compile_capture_pattern(elements) {
return dfa.find(text);
}
for start_pos in 0..=text.len() {
if let Some(end_pos) =
Self::match_elements_with_backtrack(text, start_pos, elements)
{
if end_pos > start_pos || elements.is_empty() {
return Some((start_pos, end_pos));
}
}
}
None
}
Matcher::Backreference(_) => {
None
}
Matcher::DFA(dfa) => {
dfa.find(text)
}
Matcher::LazyDFA(lazy_dfa) => {
let mut dfa = lazy_dfa.clone();
dfa.find(text)
}
Matcher::SequenceWithFlags(seq, flags) => {
seq.find_with_flags(text, flags)
}
Matcher::AlternationWithCaptures { branches, .. } => {
let mut best_match: Option<(usize, usize)> = None;
for branch in branches {
if let Some((start, end)) = branch.find(text) {
if best_match.is_none() || start < best_match.unwrap().0 {
best_match = Some((start, end));
}
}
}
best_match
}
Matcher::CaseInsensitive(inner) => {
let bytes = text.as_bytes();
let len = bytes.len();
if len <= 256 {
let mut buf = [0u8; 256];
let mut all_ascii = true;
for i in 0..len {
let b = bytes[i];
if b >= 128 {
all_ascii = false;
break;
}
buf[i] = if b >= b'A' && b <= b'Z' { b + 32 } else { b };
}
if all_ascii {
let lower = unsafe { std::str::from_utf8_unchecked(&buf[..len]) };
return inner.find(lower);
}
}
let lower_text = text.to_lowercase();
inner.find(&lower_text)
}
}
}
#[inline(always)]
fn digit_run_find(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut start = None;
for (i, &b) in bytes.iter().enumerate() {
if b.is_ascii_digit() {
start = Some(i);
break;
}
}
let start_idx = start?;
let mut end_idx = bytes.len();
for (i, &b) in bytes[start_idx..].iter().enumerate() {
if !b.is_ascii_digit() {
end_idx = start_idx + i;
break;
}
}
Some((start_idx, end_idx))
}
#[inline(always)]
fn word_run_find(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut start = None;
for (i, &b) in bytes.iter().enumerate() {
if b.is_ascii_lowercase() || b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_' {
start = Some(i);
break;
}
}
let start_idx = start?;
let mut end_idx = bytes.len();
for (i, &b) in bytes[start_idx..].iter().enumerate() {
if !(b.is_ascii_lowercase()
|| b.is_ascii_uppercase()
|| b.is_ascii_digit()
|| b == b'_')
{
end_idx = start_idx + i;
break;
}
}
Some((start_idx, end_idx))
}
fn find_all(&self, text: &str) -> Vec<(usize, usize)> {
match self {
Matcher::Literal(lit) => {
let finder = memmem::Finder::new(lit.as_bytes());
finder
.find_iter(text.as_bytes())
.map(|pos| (pos, pos + lit.len()))
.collect()
}
Matcher::MultiLiteral(ac) => ac
.find_iter(text)
.map(|mat| (mat.start(), mat.end()))
.collect(),
Matcher::AnchoredLiteral { .. } => {
if let Some(m) = self.find(text) {
vec![m]
} else {
vec![]
}
}
Matcher::AnchoredGroup { .. } => {
if let Some(m) = self.find(text) {
vec![m]
} else {
vec![]
}
}
Matcher::AnchoredPattern { .. } => {
if let Some(m) = self.find(text) {
vec![m]
} else {
vec![]
}
}
Matcher::CharClass(cc) => {
text.char_indices()
.filter(|(_, ch)| cc.matches(*ch))
.map(|(idx, ch)| (idx, idx + ch.len_utf8()))
.collect()
}
Matcher::Quantified(qp) => qp.find_all(text),
Matcher::Sequence(seq) => seq.find_all(text),
Matcher::Group(group) => group.find_all(text),
Matcher::DigitRun => Self::digit_run_find_all(text), Matcher::WordRun => Self::word_run_find_all(text), Matcher::Boundary(boundary_type) => {
boundary_type
.find_all(text)
.into_iter()
.map(|pos| (pos, pos))
.collect()
}
Matcher::Lookaround(lookaround, inner_matcher) => {
(0..=text.len())
.filter(|&pos| lookaround.matches_at(text, pos, inner_matcher))
.map(|pos| (pos, pos)) .collect()
}
Matcher::Capture(inner_matcher, _group_index) => {
inner_matcher.find_all(text)
}
Matcher::QuantifiedCapture(inner_matcher, quantifier) => {
Self::quantified_find_all(text, inner_matcher, quantifier)
}
Matcher::CombinedWithLookaround {
prefix,
lookaround,
lookaround_matcher,
} => {
let mut matches = Vec::new();
let mut search_pos = 0;
while search_pos < text.len() {
let remaining = &text[search_pos..];
if let Some((rel_start, rel_end)) = prefix.find(remaining) {
let abs_start = search_pos + rel_start;
let abs_end = search_pos + rel_end;
if lookaround.matches_at(text, abs_end, lookaround_matcher) {
matches.push((abs_start, abs_end));
}
search_pos = abs_start + 1;
} else {
break;
}
}
matches
}
Matcher::LookbehindWithSuffix {
lookbehind,
lookbehind_matcher,
suffix,
} => {
let mut matches = Vec::new();
let mut search_pos = 0;
while search_pos < text.len() {
let remaining = &text[search_pos..];
if let Some((rel_start, rel_end)) = suffix.find(remaining) {
let abs_start = search_pos + rel_start;
let abs_end = search_pos + rel_end;
if lookbehind.matches_at(text, abs_start, lookbehind_matcher) {
matches.push((abs_start, abs_end));
}
search_pos = abs_start + 1;
} else {
break;
}
}
matches
}
Matcher::PatternWithCaptures { elements, .. } => {
let mut matches = Vec::new();
let mut start_pos = 0;
while start_pos < text.len() {
let mut pos = start_pos;
let mut all_matched = true;
for element in elements {
let matcher = match element {
CompiledCaptureElement::Capture(m, _) => m,
CompiledCaptureElement::NonCapture(m) => m,
};
if let Some((rel_start, rel_end)) =
matcher.find(safe_slice(text, pos).unwrap_or(""))
{
if rel_start != 0 {
all_matched = false;
break;
}
pos += rel_end;
} else {
all_matched = false;
break;
}
}
if all_matched {
matches.push((start_pos, pos));
start_pos = pos.max(start_pos + 1); } else {
start_pos += 1;
}
}
matches
}
Matcher::Backreference(_) => {
vec![]
}
Matcher::DFA(dfa) => {
let mut matches = Vec::new();
let mut search_start = 0;
while search_start < text.len() {
if let Some((start, end)) = dfa.find(&text[search_start..]) {
let abs_start = search_start + start;
let abs_end = search_start + end;
matches.push((abs_start, abs_end));
search_start = abs_end.max(abs_start + 1);
} else {
break;
}
}
matches
}
Matcher::LazyDFA(lazy_dfa) => {
let mut dfa = lazy_dfa.clone();
let mut matches = Vec::new();
let mut search_start = 0;
while search_start < text.len() {
if let Some((start, end)) = dfa.find(&text[search_start..]) {
let abs_start = search_start + start;
let abs_end = search_start + end;
matches.push((abs_start, abs_end));
search_start = abs_end.max(abs_start + 1);
} else {
break;
}
}
matches
}
Matcher::SequenceWithFlags(seq, flags) => {
let mut matches = Vec::new();
let mut search_start = 0;
while search_start < text.len() {
if let Some((start, end)) = seq.find_with_flags(&text[search_start..], flags) {
let abs_start = search_start + start;
let abs_end = search_start + end;
matches.push((abs_start, abs_end));
search_start = abs_end.max(abs_start + 1);
} else {
break;
}
}
matches
}
Matcher::AlternationWithCaptures { branches, .. } => {
let mut matches = Vec::new();
let mut search_start = 0;
while search_start < text.len() {
let mut best_match: Option<(usize, usize)> = None;
for branch in branches {
if let Some((start, end)) = branch.find(&text[search_start..]) {
let abs_start = search_start + start;
let abs_end = search_start + end;
if best_match.is_none() || abs_start < best_match.unwrap().0 {
best_match = Some((abs_start, abs_end));
}
}
}
if let Some((start, end)) = best_match {
matches.push((start, end));
search_start = end.max(start + 1);
} else {
break;
}
}
matches
}
Matcher::CaseInsensitive(inner) => {
let bytes = text.as_bytes();
let len = bytes.len();
if len <= 256 {
let mut buf = [0u8; 256];
let mut all_ascii = true;
for i in 0..len {
let b = bytes[i];
if b >= 128 {
all_ascii = false;
break;
}
buf[i] = if b >= b'A' && b <= b'Z' { b + 32 } else { b };
}
if all_ascii {
let lower = unsafe { std::str::from_utf8_unchecked(&buf[..len]) };
return inner.find_all(lower);
}
}
let lower_text = text.to_lowercase();
inner.find_all(&lower_text)
}
}
}
#[inline]
fn digit_run_find_all(text: &str) -> Vec<(usize, usize)> {
let bytes = text.as_bytes();
let mut matches = Vec::new();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && (bytes[i] < b'0' || bytes[i] > b'9') {
i += 1;
}
if i >= bytes.len() {
break;
}
let start = i;
while i < bytes.len() && bytes[i] >= b'0' && bytes[i] <= b'9' {
i += 1;
}
matches.push((start, i));
}
matches
}
#[inline]
fn word_run_find_all(text: &str) -> Vec<(usize, usize)> {
let bytes = text.as_bytes();
let mut matches = Vec::new();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_lowercase()
|| b.is_ascii_uppercase()
|| b.is_ascii_digit()
|| b == b'_'
{
break;
}
i += 1;
}
if i >= bytes.len() {
break;
}
let start = i;
while i < bytes.len() {
let b = bytes[i];
if !(b.is_ascii_lowercase()
|| b.is_ascii_uppercase()
|| b.is_ascii_digit()
|| b == b'_')
{
break;
}
i += 1;
}
matches.push((start, i));
}
matches
}
}
fn compile_ast(ast: &Ast) -> Result<Matcher, PatternError> {
match ast {
Ast::Literal(lit) => Ok(Matcher::Literal(lit.clone())),
Ast::Dot => {
use crate::parser::charclass::CharClass;
let char_class = CharClass::parse(r"^\n")
.map_err(|e| PatternError::ParseError(format!("Dot charclass: {}", e)))?;
Ok(Matcher::CharClass(char_class))
}
Ast::Alternation(parts) => {
use aho_corasick::MatchKind;
let ac = AhoCorasick::builder()
.match_kind(MatchKind::LeftmostFirst)
.build(parts)
.map_err(|e| PatternError::ParseError(format!("Aho-Corasick: {}", e)))?;
Ok(Matcher::MultiLiteral(ac))
}
Ast::Anchored {
literal,
start,
end,
} => Ok(Matcher::AnchoredLiteral {
literal: literal.clone(),
start: *start,
end: *end,
}),
Ast::AnchoredGroup { group, start, end } => Ok(Matcher::AnchoredGroup {
group: group.clone(),
start: *start,
end: *end,
}),
Ast::AnchoredPattern { inner, start, end } => {
if let Some(literal) = literal_from_ast(inner) {
return Ok(Matcher::AnchoredLiteral {
literal,
start: *start,
end: *end,
});
}
let inner_matcher = compile_ast(inner)?;
Ok(Matcher::AnchoredPattern {
inner: Box::new(inner_matcher),
start: *start,
end: *end,
})
}
Ast::CharClass(cc) => Ok(Matcher::CharClass(cc.clone())),
Ast::Quantified(qp) => {
if let crate::parser::quantifier::Quantifier::OneOrMore = qp.quantifier {
if let crate::parser::quantifier::QuantifiedElement::CharClass(ref cc) = qp.element
{
if is_digit_charclass(cc) {
return Ok(Matcher::DigitRun);
}
if is_word_charclass(cc) {
return Ok(Matcher::WordRun);
}
}
}
Ok(Matcher::Quantified(qp.clone()))
}
Ast::Sequence(seq) => {
if let Some(dfa) = engine::dfa::DFA::try_compile(seq) {
return Ok(Matcher::DFA(dfa));
}
Ok(Matcher::Sequence(seq.clone()))
}
Ast::Group(group) => Ok(Matcher::Group(group.clone())),
Ast::Boundary(boundary_type) => Ok(Matcher::Boundary(*boundary_type)),
Ast::Lookaround(lookaround) => {
let inner_matcher = compile_ast(&lookaround.pattern)?;
Ok(Matcher::Lookaround(
Box::new(lookaround.clone()),
Box::new(inner_matcher),
))
}
Ast::Capture(inner_ast, group_index) => {
let inner_matcher = compile_ast(inner_ast)?;
Ok(Matcher::Capture(Box::new(inner_matcher), *group_index))
}
Ast::QuantifiedCapture(inner_ast, quantifier) => {
let inner_matcher = compile_ast(inner_ast)?;
Ok(Matcher::QuantifiedCapture(
Box::new(inner_matcher),
quantifier.clone(),
))
}
Ast::CombinedWithLookaround { prefix, lookaround } => {
let prefix_matcher = compile_ast(prefix)?;
let lookaround_inner = compile_ast(&lookaround.pattern)?;
Ok(Matcher::CombinedWithLookaround {
prefix: Box::new(prefix_matcher),
lookaround: Box::new(lookaround.clone()),
lookaround_matcher: Box::new(lookaround_inner),
})
}
Ast::LookbehindWithSuffix { lookbehind, suffix } => {
let lookbehind_inner = compile_ast(&lookbehind.pattern)?;
let suffix_matcher = compile_ast(suffix)?;
Ok(Matcher::LookbehindWithSuffix {
lookbehind: Box::new(lookbehind.clone()),
lookbehind_matcher: Box::new(lookbehind_inner),
suffix: Box::new(suffix_matcher),
})
}
Ast::PatternWithCaptures {
elements,
total_groups,
} => {
let mut compiled_elements = Vec::new();
for elem in elements {
match elem {
CaptureElement::Capture(ast, group_num) => {
let matcher = compile_ast(ast)?;
compiled_elements
.push(CompiledCaptureElement::Capture(matcher, *group_num));
}
CaptureElement::NonCapture(ast) => {
let matcher = compile_ast(ast)?;
compiled_elements.push(CompiledCaptureElement::NonCapture(matcher));
}
}
}
Ok(Matcher::PatternWithCaptures {
elements: compiled_elements,
total_groups: *total_groups,
})
}
Ast::AlternationWithCaptures {
branches,
total_groups,
} => {
let mut compiled_branches = Vec::new();
for branch_ast in branches {
let branch_matcher = compile_ast(branch_ast)?;
compiled_branches.push(branch_matcher);
}
Ok(Matcher::AlternationWithCaptures {
branches: compiled_branches,
total_groups: *total_groups,
})
}
Ast::Backreference(group_num) => Ok(Matcher::Backreference(*group_num)),
Ast::DotAll => {
use crate::parser::charclass::CharClass;
let mut char_class = CharClass::new();
char_class.add_range('\0', char::MAX); char_class.finalize();
Ok(Matcher::CharClass(char_class))
}
Ast::SequenceWithFlags(seq, flags) => {
Ok(Matcher::SequenceWithFlags(seq.clone(), *flags))
}
Ast::CaseInsensitive(inner) => {
let lowercased = lowercase_ast(inner);
let inner_matcher = compile_ast(&lowercased)?;
Ok(Matcher::CaseInsensitive(Box::new(inner_matcher)))
}
}
}
fn literal_from_ast(ast: &Ast) -> Option<String> {
match ast {
Ast::Literal(lit) => Some(lit.clone()),
Ast::Sequence(seq) => {
let mut literal = String::new();
for element in &seq.elements {
match element {
parser::sequence::SequenceElement::Char(ch) => literal.push(*ch),
parser::sequence::SequenceElement::Literal(lit) => literal.push_str(lit),
_ => return None,
}
}
Some(literal)
}
_ => None,
}
}
fn lowercase_ast(ast: &Ast) -> Ast {
match ast {
Ast::Literal(s) => Ast::Literal(s.to_lowercase()),
Ast::Alternation(branches) => {
Ast::Alternation(branches.iter().map(|s| s.to_lowercase()).collect())
}
Ast::Group(g) => {
let mut new_group = g.clone();
new_group.content = match &g.content {
parser::group::GroupContent::Single(s) => {
parser::group::GroupContent::Single(s.to_lowercase())
}
parser::group::GroupContent::Alternation(branches) => {
parser::group::GroupContent::Alternation(
branches.iter().map(|s| s.to_lowercase()).collect(),
)
}
parser::group::GroupContent::Sequence(seq) => {
let mut new_seq = seq.clone();
new_seq.elements = new_seq
.elements
.into_iter()
.map(|elem| match elem {
parser::sequence::SequenceElement::Literal(s) => {
parser::sequence::SequenceElement::Literal(s.to_lowercase())
}
parser::sequence::SequenceElement::Char(c) => {
let lowered: String = c.to_lowercase().collect();
if lowered.len() == 1 {
parser::sequence::SequenceElement::Char(
lowered.chars().next().unwrap(),
)
} else {
parser::sequence::SequenceElement::Literal(lowered)
}
}
other => other,
})
.collect();
parser::group::GroupContent::Sequence(new_seq)
}
parser::group::GroupContent::ParsedAlternation(sequences) => {
let new_sequences: Vec<_> = sequences
.iter()
.map(|seq| {
let mut new_seq = seq.clone();
new_seq.elements = new_seq
.elements
.into_iter()
.map(|elem| match elem {
parser::sequence::SequenceElement::Literal(s) => {
parser::sequence::SequenceElement::Literal(s.to_lowercase())
}
parser::sequence::SequenceElement::Char(c) => {
let lowered: String = c.to_lowercase().collect();
if lowered.len() == 1 {
parser::sequence::SequenceElement::Char(
lowered.chars().next().unwrap(),
)
} else {
parser::sequence::SequenceElement::Literal(lowered)
}
}
other => other,
})
.collect();
new_seq
})
.collect();
parser::group::GroupContent::ParsedAlternation(new_sequences)
}
};
Ast::Group(new_group)
}
Ast::Sequence(seq) => {
let new_elements: Vec<_> = seq
.elements
.iter()
.map(|elem| match elem {
parser::sequence::SequenceElement::Literal(s) => {
parser::sequence::SequenceElement::Literal(s.to_lowercase())
}
parser::sequence::SequenceElement::Char(c) => {
let lowered: String = c.to_lowercase().collect();
if lowered.len() == 1 {
parser::sequence::SequenceElement::Char(lowered.chars().next().unwrap())
} else {
parser::sequence::SequenceElement::Literal(lowered)
}
}
other => other.clone(),
})
.collect();
Ast::Sequence(parser::sequence::Sequence::new(new_elements))
}
Ast::Anchored {
literal,
start,
end,
} => Ast::Anchored {
literal: literal.to_lowercase(),
start: *start,
end: *end,
},
Ast::AnchoredPattern { inner, start, end } => Ast::AnchoredPattern {
inner: Box::new(lowercase_ast(inner)),
start: *start,
end: *end,
},
Ast::SequenceWithFlags(seq, flags) => {
let mut new_seq = seq.clone();
new_seq.elements = new_seq
.elements
.into_iter()
.map(|elem| match elem {
parser::sequence::SequenceElement::Literal(s) => {
parser::sequence::SequenceElement::Literal(s.to_lowercase())
}
parser::sequence::SequenceElement::Char(c) => {
let lowered: String = c.to_lowercase().collect();
if lowered.len() == 1 {
parser::sequence::SequenceElement::Char(lowered.chars().next().unwrap())
} else {
parser::sequence::SequenceElement::Literal(lowered)
}
}
other => other,
})
.collect();
Ast::SequenceWithFlags(new_seq, *flags)
}
Ast::CaseInsensitive(inner) => {
Ast::CaseInsensitive(Box::new(lowercase_ast(inner)))
}
Ast::PatternWithCaptures {
elements,
total_groups,
} => {
let new_elements = elements
.iter()
.map(|elem| match elem {
CaptureElement::NonCapture(ast) => {
CaptureElement::NonCapture(lowercase_ast(ast))
}
CaptureElement::Capture(ast, group_num) => {
CaptureElement::Capture(lowercase_ast(ast), *group_num)
}
})
.collect();
Ast::PatternWithCaptures {
elements: new_elements,
total_groups: *total_groups,
}
}
Ast::AlternationWithCaptures {
branches,
total_groups,
} => Ast::AlternationWithCaptures {
branches: branches.iter().map(lowercase_ast).collect(),
total_groups: *total_groups,
},
Ast::Capture(inner, group_index) => {
Ast::Capture(Box::new(lowercase_ast(inner)), *group_index)
}
_ => ast.clone(),
}
}
fn quantifier_bounds(q: &parser::quantifier::Quantifier) -> (usize, usize) {
use parser::quantifier::Quantifier;
match q {
Quantifier::ZeroOrMore | Quantifier::ZeroOrMoreLazy => (0, usize::MAX),
Quantifier::OneOrMore | Quantifier::OneOrMoreLazy => (1, usize::MAX),
Quantifier::ZeroOrOne | Quantifier::ZeroOrOneLazy => (0, 1),
Quantifier::Exactly(n) => (*n, *n),
Quantifier::AtLeast(n) => (*n, usize::MAX),
Quantifier::Between(n, m) => (*n, *m),
}
}
fn is_digit_charclass(cc: &CharClass) -> bool {
cc.ranges.len() == 1 && cc.ranges[0] == ('0', '9') && cc.chars.is_empty() && !cc.negated
}
fn is_word_charclass(cc: &CharClass) -> bool {
if cc.negated || cc.ranges.len() != 3 {
return false;
}
let mut has_lower = false;
let mut has_upper = false;
let mut has_digit = false;
for &(start, end) in &cc.ranges {
if start == 'a' && end == 'z' {
has_lower = true;
} else if start == 'A' && end == 'Z' {
has_upper = true;
} else if start == '0' && end == '9' {
has_digit = true;
}
}
has_lower && has_upper && has_digit && cc.chars.len() == 1 && cc.chars[0] == '_'
}
fn parse_lookaround(pattern: &str, depth: usize) -> Result<Ast, PatternError> {
let lookaround_type = if pattern.starts_with("(?=") {
LookaroundType::PositiveLookahead
} else if pattern.starts_with("(?!") {
LookaroundType::NegativeLookahead
} else if pattern.starts_with("(?<=") {
LookaroundType::PositiveLookbehind
} else if pattern.starts_with("(?<!") {
LookaroundType::NegativeLookbehind
} else {
return Err(PatternError::ParseError(
"Invalid lookaround syntax".to_string(),
));
};
let prefix_len = if pattern.starts_with("(?<=") || pattern.starts_with("(?<!") {
4 } else {
3 };
if let Some(close_idx) = find_matching_paren(pattern, 0) {
let inner = &pattern[prefix_len..close_idx];
let inner_ast = parse_pattern_with_depth(inner, depth + 1)?;
if close_idx != pattern.len() - 1 {
let suffix = &pattern[close_idx + 1..];
let suffix_ast = parse_pattern_with_depth(suffix, depth + 1)?;
if matches!(
lookaround_type,
LookaroundType::PositiveLookbehind | LookaroundType::NegativeLookbehind
) {
let lookbehind = Lookaround::new(lookaround_type, inner_ast);
return Ok(Ast::LookbehindWithSuffix {
lookbehind,
suffix: Box::new(suffix_ast),
});
} else {
return Err(PatternError::ParseError(
"Lookahead cannot have suffix pattern after it".to_string(),
));
}
}
Ok(Ast::Lookaround(Lookaround::new(lookaround_type, inner_ast)))
} else {
Err(PatternError::ParseError(
"Unmatched parenthesis in lookaround".to_string(),
))
}
}
fn parse_combined_with_lookaround(pattern: &str, depth: usize) -> Result<Ast, PatternError> {
let lookaround_patterns = ["(?=", "(?!", "(?<=", "(?<!"];
for lookaround_start in lookaround_patterns {
if let Some(pos) = pattern.find(lookaround_start) {
if pos == 0 {
continue;
}
let prefix = &pattern[..pos];
let lookaround_part = &pattern[pos..];
let prefix_ast = parse_pattern_with_depth(prefix, depth + 1)?;
let lookaround_type = if lookaround_start == "(?=" {
LookaroundType::PositiveLookahead
} else if lookaround_start == "(?!" {
LookaroundType::NegativeLookahead
} else if lookaround_start == "(?<=" {
LookaroundType::PositiveLookbehind
} else {
LookaroundType::NegativeLookbehind
};
let prefix_len = lookaround_start.len();
if let Some(close_idx) = find_matching_paren(lookaround_part, 0) {
if close_idx != lookaround_part.len() - 1 {
return Err(PatternError::ParseError(
"Extra characters after lookaround".to_string(),
));
}
let inner = &lookaround_part[prefix_len..close_idx];
let inner_ast = parse_pattern_with_depth(inner, depth + 1)?;
let lookaround = Lookaround::new(lookaround_type, inner_ast);
return Ok(Ast::CombinedWithLookaround {
prefix: Box::new(prefix_ast),
lookaround,
});
} else {
return Err(PatternError::ParseError(
"Unmatched parenthesis in lookaround".to_string(),
));
}
}
}
Err(PatternError::ParseError(
"No lookaround found in pattern".to_string(),
))
}
fn contains_unescaped_paren(pattern: &str) -> bool {
let bytes = pattern.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
i += 2; } else if bytes[i] == b'[' {
i += 1;
if i < bytes.len() && bytes[i] == b'^' {
i += 1;
}
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2;
} else if bytes[i] == b']' {
i += 1;
break;
} else {
i += 1;
}
}
} else if bytes[i] == b'(' || bytes[i] == b')' {
return true;
} else {
i += 1;
}
}
false
}
fn find_matching_paren(pattern: &str, start: usize) -> Option<usize> {
let bytes = pattern.as_bytes();
if start >= bytes.len() || bytes[start] != b'(' {
return None;
}
let mut depth = 0;
let mut i = 0;
while i < bytes[start..].len() {
match bytes[start + i] {
b'\\' => {
i += 2;
continue;
}
b'[' => {
i += 1;
if i < bytes[start..].len() && bytes[start + i] == b'^' {
i += 1;
}
while i < bytes[start..].len() {
if bytes[start + i] == b'\\' {
i += 2; } else if bytes[start + i] == b']' {
i += 1;
break;
} else {
i += 1;
}
}
continue;
}
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(start + i);
}
}
_ => {}
}
i += 1;
}
None }
fn parse_pattern_with_captures(pattern: &str) -> Result<Ast, PatternError> {
let mut group_counter = 1;
let (ast, _total_groups) = parse_pattern_with_captures_inner(pattern, &mut group_counter)?;
Ok(ast)
}
fn split_by_alternation(pattern: &str) -> Option<Vec<String>> {
let mut branches = Vec::new();
let mut current = String::new();
let mut depth = 0;
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
current.push(ch);
if let Some(next) = chars.next() {
current.push(next);
}
}
'[' => {
current.push(ch);
if chars.peek() == Some(&'^') {
current.push(chars.next().unwrap());
}
while let Some(c) = chars.next() {
current.push(c);
if c == '\\' {
if let Some(next) = chars.next() {
current.push(next);
}
} else if c == ']' {
break;
}
}
}
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth -= 1;
current.push(ch);
}
'|' if depth == 0 => {
branches.push(current.clone());
current.clear();
}
_ => {
current.push(ch);
}
}
}
if !current.is_empty() || !branches.is_empty() {
branches.push(current);
}
if branches.len() > 1 {
Some(branches)
} else {
None
}
}
fn parse_pattern_with_captures_inner(
pattern: &str,
group_counter: &mut usize,
) -> Result<(Ast, usize), PatternError> {
if let Some(branches) = split_by_alternation(pattern) {
let _start_group = *group_counter;
let mut parsed_branches = Vec::new();
for branch in branches {
let (branch_ast, _) = parse_pattern_with_captures_inner(&branch, group_counter)?;
parsed_branches.push(branch_ast);
}
let total_groups = *group_counter - 1;
let all_literals = parsed_branches
.iter()
.all(|ast| matches!(ast, Ast::Literal(_)));
if all_literals {
let literals: Vec<String> = parsed_branches
.into_iter()
.filter_map(|ast| {
if let Ast::Literal(s) = ast {
Some(s)
} else {
None
}
})
.collect();
return Ok((Ast::Alternation(literals), total_groups));
} else {
let mut sequences = Vec::new();
for branch_ast in &parsed_branches {
if let Ast::Sequence(seq) = branch_ast {
sequences.push(seq.clone());
} else {
break;
}
}
if sequences.len() == parsed_branches.len() {
use crate::parser::group::{Group, GroupContent};
return Ok((
Ast::Group(Group::new_non_capturing(GroupContent::ParsedAlternation(
sequences,
))),
total_groups,
));
} else {
return Ok((
Ast::AlternationWithCaptures {
branches: parsed_branches,
total_groups,
},
total_groups,
));
}
}
}
let mut elements: Vec<CaptureElement> = Vec::new();
let mut pos = 0;
let start_group = *group_counter;
while pos < pattern.len() {
if pattern[pos..].starts_with("(?:") {
if let Some(close_idx) = find_matching_paren(pattern, pos) {
let inner = &pattern[pos + 3..close_idx]; let (inner_ast, _) = parse_pattern_with_captures_inner(inner, group_counter)?;
let mut after_group = close_idx + 1;
let mut quantifier: Option<parser::quantifier::Quantifier> = None;
if after_group < pattern.len() {
let remaining = &pattern[after_group..];
let chars: Vec<char> = remaining.chars().take(2).collect();
if !chars.is_empty() {
let first = chars[0];
let has_lazy = chars.len() > 1 && chars[1] == '?';
match first {
'*' if has_lazy => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrMoreLazy);
after_group += 2;
}
'*' => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrMore);
after_group += 1;
}
'+' if has_lazy => {
quantifier = Some(parser::quantifier::Quantifier::OneOrMoreLazy);
after_group += 2;
}
'+' => {
quantifier = Some(parser::quantifier::Quantifier::OneOrMore);
after_group += 1;
}
'?' if has_lazy => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrOneLazy);
after_group += 2;
}
'?' => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrOne);
after_group += 1;
}
_ => {}
}
}
}
if let Some(q) = quantifier {
elements.push(CaptureElement::NonCapture(Ast::QuantifiedCapture(
Box::new(inner_ast),
q,
)));
} else {
elements.push(CaptureElement::NonCapture(inner_ast));
}
pos = after_group;
} else {
return Err(PatternError::ParseError(
"Unmatched parenthesis".to_string(),
));
}
} else if pattern[pos..].starts_with('(') && !pattern[pos..].starts_with("(?") {
if let Some(close_idx) = find_matching_paren(pattern, pos) {
let my_group_num = *group_counter;
*group_counter += 1;
let inner = &pattern[pos + 1..close_idx];
let (inner_ast, _) = parse_pattern_with_captures_inner(inner, group_counter)?;
let mut after_group = close_idx + 1;
let mut quantifier: Option<parser::quantifier::Quantifier> = None;
if after_group < pattern.len() {
let remaining = &pattern[after_group..];
let chars: Vec<char> = remaining.chars().take(2).collect();
if !chars.is_empty() {
let first = chars[0];
let has_lazy = chars.len() > 1 && chars[1] == '?';
match first {
'*' if has_lazy => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrMoreLazy);
after_group += 2;
}
'*' => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrMore);
after_group += 1;
}
'+' if has_lazy => {
quantifier = Some(parser::quantifier::Quantifier::OneOrMoreLazy);
after_group += 2;
}
'+' => {
quantifier = Some(parser::quantifier::Quantifier::OneOrMore);
after_group += 1;
}
'?' if has_lazy => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrOneLazy);
after_group += 2;
}
'?' => {
quantifier = Some(parser::quantifier::Quantifier::ZeroOrOne);
after_group += 1;
}
_ => {}
}
}
}
if let Some(q) = quantifier {
elements.push(CaptureElement::Capture(
Ast::QuantifiedCapture(Box::new(inner_ast), q),
my_group_num,
));
} else {
elements.push(CaptureElement::Capture(inner_ast, my_group_num));
}
pos = after_group;
} else {
return Err(PatternError::ParseError(
"Unmatched parenthesis".to_string(),
));
}
} else {
if pattern[pos..].starts_with('\\') && pos + 1 < pattern.len() {
let next_char = pattern.chars().nth(pos + 1);
if let Some(ch) = next_char {
if ch.is_ascii_digit() {
let digit = ch.to_digit(10).unwrap() as usize;
elements.push(CaptureElement::NonCapture(Ast::Backreference(digit)));
pos += 2; continue;
}
}
}
let next_paren = {
let mut search_pos = pos;
let mut result = pattern.len();
let bytes = pattern.as_bytes();
while search_pos < bytes.len() {
if bytes[search_pos] == b'\\' && search_pos + 1 < bytes.len() {
search_pos += 2; } else if bytes[search_pos] == b'[' {
search_pos += 1;
if search_pos < bytes.len() && bytes[search_pos] == b'^' {
search_pos += 1;
}
while search_pos < bytes.len() {
if bytes[search_pos] == b'\\' {
search_pos += 2;
} else if bytes[search_pos] == b']' {
search_pos += 1;
break;
} else {
search_pos += 1;
}
}
} else if bytes[search_pos] == b'(' {
result = search_pos;
break;
} else {
search_pos += 1;
}
}
result
};
let mut search_pos = pos;
let mut next_backref = pattern.len();
while search_pos < pattern.len() {
if pattern[search_pos..].starts_with('\\') && search_pos + 1 < pattern.len() {
let next_ch = pattern.chars().nth(search_pos + 1);
if next_ch.map(|c| c.is_ascii_digit()).unwrap_or(false) {
next_backref = search_pos;
break;
}
search_pos += 2; } else {
search_pos += 1;
}
}
let next_boundary = next_paren.min(next_backref);
if next_boundary > pos {
let segment = &pattern[pos..next_boundary];
let segment_ast = if segment.is_empty() {
Ast::Literal(String::new())
} else {
parse_pattern(segment)?
};
elements.push(CaptureElement::NonCapture(segment_ast));
pos = next_boundary;
} else {
pos += 1;
}
}
}
let total_groups = *group_counter - 1;
if elements.len() == 1 {
if let CaptureElement::Capture(ast, num) = &elements[0] {
return Ok((Ast::Capture(Box::new(ast.clone()), *num), total_groups));
}
}
Ok((
Ast::PatternWithCaptures {
elements,
total_groups,
},
*group_counter - start_group,
))
}
#[derive(Debug, Clone, PartialEq)]
enum CaptureElement {
Capture(Ast, usize), NonCapture(Ast), }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal() {
let p = Pattern::new("hello").unwrap();
assert!(p.is_match("hello world"));
assert!(!p.is_match("goodbye"));
}
#[test]
fn alternation() {
let p = Pattern::new("foo|bar|baz").unwrap();
assert!(p.is_match("foo"));
assert!(p.is_match("bar"));
assert!(!p.is_match("qux"));
}
#[test]
fn anchors() {
let p = Pattern::new("^hello$").unwrap();
assert!(p.is_match("hello"));
assert!(!p.is_match("hello world"));
}
#[test]
fn find_test() {
let p = Pattern::new("world").unwrap();
assert_eq!(p.find("hello world"), Some((6, 11)));
}
#[test]
fn cached() {
assert!(is_match("test", "this is a test").unwrap());
}
}
#[test]
fn char_class_simple() {
let p = Pattern::new("[abc]").unwrap();
assert!(p.is_match("a"));
assert!(p.is_match("apple"));
assert!(p.is_match("cab"));
assert!(!p.is_match("xyz"));
}
#[test]
fn char_class_range() {
let p = Pattern::new("[a-z]").unwrap();
assert!(p.is_match("hello"));
assert!(p.is_match("xyz"));
assert!(!p.is_match("HELLO"));
assert!(!p.is_match("123"));
}
#[test]
fn char_class_multiple_ranges() {
let p = Pattern::new("[a-zA-Z0-9]").unwrap();
assert!(p.is_match("hello"));
assert!(p.is_match("WORLD"));
assert!(p.is_match("test123"));
assert!(!p.is_match("!!!"));
}
#[test]
fn char_class_negated() {
let p = Pattern::new("[^0-9]").unwrap();
assert!(p.is_match("abc"));
assert!(!p.is_match("123"));
assert!(p.is_match("a1b")); }
#[test]
fn char_class_find() {
let p = Pattern::new("[0-9]").unwrap();
assert_eq!(p.find("abc123"), Some((3, 4)));
let matches = p.find_all("a1b2c3");
assert_eq!(matches, vec![(1, 2), (3, 4), (5, 6)]);
}
#[test]
fn debug_parse_group() {
let pattern = "(foo|bar)+";
match parser::group::parse_group(pattern) {
Ok((group, bytes_consumed)) => {
eprintln!("bytes_consumed: {}", bytes_consumed);
eprintln!("pattern.len(): {}", pattern.len());
eprintln!("group: {:?}", group);
assert_eq!(
bytes_consumed,
pattern.len(),
"Group should consume entire pattern"
);
}
Err(e) => {
panic!("Error: {}", e);
}
}
eprintln!("\n--- Testing Pattern::new ---");
let re = Pattern::new(pattern).unwrap();
eprintln!("Pattern created: {:?}", re);
eprintln!("is_match('foo'): {}", re.is_match("foo"));
eprintln!("is_match('bar'): {}", re.is_match("bar"));
eprintln!("is_match('foobar'): {}", re.is_match("foobar"));
}