pub(crate) const MAX_GLOB_PATTERN_BYTES: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GlobToken {
Literal(u8),
Star,
DoubleStar,
DoubleStarSlash,
}
pub(crate) fn validate_glob_grammar(pattern: &str) -> Result<(), &'static str> {
if pattern.contains('\\') {
return Err("pattern does not support backslash escapes");
}
let bytes = pattern.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] != b'*' {
index += 1;
continue;
}
let run_start = index;
while index < bytes.len() && bytes[index] == b'*' {
index += 1;
}
let run_len = index - run_start;
if run_len > 2 {
return Err("more than two consecutive '*' are not supported");
}
if run_len == 2 {
let before_ok = run_start == 0 || bytes[run_start - 1] == b'/';
let after_ok = index == bytes.len() || bytes[index] == b'/';
if !before_ok || !after_ok {
return Err("'**' must occupy a whole path segment");
}
}
}
Ok(())
}
pub(crate) fn compile_glob(pattern: &[u8]) -> Vec<GlobToken> {
tokenize_glob(pattern)
}
fn tokenize_glob(pattern: &[u8]) -> Vec<GlobToken> {
let mut tokens = Vec::with_capacity(pattern.len());
let mut index = 0;
while index < pattern.len() {
match pattern[index] {
b'*' => {
if pattern.get(index + 1) == Some(&b'*') {
if pattern.get(index + 2) == Some(&b'/') {
tokens.push(GlobToken::DoubleStarSlash);
index += 3;
} else {
tokens.push(GlobToken::DoubleStar);
index += 2;
}
} else {
tokens.push(GlobToken::Star);
index += 1;
}
}
byte => {
tokens.push(GlobToken::Literal(byte));
index += 1;
}
}
}
tokens
}
#[cfg(test)]
pub(crate) fn glob_match(pattern: &[u8], text: &[u8]) -> bool {
matches_tokens(&tokenize_glob(pattern), text)
}
pub(crate) fn matches_tokens(tokens: &[GlobToken], text: &[u8]) -> bool {
let len = text.len();
let mut reachable = vec![false; len + 1];
reachable[0] = true;
let mut next = vec![false; len + 1];
for &token in tokens {
next.fill(false);
match token {
GlobToken::Literal(byte) => {
for j in 0..len {
if reachable[j] && text[j] == byte {
next[j + 1] = true;
}
}
}
GlobToken::Star => {
let mut carry = false;
for j in 0..=len {
let here = reachable[j] || carry;
next[j] = here;
carry = here && j < len && text[j] != b'/';
}
}
GlobToken::DoubleStar => {
let mut seen = false;
for j in 0..=len {
seen |= reachable[j];
next[j] = seen;
}
}
GlobToken::DoubleStarSlash => {
let mut seen = false;
for j in 0..=len {
let mut here = reachable[j];
if seen && j > 0 && text[j - 1] == b'/' {
here = true;
}
next[j] = here;
if reachable[j] {
seen = true;
}
}
}
}
std::mem::swap(&mut reachable, &mut next);
}
reachable[len]
}
#[cfg(test)]
mod tests {
use super::{compile_glob, glob_match, matches_tokens};
#[test]
fn compiled_tokens_match_identically_to_one_shot_across_many_paths() {
let paths = [
"a.txt",
"src/a.rs",
"src/b.rs",
"src/deep/c.rs",
"src/deep/deeper/d.rs",
"notes/today.md",
];
for pattern in [
"*.txt",
"src/*.rs",
"src/**/*.rs",
"**/*.md",
"src/**",
"no*match",
] {
let tokens = compile_glob(pattern.as_bytes());
for path in paths {
assert_eq!(
matches_tokens(&tokens, path.as_bytes()),
glob_match(pattern.as_bytes(), path.as_bytes()),
"pattern {pattern:?} vs {path:?} diverged between compiled and one-shot",
);
}
}
}
}