/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use ahash::{AHashMap, AHashSet}; use serde::Deserialize; use std::borrow::Cow; #[derive(Debug, Clone, PartialEq, Eq)] pub enum MatchType { Equal(String), StartsWith(String), EndsWith(String), Matches(GlobPattern), All, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GlobPattern { pattern: Vec, to_lower: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum PatternChar { WildcardMany { num: usize, match_pos: usize }, WildcardSingle { match_pos: usize }, Char { char: char, match_pos: usize }, } impl GlobPattern { pub fn compile(pattern: &str, to_lower: bool) -> Self { let mut chars = Vec::new(); let mut is_escaped = false; let mut str = pattern.chars().peekable(); while let Some(char) = str.next() { match char { '*' if !is_escaped => { let mut num = 1; while let Some('*') = str.peek() { num += 1; str.next(); } chars.push(PatternChar::WildcardMany { num, match_pos: 0 }); } '?' if !is_escaped => { chars.push(PatternChar::WildcardSingle { match_pos: 0 }); } '\\' if !is_escaped => { is_escaped = true; continue; } _ => { if is_escaped { is_escaped = false; } if to_lower && char.is_uppercase() { for char in char.to_lowercase() { chars.push(PatternChar::Char { char, match_pos: 0 }); } } else { chars.push(PatternChar::Char { char, match_pos: 0 }); } } } } GlobPattern { pattern: chars, to_lower, } } pub fn try_compile(pattern: &str, to_lower: bool) -> Result { // Detect if the key is a glob pattern let mut last_ch = '\0'; let mut has_escape = false; let mut is_glob = false; for ch in pattern.chars() { match ch { '\\' => { has_escape = true; } '*' | '?' if last_ch != '\\' => { is_glob = true; } _ => {} } last_ch = ch; } if is_glob { Ok(GlobPattern::compile(pattern, to_lower)) } else { Err(if has_escape { pattern.replace('\\', "") } else { pattern.to_string() }) } } // Credits: Algorithm ported from https://research.swtch.com/glob pub fn matches(&self, value: &str) -> bool { let value = if self.to_lower { value.to_lowercase().chars().collect::>() } else { value.chars().collect::>() }; let mut px = 0; let mut nx = 0; let mut next_px = 0; let mut next_nx = 0; while px < self.pattern.len() || nx < value.len() { match self.pattern.get(px) { Some(PatternChar::Char { char, .. }) => { if matches!(value.get(nx), Some(nc) if nc == char ) { px += 1; nx += 1; continue; } } Some(PatternChar::WildcardSingle { .. }) => { if nx < value.len() { px += 1; nx += 1; continue; } } Some(PatternChar::WildcardMany { .. }) => { next_px = px; next_nx = nx + 1; px += 1; continue; } _ => (), } if 0 < next_nx && next_nx <= value.len() { px = next_px; nx = next_nx; continue; } return false; } true } } #[derive(Debug, Clone, Default)] pub struct GlobSet { entries: AHashSet, patterns: Vec, } #[derive(Debug, Clone)] pub struct GlobMap { entries: AHashMap, patterns: Vec<(GlobPattern, V)>, } impl GlobSet { pub fn new() -> Self { GlobSet::default() } pub fn insert_pattern(&mut self, pattern: &str) { match GlobPattern::try_compile(pattern, false) { Ok(glob) => { self.patterns.push(glob); } Err(entry) => { self.entries.insert(entry); } } } pub fn insert_entry(&mut self, entry: String) { self.entries.insert(entry); } pub fn contains(&self, key: &str) -> bool { self.entries.contains(key) || self.patterns.iter().any(|pattern| pattern.matches(key)) } } impl GlobMap { pub fn new() -> Self { GlobMap { entries: AHashMap::new(), patterns: Vec::new(), } } pub fn insert_pattern(&mut self, pattern: &str, value: V) { match GlobPattern::try_compile(pattern, false) { Ok(glob) => { self.patterns.push((glob, value)); } Err(entry) => { self.entries.insert(entry, value); } } } pub fn insert_entry(&mut self, entry: String, value: V) { self.entries.insert(entry, value); } pub fn get(&self, key: &str) -> Option<&V> { self.entries.get(key).or_else(|| { self.patterns .iter() .find_map(|(pattern, value)| pattern.matches(key).then_some(value)) }) } } impl Default for GlobMap { fn default() -> Self { GlobMap::new() } } impl MatchType { pub fn parse(value: &str) -> Self { if value == "*" { MatchType::All } else if let Some(value) = value.strip_suffix('*') { MatchType::StartsWith(value.to_string()) } else if let Some(value) = value.strip_prefix('*') { MatchType::EndsWith(value.to_string()) } else if value.contains('*') { MatchType::Matches(GlobPattern::compile(value, false)) } else { MatchType::Equal(value.to_string()) } } pub fn matches(&self, value: &str) -> bool { match self { MatchType::Equal(pattern) => value == pattern, MatchType::StartsWith(pattern) => value.starts_with(pattern), MatchType::EndsWith(pattern) => value.ends_with(pattern), MatchType::Matches(pattern) => pattern.matches(value), MatchType::All => true, } } } impl<'de> Deserialize<'de> for GlobPattern { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { Ok(GlobPattern::compile( >::deserialize(deserializer)?.as_ref(), true, )) } }