use memchr::memmem::Finder;
use crate::chars::char_offset;
use crate::search::{find_lit, lit_eq};
pub(crate) const ANY_MANY: u8 = 0xFF;
pub(crate) const ANY_ONE: u8 = 0xFE;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LikePattern(Vec<u8>);
impl LikePattern {
pub fn encode(sql: &str, escape: Option<char>) -> Option<Self> {
let mut out = Vec::with_capacity(sql.len());
let mut chars = sql.chars();
while let Some(c) = chars.next() {
let lit = match c {
_ if Some(c) == escape => chars.next()?,
'%' => {
out.push(ANY_MANY);
continue;
}
'_' => {
out.push(ANY_ONE);
continue;
}
_ => c,
};
out.extend_from_slice(lit.encode_utf8(&mut [0; 4]).as_bytes());
}
Some(LikePattern(out))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub(crate) fn is_encoding(bytes: &[u8]) -> bool {
bytes.split(is_wildcard).all(|text| std::str::from_utf8(text).is_ok())
}
}
fn is_wildcard(b: &u8) -> bool {
matches!(*b, ANY_MANY | ANY_ONE)
}
pub(crate) struct LikeMatcher {
kind: LikeKind,
ci: bool,
}
#[allow(clippy::large_enum_variant)]
enum LikeKind {
Exact(Vec<u8>),
Prefix(Vec<u8>),
Suffix(Vec<u8>),
Contains(Finder<'static>),
Generic(Vec<LikeTok>),
}
#[allow(clippy::large_enum_variant)]
enum LikeTok {
Lit(Finder<'static>),
AnyOne,
SkipTo(Finder<'static>),
AnyRest,
}
fn tokenize(pattern: &[u8], ci: bool) -> Vec<LikeTok> {
let finder = |lit: &[u8]| match ci {
true => Finder::new(&lit.to_ascii_lowercase()).into_owned(),
false => Finder::new(lit).into_owned(),
};
let mut toks = Vec::new();
let mut rest = pattern;
while !rest.is_empty() {
let (wild, tail) = rest.split_at(rest.iter().take_while(|b| is_wildcard(b)).count());
let (lit, tail) = tail.split_at(tail.iter().position(is_wildcard).unwrap_or(tail.len()));
toks.extend(wild.iter().filter(|&&b| b == ANY_ONE).map(|_| LikeTok::AnyOne));
match (wild.contains(&ANY_MANY), lit.is_empty()) {
(true, true) => toks.push(LikeTok::AnyRest),
(true, false) => toks.push(LikeTok::SkipTo(finder(lit))),
(false, true) => {}
(false, false) => toks.push(LikeTok::Lit(finder(lit))),
}
rest = tail;
}
toks
}
fn specialize(toks: Vec<LikeTok>) -> LikeKind {
use LikeTok::{AnyRest, Lit, SkipTo};
match toks.as_slice() {
[] => LikeKind::Exact(Vec::new()),
[Lit(l)] => LikeKind::Exact(l.needle().to_vec()),
[Lit(l), AnyRest] => LikeKind::Prefix(l.needle().to_vec()),
[AnyRest] => LikeKind::Prefix(Vec::new()),
[SkipTo(l)] => LikeKind::Suffix(l.needle().to_vec()),
[SkipTo(l), AnyRest] => LikeKind::Contains(l.clone()),
_ => LikeKind::Generic(toks),
}
}
impl LikeMatcher {
pub(crate) fn compile(pattern: &[u8], ci: bool) -> Self {
LikeMatcher {
kind: specialize(tokenize(pattern, ci)),
ci,
}
}
#[inline]
pub(crate) fn matches(&self, h: &[u8], folded: &mut Vec<u8>) -> bool {
let ci = self.ci;
match &self.kind {
LikeKind::Exact(l) => anchored_eq(h, l, ci),
LikeKind::Prefix(l) => h.len() >= l.len() && anchored_eq(&h[..l.len()], l, ci),
LikeKind::Suffix(l) => h.len() >= l.len() && anchored_eq(&h[h.len() - l.len()..], l, ci),
LikeKind::Contains(f) => find_lit(fold(ci, h, folded), f).is_some(),
LikeKind::Generic(toks) => generic_match(toks, fold(ci, h, folded)),
}
}
}
#[inline]
fn anchored_eq(w: &[u8], l: &[u8], ci: bool) -> bool {
if ci {
w.eq_ignore_ascii_case(l)
} else {
lit_eq(w, l)
}
}
#[inline]
fn fold<'a>(ci: bool, h: &'a [u8], folded: &'a mut Vec<u8>) -> &'a [u8] {
if !ci {
return h;
}
folded.resize(h.len(), 0);
for (d, s) in folded.iter_mut().zip(h) {
*d = s.to_ascii_lowercase();
}
folded
}
fn generic_match(toks: &[LikeTok], h: &[u8]) -> bool {
let n = h.len();
let (mut ti, mut hi) = (0usize, 0usize);
let mut anchor: Option<(&Finder<'static>, usize, usize)> = None;
loop {
match toks.get(ti) {
Some(LikeTok::AnyRest) => return true,
Some(LikeTok::SkipTo(l)) => anchor = Some((l, ti + 1, hi)),
Some(LikeTok::AnyOne) if hi < n => {
hi += char_offset(&h[hi..], 1);
ti += 1;
continue;
}
Some(LikeTok::Lit(l)) => {
let l = l.needle();
if h.get(hi..hi + l.len()).is_some_and(|w| lit_eq(w, l)) {
hi += l.len();
ti += 1;
continue;
}
}
Some(LikeTok::AnyOne) => {}
None if hi == n => return true,
None => {}
}
let Some((l, next, from)) = &mut anchor else {
return false;
};
let Some(k) = find_lit(&h[*from..], l) else {
return false;
};
let at = *from + k;
*from = at + 1;
(ti, hi) = (*next, at + l.needle().len());
}
}
#[cfg(test)]
#[path = "tests/like.rs"]
mod tests;