use bitflags::bitflags;bitflags! { /// The match mode employed in [`Pattern::matches()`][crate::Pattern::matches()]. #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct Mode: u8 { /// Let globs like `*` and `?` not match the slash `/` literal, which is useful when matching paths. const NO_MATCH_SLASH_LITERAL = 1 << 0; /// Match case insensitively for ascii characters only. const IGNORE_CASE = 1 << 1; }} pub(crate) mod function { use bstr::{BStr, ByteSlice}; use crate::wildmatch::Mode; #[derive(Eq, PartialEq)] enum Result { Match, NoMatch, AbortAll, AbortToStarStar, } const STAR: u8 = b'*'; const BACKSLASH: u8 = b'\\'; const SLASH: u8 = b'/'; const BRACKET_OPEN: u8 = b'['; const BRACKET_CLOSE: u8 = b']'; const COLON: u8 = b':'; const NEGATE_CLASS: u8 = b'!'; fn match_recursive(pattern: &BStr, text: &BStr, mode: Mode) -> Result { use self::Result::*; let possibly_lowercase = |c: &u8| { if mode.contains(Mode::IGNORE_CASE) { c.to_ascii_lowercase() } else { *c } }; let mut p = pattern.iter().map(possibly_lowercase).enumerate().peekable(); let mut t = text.iter().map(possibly_lowercase).enumerate(); while let Some((mut p_idx, mut p_ch)) = p.next() { let (mut t_idx, mut t_ch) = match t.next() { Some(c) => c, None if p_ch != STAR => return AbortAll, None => (text.len(), 0), }; if p_ch == BACKSLASH { match p.next() { Some((_p_idx, p_ch)) => { if p_ch != t_ch { return NoMatch; } else { continue; } } None => return NoMatch, }; } match p_ch { b'?' => { if mode.contains(Mode::NO_MATCH_SLASH_LITERAL) && t_ch == SLASH { return NoMatch; } else { continue; } } STAR => { let mut match_slash = mode .contains(Mode::NO_MATCH_SLASH_LITERAL) .then(|| false) .unwrap_or(true); match p.next() { Some((next_p_idx, next_p_ch)) => { let next; if next_p_ch == STAR { let leading_slash_idx = p_idx.checked_sub(1); while p.next_if(|(_, c)| *c == STAR).is_some() {} next = p.next(); if !mode.contains(Mode::NO_MATCH_SLASH_LITERAL) { match_slash = true; } else if leading_slash_idx.map_or(true, |idx| pattern[idx] == SLASH) && next.map_or(true, |(_, c)| { c == SLASH || (c == BACKSLASH && p.peek().map(|t| t.1) == Some(SLASH)) }) { if next.map_or(NoMatch, |(idx, _)| { match_recursive(pattern[idx + 1..].as_bstr(), text[t_idx..].as_bstr(), mode) }) == Match { return Match; } match_slash = true; } else { match_slash = false; } } else { next = Some((next_p_idx, next_p_ch)); } match next { None => { return if !match_slash && text[t_idx..].contains(&SLASH) { NoMatch } else { Match }; } Some((next_p_idx, next_p_ch)) => { p_idx = next_p_idx; p_ch = next_p_ch; if !match_slash && p_ch == SLASH { match text[t_idx..].find_byte(SLASH) { Some(distance_to_slash) => { for _ in t.by_ref().take(distance_to_slash) {} continue; } None => return NoMatch, } } } } } None => { return if !match_slash && text[t_idx..].contains(&SLASH) { NoMatch } else { Match } } } return loop { if !crate::parse::GLOB_CHARACTERS.contains(&p_ch) { loop { if (!match_slash && t_ch == SLASH) || t_ch == p_ch { break; } match t.next() { Some(t) => { t_idx = t.0; t_ch = t.1; } None => break, }; } if t_ch != p_ch { return NoMatch; } } let res = match_recursive(pattern[p_idx..].as_bstr(), text[t_idx..].as_bstr(), mode); if res != NoMatch { if !match_slash || res != AbortToStarStar { return res; } } else if !match_slash && t_ch == SLASH { return AbortToStarStar; } match t.next() { Some(t) => { t_idx = t.0; t_ch = t.1; } None => break AbortAll, }; }; } BRACKET_OPEN => { match p.next() { Some(t) => { p_idx = t.0; p_ch = t.1; } None => return AbortAll, }; if p_ch == b'^' { p_ch = NEGATE_CLASS; } let negated = p_ch == NEGATE_CLASS; let mut next = if negated { p.next() } else { Some((p_idx, p_ch)) }; let mut prev_p_ch = 0; let mut matched = false; loop { match next { None => return AbortAll, Some((p_idx, mut p_ch)) => match p_ch { BACKSLASH => match p.next() { Some((_, p_ch)) => { if p_ch == t_ch { matched = true } else { prev_p_ch = p_ch; } } None => return AbortAll, }, b'-' if prev_p_ch != 0 && p.peek().is_some() && p.peek().map(|t| t.1) != Some(BRACKET_CLOSE) => { p_ch = p.next().expect("peeked").1; if p_ch == BACKSLASH { p_ch = match p.next() { Some(t) => t.1, None => return AbortAll, }; } if t_ch <= p_ch && t_ch >= prev_p_ch { matched = true; } else if mode.contains(Mode::IGNORE_CASE) && t_ch.is_ascii_lowercase() { let t_ch_upper = t_ch.to_ascii_uppercase(); if (t_ch_upper <= p_ch.to_ascii_uppercase() && t_ch_upper >= prev_p_ch.to_ascii_uppercase()) || (t_ch_upper <= prev_p_ch.to_ascii_uppercase() && t_ch_upper >= p_ch.to_ascii_uppercase()) { matched = true; } } prev_p_ch = 0; } BRACKET_OPEN if matches!(p.peek(), Some((_, COLON))) => { p.next(); while p.peek().map_or(false, |t| t.1 != BRACKET_CLOSE) { p.next(); } let closing_bracket_idx = match p.next() { Some((idx, _)) => idx, None => return AbortAll, }; const BRACKET__COLON__BRACKET: usize = 3; if closing_bracket_idx - p_idx < BRACKET__COLON__BRACKET || pattern[closing_bracket_idx - 1] != COLON { if t_ch == BRACKET_OPEN { matched = true } p = pattern[p_idx + 1..] .iter() .map(possibly_lowercase) .enumerate() .peekable(); } else { let class = &pattern.as_ref()[p_idx + 2..closing_bracket_idx - 1]; match class { b"alnum" => { if t_ch.is_ascii_alphanumeric() { matched = true; } } b"alpha" => { if t_ch.is_ascii_alphabetic() { matched = true; } } b"blank" => { if t_ch.is_ascii_whitespace() { matched = true; } } b"cntrl" => { if t_ch.is_ascii_control() { matched = true; } } b"digit" => { if t_ch.is_ascii_digit() { matched = true; } } b"graph" => { if t_ch.is_ascii_graphic() { matched = true; } } b"lower" => { if t_ch.is_ascii_lowercase() { matched = true; } } b"print" => { if (0x20u8..=0x7e).contains(&t_ch) { matched = true; } } b"punct" => { if t_ch.is_ascii_punctuation() { matched = true; } } b"space" => { if t_ch == b' ' { matched = true; } } b"upper" => { if t_ch.is_ascii_uppercase() || mode.contains(Mode::IGNORE_CASE) && t_ch.is_ascii_lowercase() { matched = true; } } b"xdigit" => { if t_ch.is_ascii_hexdigit() { matched = true; } } _ => return AbortAll, }; prev_p_ch = 0; } } _ => { prev_p_ch = p_ch; if p_ch == t_ch { matched = true; } } }, }; next = p.next(); if let Some((_, BRACKET_CLOSE)) = next { break; } } if matched == negated || mode.contains(Mode::NO_MATCH_SLASH_LITERAL) && t_ch == SLASH { return NoMatch; } continue; } non_glob_ch => { if non_glob_ch != t_ch { return NoMatch; } else { continue; } } } } t.next().map(|_| NoMatch).unwrap_or(Match) } /// Employ pattern matching to see if `value` matches `pattern`. /// /// `mode` can be used to adjust the way the matching is performed. pub fn wildmatch(pattern: &BStr, value: &BStr, mode: Mode) -> bool { match_recursive(pattern, value, mode) == Result::Match }}use bitflags::bitflags;
bitflags! {
/// The match mode employed in [`Pattern::matches()`][crate::Pattern::matches()].
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct Mode: u8 {
/// Let globs like `*` and `?` not match the slash `/` literal, which is useful when matching paths.
const NO_MATCH_SLASH_LITERAL = 1 << 0;
/// Match case insensitively for ascii characters only.
const IGNORE_CASE = 1 << 1;
}
}
pub(crate) mod function {
use bstr::{BStr, ByteSlice};
use crate::wildmatch::Mode;
#[derive(Eq, PartialEq)]
enum Result {
Match,
NoMatch,
AbortAll,
AbortToStarStar,
}
const STAR: u8 = b'*';
const BACKSLASH: u8 = b'\\';
const SLASH: u8 = b'/';
const BRACKET_OPEN: u8 = b'[';
const BRACKET_CLOSE: u8 = b']';
const COLON: u8 = b':';
const NEGATE_CLASS: u8 = b'!';
fn match_recursive(pattern: &BStr, text: &BStr, mode: Mode) -> Result {
use self::Result::*;
let possibly_lowercase = |c: &u8| {
if mode.contains(Mode::IGNORE_CASE) {
c.to_ascii_lowercase()
} else {
*c
}
};
let mut p = pattern.iter().map(possibly_lowercase).enumerate().peekable();
let mut t = text.iter().map(possibly_lowercase).enumerate();
while let Some((mut p_idx, mut p_ch)) = p.next() {
let (mut t_idx, mut t_ch) = match t.next() {
Some(c) => c,
None if p_ch != STAR => return AbortAll,
None => (text.len(), 0),
};
if p_ch == BACKSLASH {
match p.next() {
Some((_p_idx, p_ch)) => {
if p_ch != t_ch {
return NoMatch;
} else {
continue;
}
}
None => return NoMatch,
};
}
match p_ch {
b'?' => {
if mode.contains(Mode::NO_MATCH_SLASH_LITERAL) && t_ch == SLASH {
return NoMatch;
} else {
continue;
}
}
STAR => {
let mut match_slash = mode
.contains(Mode::NO_MATCH_SLASH_LITERAL)
.then(|| false)
.unwrap_or(true);
match p.next() {
Some((next_p_idx, next_p_ch)) => {
let next;
if next_p_ch == STAR {
let leading_slash_idx = p_idx.checked_sub(1);
while p.next_if(|(_, c)| *c == STAR).is_some() {}
next = p.next();
if !mode.contains(Mode::NO_MATCH_SLASH_LITERAL) {
match_slash = true;
} else if leading_slash_idx.map_or(true, |idx| pattern[idx] == SLASH)
&& next.map_or(true, |(_, c)| {
c == SLASH || (c == BACKSLASH && p.peek().map(|t| t.1) == Some(SLASH))
})
{
if next.map_or(NoMatch, |(idx, _)| {
match_recursive(pattern[idx + 1..].as_bstr(), text[t_idx..].as_bstr(), mode)
}) == Match
{
return Match;
}
match_slash = true;
} else {
match_slash = false;
}
} else {
next = Some((next_p_idx, next_p_ch));
}
match next {
None => {
return if !match_slash && text[t_idx..].contains(&SLASH) {
NoMatch
} else {
Match
};
}
Some((next_p_idx, next_p_ch)) => {
p_idx = next_p_idx;
p_ch = next_p_ch;
if !match_slash && p_ch == SLASH {
match text[t_idx..].find_byte(SLASH) {
Some(distance_to_slash) => {
for _ in t.by_ref().take(distance_to_slash) {}
continue;
}
None => return NoMatch,
}
}
}
}
}
None => {
return if !match_slash && text[t_idx..].contains(&SLASH) {
NoMatch
} else {
Match
}
}
}
return loop {
if !crate::parse::GLOB_CHARACTERS.contains(&p_ch) {
loop {
if (!match_slash && t_ch == SLASH) || t_ch == p_ch {
break;
}
match t.next() {
Some(t) => {
t_idx = t.0;
t_ch = t.1;
}
None => break,
};
}
if t_ch != p_ch {
return NoMatch;
}
}
let res = match_recursive(pattern[p_idx..].as_bstr(), text[t_idx..].as_bstr(), mode);
if res != NoMatch {
if !match_slash || res != AbortToStarStar {
return res;
}
} else if !match_slash && t_ch == SLASH {
return AbortToStarStar;
}
match t.next() {
Some(t) => {
t_idx = t.0;
t_ch = t.1;
}
None => break AbortAll,
};
};
}
BRACKET_OPEN => {
match p.next() {
Some(t) => {
p_idx = t.0;
p_ch = t.1;
}
None => return AbortAll,
};
if p_ch == b'^' {
p_ch = NEGATE_CLASS;
}
let negated = p_ch == NEGATE_CLASS;
let mut next = if negated { p.next() } else { Some((p_idx, p_ch)) };
let mut prev_p_ch = 0;
let mut matched = false;
loop {
match next {
None => return AbortAll,
Some((p_idx, mut p_ch)) => match p_ch {
BACKSLASH => match p.next() {
Some((_, p_ch)) => {
if p_ch == t_ch {
matched = true
} else {
prev_p_ch = p_ch;
}
}
None => return AbortAll,
},
b'-' if prev_p_ch != 0
&& p.peek().is_some()
&& p.peek().map(|t| t.1) != Some(BRACKET_CLOSE) =>
{
p_ch = p.next().expect("peeked").1;
if p_ch == BACKSLASH {
p_ch = match p.next() {
Some(t) => t.1,
None => return AbortAll,
};
}
if t_ch <= p_ch && t_ch >= prev_p_ch {
matched = true;
} else if mode.contains(Mode::IGNORE_CASE) && t_ch.is_ascii_lowercase() {
let t_ch_upper = t_ch.to_ascii_uppercase();
if (t_ch_upper <= p_ch.to_ascii_uppercase()
&& t_ch_upper >= prev_p_ch.to_ascii_uppercase())
|| (t_ch_upper <= prev_p_ch.to_ascii_uppercase()
&& t_ch_upper >= p_ch.to_ascii_uppercase())
{
matched = true;
}
}
prev_p_ch = 0;
}
BRACKET_OPEN if matches!(p.peek(), Some((_, COLON))) => {
p.next();
while p.peek().map_or(false, |t| t.1 != BRACKET_CLOSE) {
p.next();
}
let closing_bracket_idx = match p.next() {
Some((idx, _)) => idx,
None => return AbortAll,
};
const BRACKET__COLON__BRACKET: usize = 3;
if closing_bracket_idx - p_idx < BRACKET__COLON__BRACKET
|| pattern[closing_bracket_idx - 1] != COLON
{
if t_ch == BRACKET_OPEN {
matched = true
}
p = pattern[p_idx + 1..]
.iter()
.map(possibly_lowercase)
.enumerate()
.peekable();
} else {
let class = &pattern.as_ref()[p_idx + 2..closing_bracket_idx - 1];
match class {
b"alnum" => {
if t_ch.is_ascii_alphanumeric() {
matched = true;
}
}
b"alpha" => {
if t_ch.is_ascii_alphabetic() {
matched = true;
}
}
b"blank" => {
if t_ch.is_ascii_whitespace() {
matched = true;
}
}
b"cntrl" => {
if t_ch.is_ascii_control() {
matched = true;
}
}
b"digit" => {
if t_ch.is_ascii_digit() {
matched = true;
}
}
b"graph" => {
if t_ch.is_ascii_graphic() {
matched = true;
}
}
b"lower" => {
if t_ch.is_ascii_lowercase() {
matched = true;
}
}
b"print" => {
if (0x20u8..=0x7e).contains(&t_ch) {
matched = true;
}
}
b"punct" => {
if t_ch.is_ascii_punctuation() {
matched = true;
}
}
b"space" => {
if t_ch == b' ' {
matched = true;
}
}
b"upper" => {
if t_ch.is_ascii_uppercase()
|| mode.contains(Mode::IGNORE_CASE) && t_ch.is_ascii_lowercase()
{
matched = true;
}
}
b"xdigit" => {
if t_ch.is_ascii_hexdigit() {
matched = true;
}
}
_ => return AbortAll,
};
prev_p_ch = 0;
}
}
_ => {
prev_p_ch = p_ch;
if p_ch == t_ch {
matched = true;
}
}
},
};
next = p.next();
if let Some((_, BRACKET_CLOSE)) = next {
break;
}
}
if matched == negated || mode.contains(Mode::NO_MATCH_SLASH_LITERAL) && t_ch == SLASH {
return NoMatch;
}
continue;
}
non_glob_ch => {
if non_glob_ch != t_ch {
return NoMatch;
} else {
continue;
}
}
}
}
t.next().map(|_| NoMatch).unwrap_or(Match)
}
/// Employ pattern matching to see if `value` matches `pattern`.
///
/// `mode` can be used to adjust the way the matching is performed.
pub fn wildmatch(pattern: &BStr, value: &BStr, mode: Mode) -> bool {
match_recursive(pattern, value, mode) == Result::Match
}
}