#[must_use]
pub fn glob_match(pattern: &str, text: &str) -> bool {
if !has_wildcard(pattern) {
return pattern == text;
}
let pattern: Vec<char> = pattern.chars().collect();
let text: Vec<char> = text.chars().collect();
wildcard(&pattern, &text)
}
#[must_use]
pub fn has_wildcard(pattern: &str) -> bool {
pattern.chars().any(|ch| ch == '*' || ch == '?')
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Glob {
pattern: String,
parsed: Option<Vec<char>>,
}
impl Glob {
#[must_use]
pub fn new(pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
let parsed = has_wildcard(&pattern).then(|| pattern.chars().collect());
Self { pattern, parsed }
}
#[must_use]
pub const fn is_literal(&self) -> bool {
self.parsed.is_none()
}
#[must_use]
pub fn pattern(&self) -> &str {
&self.pattern
}
#[must_use]
pub fn matches(&self, text: &str) -> bool {
self.parsed.as_ref().map_or_else(
|| self.pattern == text,
|chars| {
let text: Vec<char> = text.chars().collect();
wildcard(chars, &text)
},
)
}
}
fn wildcard(pattern: &[char], text: &[char]) -> bool {
let (mut p, mut t) = (0, 0);
let (mut star, mut mark) = (None, 0);
while t < text.len() {
if p < pattern.len() && (pattern[p] == '?' || pattern[p] == text[t]) {
p += 1;
t += 1;
} else if p < pattern.len() && pattern[p] == '*' {
star = Some(p);
mark = t;
p += 1;
} else if let Some(star_pos) = star {
p = star_pos + 1;
mark += 1;
t = mark;
} else {
return false;
}
}
while p < pattern.len() && pattern[p] == '*' {
p += 1;
}
p == pattern.len()
}
#[cfg(test)]
mod tests {
use super::{Glob, glob_match, has_wildcard};
#[test]
fn literal_matches_only_equal() {
assert!(glob_match("core", "core"));
assert!(!glob_match("core", "cores"));
assert!(!glob_match("core", "cor"));
}
#[test]
fn star_matches_any_run_including_empty() {
assert!(glob_match("item-*", "item-core"));
assert!(glob_match("item-*", "item-"));
assert!(glob_match("*-core", "item-core"));
assert!(glob_match("*", "anything"));
assert!(glob_match("*", ""));
}
#[test]
fn question_matches_exactly_one() {
assert!(glob_match("co?e", "core"));
assert!(!glob_match("co?e", "coe"));
assert!(!glob_match("co?e", "coree"));
}
#[test]
fn wildcards_operate_on_whole_chars() {
assert!(glob_match("caf?", "café"));
assert!(glob_match("*é", "café"));
assert!(glob_match("caf?", "cafe"));
assert!(!glob_match("caf?", "café!"));
}
#[test]
fn segments_do_not_leak_across_a_qualifier() {
assert!(!glob_match("service:*", "tenant:api"));
assert!(glob_match("service:*", "service:api"));
}
#[test]
fn reports_wildcard_presence() {
assert!(has_wildcard("item-*"));
assert!(has_wildcard("co?e"));
assert!(!has_wildcard("core"));
}
#[test]
fn compiled_glob_tracks_literalness() {
let literal = Glob::new("core");
assert!(literal.is_literal());
assert!(literal.matches("core"));
assert!(!literal.matches("cores"));
let pattern = Glob::new("item-*");
assert!(!pattern.is_literal());
assert!(pattern.matches("item-core"));
assert_eq!(pattern.pattern(), "item-*");
}
#[test]
fn trailing_stars_after_exhausted_text() {
assert!(glob_match("a**", "a"));
assert!(glob_match("a*b*", "ab"));
}
}