use std::ops::Range;
use regex_automata::nfa::thompson::{self, pikevm};
use regex_automata::util::syntax;
use regex_automata::{Anchored, Input, MatchKind, meta};
use crate::posix_regex::{Flavor, literal, translate};
const NFA_SIZE_LIMIT: usize = 8 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum Syntax {
Fixed,
#[default]
Basic,
Extended,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Expression {
Pattern(Vec<u8>),
Not(Box<Expression>),
And(Box<Expression>, Box<Expression>),
Or(Box<Expression>, Box<Expression>),
}
impl Expression {
pub fn pattern(text: impl Into<Vec<u8>>) -> Self {
Self::Pattern(text.into())
}
#[must_use]
pub fn and(self, other: Self) -> Self {
Self::And(Box::new(self), Box::new(other))
}
#[must_use]
pub fn or(self, other: Self) -> Self {
Self::Or(Box::new(self), Box::new(other))
}
}
impl std::ops::Not for Expression {
type Output = Self;
fn not(self) -> Self {
Self::Not(Box::new(self))
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Matching {
syntax: Syntax,
ignore_case: bool,
word: bool,
bytes: bool,
}
impl Matching {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_syntax(mut self, syntax: Syntax) -> Self {
self.syntax = syntax;
self
}
#[must_use]
pub fn with_ignore_case(mut self, ignore_case: bool) -> Self {
self.ignore_case = ignore_case;
self
}
#[must_use]
pub fn with_bytes(mut self, bytes: bool) -> Self {
self.bytes = bytes;
self
}
#[must_use]
pub fn with_word(mut self, word: bool) -> Self {
self.word = word;
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PatternError {
#[error("grep pattern is not UTF-8")]
Encoding,
#[error("unsupported grep pattern construct: {0}")]
Unsupported(&'static str),
#[error("invalid grep pattern")]
Invalid(#[source] Box<dyn std::error::Error + Send + Sync>),
}
#[derive(Debug)]
pub struct Query {
node: Node,
leaves: Vec<Leaf>,
word: bool,
all_match: bool,
}
#[derive(Debug)]
pub(super) enum Node {
Leaf(usize),
Not(Box<Node>),
And(Box<Node>, Box<Node>),
Or(Box<Node>, Box<Node>),
}
#[derive(Debug)]
struct Leaf {
find: meta::Regex,
longest: pikevm::PikeVM,
negated: bool,
}
impl Query {
pub fn new(expression: &Expression, matching: Matching) -> Result<Self, PatternError> {
let mut leaves = Vec::new();
let node = compile(expression, matching, false, &mut leaves)?;
Ok(Self {
node,
leaves,
word: matching.word,
all_match: false,
})
}
#[must_use]
pub fn with_all_match(mut self, all_match: bool) -> Self {
self.all_match = all_match;
self
}
#[must_use]
pub fn is_match(&self, line: &[u8]) -> bool {
self.eval(&self.node, line)
}
#[must_use]
pub fn matches(&self, line: &[u8]) -> Vec<Range<usize>> {
let mut found = Vec::new();
let mut caches: Vec<_> = self.leaves.iter().map(|_| None).collect();
let mut at = 0;
while at <= line.len() {
let best = self
.leaves
.iter()
.zip(&mut caches)
.filter(|(leaf, _)| !leaf.negated)
.filter_map(|(leaf, cache)| self.find(leaf, cache, line, at))
.min_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
let Some(span) = best.filter(|span| !span.is_empty()) else {
break;
};
at = span.end;
found.push(span);
}
found
}
pub(super) fn branches(&self) -> Vec<&Node> {
fn collect<'a>(node: &'a Node, out: &mut Vec<&'a Node>) {
match node {
Node::Or(left, right) => {
collect(left, out);
collect(right, out);
}
other => out.push(other),
}
}
let mut out = Vec::new();
collect(&self.node, &mut out);
out
}
pub(super) fn all_match(&self) -> bool {
self.all_match
}
pub(super) fn eval(&self, node: &Node, line: &[u8]) -> bool {
match node {
Node::Leaf(index) => self
.leaves
.get(*index)
.and_then(|leaf| self.find(leaf, &mut None, line, 0))
.is_some(),
Node::Not(inner) => !self.eval(inner, line),
Node::And(left, right) => self.eval(left, line) && self.eval(right, line),
Node::Or(left, right) => self.eval(left, line) || self.eval(right, line),
}
}
pub(super) fn skip_to(&self, haystack: &[u8], from: usize) -> Option<usize> {
if has_and(&self.node) {
return Some(from);
}
self.leaves
.iter()
.filter_map(|leaf| {
leaf.find
.search(&Input::new(haystack).range(from..))
.map(|found| found.start())
})
.min()
}
fn find(
&self,
leaf: &Leaf,
cache: &mut Option<pikevm::Cache>,
line: &[u8],
mut at: usize,
) -> Option<Range<usize>> {
loop {
let found = leaf.find.search(&Input::new(line).range(at..))?;
let start = found.start();
let cache = cache.get_or_insert_with(|| leaf.longest.create_cache());
let input = Input::new(line).range(start..).anchored(Anchored::Yes);
let end = leaf
.longest
.find(cache, input)
.map_or(found.end(), |longest| longest.end().max(found.end()));
if !self.word || is_word_bounded(line, start, end) {
return Some(start..end);
}
at = start + 1;
if at > line.len() {
return None;
}
}
}
}
fn has_and(node: &Node) -> bool {
match node {
Node::Leaf(_) => false,
Node::Not(_) | Node::And(..) => true,
Node::Or(left, right) => has_and(left) || has_and(right),
}
}
fn is_word_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
fn is_word_bounded(line: &[u8], start: usize, end: usize) -> bool {
let before = start
.checked_sub(1)
.and_then(|index| line.get(index))
.is_some_and(|&byte| is_word_byte(byte));
let after = line.get(end).is_some_and(|&byte| is_word_byte(byte));
!before && !after
}
fn compile(
expression: &Expression,
matching: Matching,
negated: bool,
leaves: &mut Vec<Leaf>,
) -> Result<Node, PatternError> {
Ok(match expression {
Expression::Pattern(text) => {
let mut node = None;
for line in text.split(|&byte| byte == b'\n') {
leaves.push(leaf(line, matching, negated)?);
let next = Node::Leaf(leaves.len() - 1);
node = Some(match node {
None => next,
Some(previous) => Node::Or(Box::new(previous), Box::new(next)),
});
}
node.unwrap_or(Node::Leaf(0))
}
Expression::Not(inner) => Node::Not(Box::new(compile(inner, matching, !negated, leaves)?)),
Expression::And(left, right) => Node::And(
Box::new(compile(left, matching, negated, leaves)?),
Box::new(compile(right, matching, negated, leaves)?),
),
Expression::Or(left, right) => Node::Or(
Box::new(compile(left, matching, negated, leaves)?),
Box::new(compile(right, matching, negated, leaves)?),
),
})
}
fn leaf(text: &[u8], matching: Matching, negated: bool) -> Result<Leaf, PatternError> {
let text = std::str::from_utf8(text).map_err(|_| PatternError::Encoding)?;
let expression = match matching.syntax {
Syntax::Fixed => {
let mut out = String::with_capacity(text.len());
text.chars().for_each(|c| literal(c, &mut out));
out
}
Syntax::Basic => translate(text, Flavor::Basic).map_err(PatternError::Unsupported)?,
Syntax::Extended => translate(text, Flavor::Extended).map_err(PatternError::Unsupported)?,
};
let syntax = syntax::Config::new()
.case_insensitive(matching.ignore_case)
.multi_line(true)
.unicode(!matching.bytes)
.utf8(!matching.bytes);
let invalid = |error: Box<dyn std::error::Error + Send + Sync>| PatternError::Invalid(error);
let find = meta::Regex::builder()
.syntax(syntax)
.configure(meta::Config::new().nfa_size_limit(Some(NFA_SIZE_LIMIT)))
.build(&expression)
.map_err(|error| invalid(Box::new(error)))?;
let longest = pikevm::PikeVM::builder()
.syntax(syntax)
.thompson(thompson::Config::new().nfa_size_limit(Some(NFA_SIZE_LIMIT)))
.configure(pikevm::Config::new().match_kind(MatchKind::All))
.build(&expression)
.map_err(|error| invalid(Box::new(error)))?;
Ok(Leaf {
find,
longest,
negated,
})
}