use std::collections::HashSet;
use std::fmt::Debug;
const SEGMENT_SEP: char = ':';
const WILDCARD_ONE: &str = "*";
const WILDCARD_MANY: &str = "**";
pub trait PermissionMatcher: Send + Sync + Debug {
fn matches_one(&self, owned: &str, required: &str) -> bool;
fn matches(&self, owned: &[String], required: &str) -> bool {
if required.is_empty() {
return false;
}
owned.iter().any(|p| self.matches_one(p, required))
}
fn matches_all(&self, owned: &[String], required: &[&str]) -> bool {
required.iter().all(|r| self.matches(owned, r))
}
fn matches_any(&self, owned: &[String], required: &[&str]) -> bool {
required.iter().any(|r| self.matches(owned, r))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AntPermissionMatcher;
impl AntPermissionMatcher {
fn segment_match(pattern: &str, target: &str) -> bool {
let mut pat = pattern.split(SEGMENT_SEP);
let mut tgt = target.split(SEGMENT_SEP);
loop {
match (pat.next(), tgt.next()) {
(None, None) => return true,
(None, Some(_)) => return false,
(Some(WILDCARD_MANY), _) => return true,
(Some(_), None) => return false,
(Some(WILDCARD_ONE), Some(_)) => continue,
(Some(p), Some(t)) => {
if p != t {
return false;
}
}
}
}
}
}
impl PermissionMatcher for AntPermissionMatcher {
fn matches_one(&self, owned: &str, required: &str) -> bool {
if owned == required {
return true;
}
if owned == WILDCARD_ONE || owned == WILDCARD_MANY {
return true;
}
if !owned.contains('*') {
return false;
}
Self::segment_match(owned, required)
}
fn matches_all(&self, owned: &[String], required: &[&str]) -> bool {
if required.is_empty() {
return true;
}
if required.len() == 1 {
return required.first().is_some_and(|r| self.matches(owned, r));
}
let (exact, wildcard) = split_exact_and_wildcard(owned);
required.iter().all(|r| {
!r.is_empty() && (exact.contains(*r) || wildcard.iter().any(|w| self.matches_one(w, r)))
})
}
fn matches_any(&self, owned: &[String], required: &[&str]) -> bool {
if required.is_empty() {
return false;
}
if required.len() == 1 {
return required.first().is_some_and(|r| self.matches(owned, r));
}
let (exact, wildcard) = split_exact_and_wildcard(owned);
required.iter().any(|r| {
!r.is_empty() && (exact.contains(*r) || wildcard.iter().any(|w| self.matches_one(w, r)))
})
}
}
fn split_exact_and_wildcard(owned: &[String]) -> (HashSet<&str>, Vec<&str>) {
let mut exact = HashSet::with_capacity(owned.len());
let mut wildcard = Vec::new();
for item in owned {
if item.contains('*') {
wildcard.push(item.as_str());
} else {
exact.insert(item.as_str());
}
}
(exact, wildcard)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ExactMatcher;
impl PermissionMatcher for ExactMatcher {
fn matches_one(&self, owned: &str, required: &str) -> bool {
owned == required
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ant_star_vs_double_star() {
let m = AntPermissionMatcher;
let owned = vec!["user:*".to_string()];
assert!(m.matches(&owned, "user:list"));
assert!(!m.matches(&owned, "user:a:b"));
let owned2 = vec!["user:**".to_string()];
assert!(m.matches(&owned2, "user:a:b"));
assert!(!m.matches(&owned2, "other:x"));
}
#[test]
fn ant_empty_and_or() {
let m = AntPermissionMatcher;
let owned = vec!["a".to_string()];
assert!(m.matches_all(&owned, &[]));
assert!(!m.matches_any(&owned, &[]));
}
#[test]
fn exact_matcher() {
let m = ExactMatcher;
assert!(m.matches_one("admin", "admin"));
assert!(!m.matches_one("admin", "user"));
}
}