267 lines
7.3 KiB
Rust
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,
|
|
))
|
|
}
|
|
}
|