pub fn matches(pattern: &str, string: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let s: Vec<char> = string.chars().collect();
match_pat(&pat, &s)
}
fn match_pat(pat: &[char], s: &[char]) -> bool {
match pat.first() {
None => s.is_empty(),
Some('*') => {
let rest = &pat[1..];
for i in 0..=s.len() {
if match_pat(rest, &s[i..]) {
return true;
}
}
false
}
Some('?') => !s.is_empty() && match_pat(&pat[1..], &s[1..]),
Some('[') => {
if let Some((consumed, matched_char)) = parse_bracket(&pat[1..], s.first().copied()) {
!s.is_empty() && matched_char && match_pat(&pat[1 + consumed..], &s[1..])
} else {
!s.is_empty() && s[0] == '[' && match_pat(&pat[1..], &s[1..])
}
}
Some('\\') => {
if pat.len() >= 2 {
!s.is_empty() && s[0] == pat[1] && match_pat(&pat[2..], &s[1..])
} else {
!s.is_empty() && s[0] == '\\' && match_pat(&pat[1..], &s[1..])
}
}
Some(&c) => !s.is_empty() && s[0] == c && match_pat(&pat[1..], &s[1..]),
}
}
fn parse_bracket(pat: &[char], ch: Option<char>) -> Option<(usize, bool)> {
if pat.is_empty() {
return None;
}
let mut i = 0;
let negate = pat[0] == '!';
if negate {
i += 1;
}
let mut members: Vec<BracketItem> = Vec::new();
let mut found_close = false;
while i < pat.len() {
if pat[i] == ']' && !members.is_empty() {
i += 1;
found_close = true;
break;
}
if i + 2 < pat.len() && pat[i + 1] == '-' && pat[i + 2] != ']' {
members.push(BracketItem::Range(pat[i], pat[i + 2]));
i += 3;
} else {
members.push(BracketItem::Char(pat[i]));
i += 1;
}
}
if !found_close {
return None;
}
let inner_match = ch
.map(|c| members.iter().any(|m| m.matches(c)))
.unwrap_or(false);
let result = if negate { !inner_match } else { inner_match };
Some((i, result))
}
enum BracketItem {
Char(char),
Range(char, char),
}
impl BracketItem {
fn matches(&self, c: char) -> bool {
match self {
BracketItem::Char(x) => *x == c,
BracketItem::Range(lo, hi) => {
let lo = *lo as u32;
let hi = *hi as u32;
let c = c as u32;
c >= lo && c <= hi
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_literal_match() {
assert!(matches("hello", "hello"));
}
#[test]
fn test_literal_no_match() {
assert!(!matches("hello", "world"));
}
#[test]
fn test_empty_pattern_empty_string() {
assert!(matches("", ""));
}
#[test]
fn test_empty_pattern_nonempty_string() {
assert!(!matches("", "a"));
}
#[test]
fn test_star_matches_empty() {
assert!(matches("*", ""));
}
#[test]
fn test_star_matches_any() {
assert!(matches("*", "anything"));
}
#[test]
fn test_star_prefix() {
assert!(matches("*.txt", "file.txt"));
assert!(!matches("*.txt", "file.rs"));
}
#[test]
fn test_star_suffix() {
assert!(matches("file.*", "file.txt"));
assert!(matches("file.*", "file.rs"));
assert!(!matches("file.*", "other.txt"));
}
#[test]
fn test_double_star() {
assert!(matches("a**b", "ab"));
assert!(matches("a**b", "axyzb"));
}
#[test]
fn test_question_single_char() {
assert!(matches("?", "a"));
assert!(matches("?", "z"));
assert!(!matches("?", ""));
assert!(!matches("?", "ab"));
}
#[test]
fn test_question_in_middle() {
assert!(matches("a?c", "abc"));
assert!(!matches("a?c", "ac"));
}
#[test]
fn test_bracket_set() {
assert!(matches("[abc]", "a"));
assert!(matches("[abc]", "b"));
assert!(matches("[abc]", "c"));
assert!(!matches("[abc]", "d"));
}
#[test]
fn test_bracket_range() {
assert!(matches("[a-z]", "a"));
assert!(matches("[a-z]", "m"));
assert!(matches("[a-z]", "z"));
assert!(!matches("[a-z]", "A"));
assert!(!matches("[a-z]", "0"));
}
#[test]
fn test_bracket_negated() {
assert!(!matches("[!abc]", "a"));
assert!(matches("[!abc]", "d"));
}
#[test]
fn test_bracket_negated_range() {
assert!(!matches("[!a-z]", "m"));
assert!(matches("[!a-z]", "A"));
assert!(matches("[!a-z]", "0"));
}
#[test]
fn test_backslash_literal_star() {
assert!(matches("\\*", "*"));
assert!(!matches("\\*", "a"));
}
#[test]
fn test_backslash_literal_char() {
assert!(matches("\\a", "a"));
assert!(!matches("\\a", "b"));
}
#[test]
fn test_complex_pattern() {
assert!(matches("file[0-9].txt", "file3.txt"));
assert!(!matches("file[0-9].txt", "fileA.txt"));
}
#[test]
fn test_star_question_combined() {
assert!(matches("*?", "a"));
assert!(matches("*?", "ab"));
assert!(!matches("*?", ""));
}
}