pub fn glob_match(pattern: &str, text: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
match_pattern(&pat, &txt)
}
fn match_class(class_chars: &[char], ch: char) -> Option<(bool, usize)> {
let mut i = 0;
let negate = if i < class_chars.len() && (class_chars[i] == '!' || class_chars[i] == '^') {
i += 1;
true
} else {
false
};
let mut matched = false;
let mut found_close = false;
while i < class_chars.len() {
if class_chars[i] == ']' {
found_close = true;
i += 1;
break;
}
if i + 2 < class_chars.len() && class_chars[i + 1] == '-' && class_chars[i + 2] != ']' {
let lo = class_chars[i];
let hi = class_chars[i + 2];
if ch >= lo && ch <= hi {
matched = true;
}
i += 3;
} else {
if class_chars[i] == ch {
matched = true;
}
i += 1;
}
}
if !found_close {
return None;
}
Some((if negate { !matched } else { matched }, i))
}
fn match_pattern(pat: &[char], txt: &[char]) -> bool {
let mut pi = 0;
let mut ti = 0;
let mut star_pi: Option<usize> = None;
let mut star_ti: usize = 0;
loop {
if pi < pat.len() {
match pat[pi] {
'*' => {
while pi < pat.len() && pat[pi] == '*' {
pi += 1;
}
if pi == pat.len() {
return true;
}
star_pi = Some(pi);
star_ti = ti;
continue;
}
'?' => {
if ti < txt.len() {
pi += 1;
ti += 1;
continue;
}
}
'[' => {
let class_slice = &pat[pi + 1..];
match match_class(class_slice, if ti < txt.len() { txt[ti] } else { '\0' }) {
Some((matched, consumed)) if ti < txt.len() && matched => {
pi += 1 + consumed; ti += 1;
continue;
}
Some(_) => {
}
None => {
if ti < txt.len() && txt[ti] == '[' {
pi += 1;
ti += 1;
continue;
}
}
}
}
literal => {
if ti < txt.len() && txt[ti] == literal {
pi += 1;
ti += 1;
continue;
}
}
}
} else {
if ti == txt.len() {
return true;
}
}
if let Some(spi) = star_pi {
star_ti += 1;
if star_ti <= txt.len() {
pi = spi;
ti = star_ti;
continue;
}
}
return false;
}
}
#[cfg(test)]
mod tests {
use super::glob_match;
#[test]
fn literal_exact_match() {
assert!(glob_match("hello", "hello"));
}
#[test]
fn literal_mismatch() {
assert!(!glob_match("hello", "world"));
}
#[test]
fn literal_prefix_not_full_match() {
assert!(!glob_match("a", "ab"));
}
#[test]
fn literal_suffix_not_full_match() {
assert!(!glob_match("b", "ab"));
}
#[test]
fn empty_pattern_matches_empty_text() {
assert!(glob_match("", ""));
}
#[test]
fn empty_pattern_does_not_match_nonempty_text() {
assert!(!glob_match("", "x"));
}
#[test]
fn nonempty_pattern_does_not_match_empty_text() {
assert!(!glob_match("a", ""));
}
#[test]
fn unicode_literal_match() {
assert!(glob_match("héllo", "héllo"));
}
#[test]
fn unicode_literal_mismatch() {
assert!(!glob_match("héllo", "hello"));
}
#[test]
fn unicode_wildcard() {
assert!(glob_match("h*o", "héllo"));
}
#[test]
fn star_matches_empty() {
assert!(glob_match("a*", "a"));
}
#[test]
fn star_matches_nonempty() {
assert!(glob_match("a*", "abc"));
}
#[test]
fn star_at_start() {
assert!(glob_match("*c", "abc"));
assert!(!glob_match("*c", "ab"));
}
#[test]
fn star_at_middle() {
assert!(glob_match("a*c", "ac"));
assert!(glob_match("a*c", "abc"));
assert!(glob_match("a*c", "aXYZc"));
assert!(!glob_match("a*c", "abd"));
}
#[test]
fn star_alone_matches_anything() {
assert!(glob_match("*", ""));
assert!(glob_match("*", "abc"));
assert!(glob_match("*", "x"));
}
#[test]
fn double_star_same_as_single() {
assert!(glob_match("a**c", "ac"));
assert!(glob_match("a**c", "aXc"));
assert!(!glob_match("a**c", "abd"));
}
#[test]
fn multiple_stars() {
assert!(glob_match("*.*", "foo.rs"));
assert!(glob_match("*.*", "a.b.c"));
assert!(!glob_match("*.*", "nodot"));
}
#[test]
fn star_does_not_require_separator() {
assert!(glob_match("src/*", "src/main.rs"));
assert!(glob_match("src/*", "src/a/b.rs"));
}
#[test]
fn question_matches_one_char() {
assert!(glob_match("a?c", "abc"));
assert!(glob_match("a?c", "axc"));
}
#[test]
fn question_does_not_match_empty() {
assert!(!glob_match("a?c", "ac"));
}
#[test]
fn question_does_not_match_two_chars() {
assert!(!glob_match("a?c", "axyc"));
}
#[test]
fn multiple_questions() {
assert!(glob_match("???", "abc"));
assert!(!glob_match("???", "ab"));
assert!(!glob_match("???", "abcd"));
}
#[test]
fn class_simple_match() {
assert!(glob_match("[abc]", "a"));
assert!(glob_match("[abc]", "b"));
assert!(glob_match("[abc]", "c"));
assert!(!glob_match("[abc]", "d"));
}
#[test]
fn class_range_lowercase() {
assert!(glob_match("[a-z]", "m"));
assert!(!glob_match("[a-z]", "A"));
assert!(!glob_match("[a-z]", "0"));
}
#[test]
fn class_range_digits() {
assert!(glob_match("[0-9]", "5"));
assert!(!glob_match("[0-9]", "a"));
}
#[test]
fn class_negate_exclamation() {
assert!(glob_match("[!0-9]", "a"));
assert!(!glob_match("[!0-9]", "5"));
}
#[test]
fn class_negate_caret() {
assert!(glob_match("[^abc]", "d"));
assert!(!glob_match("[^abc]", "a"));
}
#[test]
fn class_mixed() {
assert!(glob_match("[0-9_]", "3"));
assert!(glob_match("[0-9_]", "_"));
assert!(!glob_match("[0-9_]", "a"));
}
#[test]
fn class_in_pattern() {
assert!(glob_match("file[0-9].rs", "file3.rs"));
assert!(!glob_match("file[0-9].rs", "fileX.rs"));
}
#[test]
fn unterminated_class_literal_bracket() {
assert!(glob_match("[abc", "[abc"));
assert!(!glob_match("[abc", "a"));
}
#[test]
fn unterminated_class_with_star() {
assert!(glob_match("[*", "[hello"));
assert!(!glob_match("[*", "hello"));
}
#[test]
fn anchored_both_ends() {
assert!(!glob_match("ab", "xab"));
assert!(!glob_match("ab", "abx"));
assert!(glob_match("ab", "ab"));
}
#[test]
fn path_src_star() {
assert!(glob_match("src/*", "src/main.rs"));
assert!(glob_match("src/*", "src/lib.rs"));
assert!(!glob_match("src/x/*", "src/y/a.rs"));
}
#[test]
fn path_src_star_rs() {
assert!(glob_match("src/*.rs", "src/a.rs"));
assert!(glob_match("src/*.rs", "src/main.rs"));
assert!(!glob_match("src/*.rs", "src/a.txt"));
}
#[test]
fn path_double_wildcard_deep() {
assert!(glob_match("src/*", "src/a/b.rs"));
}
#[test]
fn path_exact() {
assert!(glob_match("src/main.rs", "src/main.rs"));
assert!(!glob_match("src/main.rs", "src/lib.rs"));
}
#[test]
fn path_extension_glob() {
assert!(glob_match("*.toml", "Cargo.toml"));
assert!(glob_match("*.toml", "config.toml"));
assert!(!glob_match("*.toml", "main.rs"));
}
#[test]
fn path_question_in_name() {
assert!(glob_match("file?.rs", "file1.rs"));
assert!(glob_match("file?.rs", "fileA.rs"));
assert!(!glob_match("file?.rs", "file.rs"));
assert!(!glob_match("file?.rs", "file12.rs"));
}
#[test]
fn pattern_only_stars_matches_anything() {
assert!(glob_match("***", ""));
assert!(glob_match("***", "hello"));
}
#[test]
fn star_at_both_ends() {
assert!(glob_match("*foo*", "foo"));
assert!(glob_match("*foo*", "foobar"));
assert!(glob_match("*foo*", "barfoo"));
assert!(glob_match("*foo*", "barfoobar"));
assert!(!glob_match("*foo*", "bar"));
}
#[test]
fn empty_class_not_matchable() {
assert!(!glob_match("[]", "a"));
assert!(!glob_match("[]", "]"));
}
#[test]
fn star_before_literal_backtracking() {
assert!(glob_match("*b", "aab"));
assert!(glob_match("a*b*c", "aXbYc"));
assert!(glob_match("a*b*c", "abc"));
assert!(!glob_match("a*b*c", "axc"));
}
#[test]
fn question_with_star() {
assert!(glob_match("?*", "a")); assert!(glob_match("?*", "ab"));
assert!(!glob_match("?*", "")); }
}