#[derive(Clone, Debug, PartialEq, Eq)]
enum Token<'a> {
Literal(u8),
Question,
Star,
DoubleStar,
DoubleStarSlash,
SlashDoubleStar,
SlashDoubleStarSlash,
Class {
body: &'a [u8],
negated: bool,
},
}
fn parse_class_token<'a>(pattern: &'a [u8]) -> (Token<'a>, usize) {
if let Some(end) = class_end(pattern) {
let raw_body = &pattern[1..end];
let (negated, body) = match raw_body.first() {
Some(&(b'!' | b'^')) => (true, &raw_body[1..]),
_ => (false, raw_body),
};
(Token::Class { body, negated }, end.saturating_add(1))
} else {
(Token::Literal(b'['), 1)
}
}
fn match_four_byte_prefix<'a>(pattern: &'a [u8]) -> Option<(Token<'a>, usize)> {
if pattern.starts_with(b"/**/") {
Some((Token::SlashDoubleStarSlash, 4))
} else {
None
}
}
fn match_three_byte_prefix<'a>(pattern: &'a [u8]) -> Option<(Token<'a>, usize)> {
if pattern.starts_with(b"/**") {
Some((Token::SlashDoubleStar, 3))
} else if pattern.starts_with(b"**/") {
Some((Token::DoubleStarSlash, 3))
} else {
None
}
}
fn match_special_prefix<'a>(pattern: &'a [u8]) -> Option<(Token<'a>, usize)> {
if let Some(tok) = match_four_byte_prefix(pattern) {
Some(tok)
} else if let Some(tok) = match_three_byte_prefix(pattern) {
Some(tok)
} else if pattern.starts_with(b"**") {
Some((Token::DoubleStar, 2))
} else {
None
}
}
fn match_single_char_token<'a>(pattern: &'a [u8]) -> (Token<'a>, usize) {
match pattern[0] {
b'*' => (Token::Star, 1),
b'?' => (Token::Question, 1),
b'[' => parse_class_token(pattern),
ch => (Token::Literal(ch), 1),
}
}
fn next_token<'a>(pattern: &'a [u8]) -> Option<(Token<'a>, usize)> {
if pattern.is_empty() {
return None;
}
if let Some(tok) = match_special_prefix(pattern) {
Some(tok)
} else {
Some(match_single_char_token(pattern))
}
}
fn tokenize<'a>(mut pattern: &'a [u8]) -> Vec<Token<'a>> {
let mut tokens = Vec::new();
while let Some((tok, consumed)) = next_token(pattern) {
tokens.push(tok);
pattern = &pattern[consumed..];
}
tokens
}
fn step_literal(literal: u8, path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 1..=path.len() {
if dp[j.saturating_sub(1)] && path[j.saturating_sub(1)] == literal {
next[j] = true;
}
}
}
fn step_question(path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 1..=path.len() {
if dp[j.saturating_sub(1)] && path[j.saturating_sub(1)] != b'/' {
next[j] = true;
}
}
}
fn step_class(body: &[u8], negated: bool, path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 1..=path.len() {
if dp[j.saturating_sub(1)] && path[j.saturating_sub(1)] != b'/' {
let matches_body = scan_class_body(body, path[j.saturating_sub(1)]);
let hit = if negated { !matches_body } else { matches_body };
if hit {
next[j] = true;
}
}
}
}
fn step_star(path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 0..=path.len() {
if dp[j] {
next[j] = true;
for reach in j.saturating_add(1)..=path.len() {
if path[reach.saturating_sub(1)] == b'/' {
break;
}
next[reach] = true;
}
}
}
}
fn step_double_star(path: &[u8], dp: &[bool], next: &mut [bool]) {
let mut any = false;
for j in 0..=path.len() {
any |= dp[j];
if any {
next[j] = true;
}
}
}
fn step_double_star_slash(path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 0..=path.len() {
if dp[j] {
next[j] = true;
for reach in j.saturating_add(1)..=path.len() {
if path[reach.saturating_sub(1)] == b'/' {
next[reach] = true;
}
}
}
}
}
fn step_slash_double_star(path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 0..=path.len() {
if dp[j] {
next[j] = true;
if j < path.len() && path[j] == b'/' {
for slot in &mut next[j.saturating_add(1)..=path.len()] {
*slot = true;
}
}
}
}
}
fn step_slash_double_star_slash(path: &[u8], dp: &[bool], next: &mut [bool]) {
for j in 0..=path.len() {
if dp[j] && j < path.len() && path[j] == b'/' {
next[j.saturating_add(1)] = true;
for reach in j.saturating_add(2)..=path.len() {
if path[reach.saturating_sub(1)] == b'/' {
next[reach] = true;
}
}
}
}
}
fn step_token(token: &Token<'_>, path: &[u8], dp: &[bool], next: &mut [bool]) {
match *token {
Token::Literal(literal) => step_literal(literal, path, dp, next),
Token::Question => step_question(path, dp, next),
Token::Class { body, negated } => step_class(body, negated, path, dp, next),
Token::Star => step_star(path, dp, next),
Token::DoubleStar => step_double_star(path, dp, next),
Token::DoubleStarSlash => step_double_star_slash(path, dp, next),
Token::SlashDoubleStar => step_slash_double_star(path, dp, next),
Token::SlashDoubleStarSlash => step_slash_double_star_slash(path, dp, next),
}
}
#[must_use]
pub fn matches(pattern: &str, path: &str) -> bool {
let tokens = tokenize(pattern.as_bytes());
let path_bytes = path.as_bytes();
let row_len = path_bytes.len().saturating_add(1);
let mut dp = vec![false; row_len];
dp[0] = true;
for token in &tokens {
let mut next = vec![false; row_len];
step_token(token, path_bytes, &dp, &mut next);
dp = next;
}
dp[path_bytes.len()]
}
fn skip_class_prefix(pattern: &[u8]) -> usize {
let mut idx = 1;
if matches!(pattern.get(idx), Some(b'!' | b'^')) {
idx = idx.saturating_add(1);
}
if pattern.get(idx) == Some(&b']') {
idx = idx.saturating_add(1);
}
idx
}
fn find_closing_bracket(pattern: &[u8], start: usize) -> Option<usize> {
(start..pattern.len()).find(|&idx| pattern[idx] == b']')
}
fn class_end(pattern: &[u8]) -> Option<usize> {
let start = skip_class_prefix(pattern);
find_closing_bracket(pattern, start)
}
fn match_range_or_single(body: &[u8], candidate: u8, idx: usize) -> (bool, usize) {
if idx.saturating_add(2) < body.len() && body[idx.saturating_add(1)] == b'-' {
let in_range = body[idx] <= candidate && candidate <= body[idx.saturating_add(2)];
(in_range, 3)
} else {
let is_match = body[idx] == candidate;
(is_match, 1)
}
}
fn scan_class_body(body: &[u8], candidate: u8) -> bool {
let mut idx = 0;
while idx < body.len() {
let (matched, step) = match_range_or_single(body, candidate, idx);
if matched {
return true;
}
idx = idx.saturating_add(step);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_pattern_matches_only_an_empty_path() {
assert!(matches("", ""));
assert!(!matches("", "a"));
}
#[test]
fn a_literal_pattern_matches_only_itself() {
assert!(matches("hello", "hello"));
assert!(!matches("hello", "world"));
assert!(!matches("hello", "hello/world"));
}
#[test]
fn question_mark_matches_one_non_separator() {
assert!(matches("a?c", "abc"));
assert!(matches("a?c", "a.c"));
assert!(!matches("a?c", "a/c"));
assert!(!matches("a?c", "ac"));
assert!(!matches("a?c", "abbc"));
}
#[test]
fn star_does_not_cross_a_separator() {
assert!(matches("a*c", "ac"));
assert!(matches("a*c", "abc"));
assert!(matches("a*c", "abbc"));
assert!(!matches("a*c", "a/c"));
assert!(!matches("a*c", "a/b/c"));
assert!(matches("*.rs", "main.rs"));
assert!(!matches("*.rs", "src/main.rs"));
}
#[test]
fn double_star_crosses_separators() {
assert!(matches("a/**/b", "a/b"));
assert!(matches("a/**/b", "a/x/b"));
assert!(matches("a/**/b", "a/x/y/z/b"));
assert!(matches("**/b", "b"));
assert!(matches("**/b", "a/b"));
assert!(matches("**/b", "x/y/b"));
assert!(matches("a/**", "a"));
assert!(matches("a/**", "a/b"));
assert!(matches("a/**", "a/b/c"));
}
#[test]
fn double_star_matches_nothing_in_between() {
assert!(matches("a**b", "ab"));
assert!(matches("a**b", "axb"));
assert!(matches("a**b", "a/b"));
assert!(matches("a**b", "a/x/y/b"));
}
#[test]
fn a_class_never_matches_a_separator() {
assert!(!matches("[/]", "/"));
assert!(!matches("[a/z]", "/"));
assert!(!matches("[!a]", "/"));
}
#[test]
fn a_negated_class_excludes_its_members() {
assert!(!matches("[!abc]", "a"));
assert!(!matches("[!abc]", "b"));
assert!(!matches("[!abc]", "c"));
assert!(matches("[!abc]", "d"));
assert!(!matches("[^abc]", "a"));
assert!(matches("[^abc]", "d"));
}
#[test]
fn a_trailing_dash_in_a_class_is_a_literal() {
assert!(matches("[a-]", "a"));
assert!(matches("[a-]", "-"));
assert!(!matches("[a-]", "b"));
}
#[test]
fn an_unterminated_class_is_a_literal_bracket() {
assert!(matches("[abc", "[abc"));
assert!(!matches("[abc", "a"));
}
#[test]
fn backtracking_terminates_on_a_pathological_pattern() {
let pattern = "a*a*a*a*a*a*b";
let path = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
assert!(!matches(pattern, path));
}
}