Files
Stalwart/crates/utils/src/glob.rs
2026-03-11 17:00:39 +01:00

267 lines
7.3 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<PatternChar>,
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<Self, String> {
// 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::<Vec<_>>()
} else {
value.chars().collect::<Vec<_>>()
};
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<String>,
patterns: Vec<GlobPattern>,
}
#[derive(Debug, Clone)]
pub struct GlobMap<V> {
entries: AHashMap<String, V>,
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<V> GlobMap<V> {
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<V> Default for GlobMap<V> {
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(GlobPattern::compile(
<Cow<&str>>::deserialize(deserializer)?.as_ref(),
true,
))
}
}