use lru::LruCache;
use std::fmt;
use std::num::NonZeroUsize;
use std::sync::Mutex;
pub struct GlobMatcher {
pattern_lower: String,
cache: Mutex<LruCache<String, bool>>,
}
impl fmt::Debug for GlobMatcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GlobMatcher")
.field("pattern_lower", &self.pattern_lower)
.field("cache_size", &self.cache.lock().unwrap().len())
.finish()
}
}
impl GlobMatcher {
pub fn new(pattern: &str) -> Self {
let cache_size = NonZeroUsize::new(256).unwrap();
GlobMatcher {
pattern_lower: pattern.to_lowercase(),
cache: Mutex::new(LruCache::new(cache_size)),
}
}
pub fn pattern(&self) -> String {
self.pattern_lower.clone()
}
pub fn matches(&self, subject: &str) -> bool {
let subject_lower = subject.to_lowercase();
if self.pattern_lower == "*" {
return true;
}
if self.pattern_lower == subject_lower {
return true;
}
if !self.pattern_lower.contains('*') && !self.pattern_lower.contains('?') {
return false;
}
{
let mut cache = self.cache.lock().unwrap();
if let Some(&result) = cache.get(&subject_lower) {
return result;
}
}
let pattern = self.pattern_lower.as_bytes();
let subject = subject_lower.as_bytes();
let mut px = 0; let mut sx = 0; let mut next_px = 0; let mut next_sx = 0;
while px < pattern.len() || sx < subject.len() {
if px < pattern.len() {
let char = pattern[px];
if char == b'?' {
if sx < subject.len() {
px += 1;
sx += 1;
continue;
}
} else if char == b'*' {
next_px = px;
next_sx = sx + 1;
px += 1;
continue;
} else if sx < subject.len() && subject[sx] == char {
px += 1;
sx += 1;
continue;
}
}
if 0 < next_sx && next_sx <= subject.len() {
px = next_px;
sx = next_sx;
continue;
}
{
let mut cache = self.cache.lock().unwrap();
cache.put(subject_lower, false);
}
return false;
}
{
let mut cache = self.cache.lock().unwrap();
cache.put(subject_lower, true);
}
true
}
}
impl Clone for GlobMatcher {
fn clone(&self) -> Self {
GlobMatcher {
pattern_lower: self.pattern_lower.clone(),
cache: Mutex::new(LruCache::new(NonZeroUsize::new(256).unwrap())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_exact_match() {
let matcher = GlobMatcher::new("hello");
assert!(matcher.matches("hello"));
assert!(matcher.matches("HELLO")); assert!(!matcher.matches("hello world"));
assert!(!matcher.matches("hell"));
}
#[test]
fn test_glob_question_mark() {
let matcher = GlobMatcher::new("h?llo");
assert!(matcher.matches("hello"));
assert!(matcher.matches("hallo"));
assert!(!matcher.matches("hlo"));
assert!(!matcher.matches("heello"));
}
#[test]
fn test_glob_asterisk() {
let matcher = GlobMatcher::new("h*o");
assert!(matcher.matches("hello"));
assert!(matcher.matches("ho"));
assert!(matcher.matches("hello world o"));
assert!(!matcher.matches("hell"));
let matcher = GlobMatcher::new("h*");
assert!(matcher.matches("hello"));
assert!(matcher.matches("h"));
assert!(!matcher.matches("world"));
}
#[test]
fn test_glob_complex() {
let matcher = GlobMatcher::new("c*t?r*");
assert!(matcher.matches("contoroller"));
assert!(matcher.matches("cater"));
assert!(matcher.matches("ctfr!"));
assert!(!matcher.matches("car"));
let matcher = GlobMatcher::new("*service*");
assert!(matcher.matches("myservice"));
assert!(matcher.matches("service"));
assert!(matcher.matches("my service name"));
assert!(!matcher.matches("svc"));
}
#[test]
fn test_glob_caching() {
let matcher = GlobMatcher::new("c*t?r*");
assert!(matcher.matches("contoroller"));
let cache = matcher.cache.lock().unwrap();
assert!(cache.contains(&"contoroller".to_string()));
drop(cache);
assert!(!matcher.matches("car"));
let cache = matcher.cache.lock().unwrap();
assert!(cache.contains(&"contoroller".to_string()));
assert!(cache.contains(&"car".to_string()));
}
}