use std::path::Path;
use anyhow::{Context, Result};
use glob::Pattern;
use regex::{Regex, RegexBuilder};
use super::policy::Matcher;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GlobMatchMode {
#[default]
PathOrFilename,
FullPathOnly,
FilenameOnly,
}
#[derive(Debug, Clone)]
pub struct GlobMatcher {
pattern: Pattern,
raw: String,
mode: GlobMatchMode,
}
impl GlobMatcher {
pub fn new(pattern: &str) -> Result<Self> {
Self::with_mode(pattern, GlobMatchMode::default())
}
pub fn with_mode(pattern: &str, mode: GlobMatchMode) -> Result<Self> {
let glob_pattern =
Pattern::new(pattern).with_context(|| format!("Invalid glob pattern: {}", pattern))?;
Ok(Self {
pattern: glob_pattern,
raw: pattern.to_string(),
mode,
})
}
pub fn pattern(&self) -> &str {
&self.raw
}
pub fn mode(&self) -> GlobMatchMode {
self.mode
}
fn matches_filename(&self, path: &Path) -> bool {
if let Some(filename) = path.file_name() {
if let Some(filename_str) = filename.to_str() {
return self.pattern.matches(filename_str);
}
}
false
}
}
impl Matcher for GlobMatcher {
fn matches(&self, path: &Path) -> bool {
match self.mode {
GlobMatchMode::PathOrFilename => {
if self.pattern.matches_path(path) {
return true;
}
self.matches_filename(path)
}
GlobMatchMode::FullPathOnly => self.pattern.matches_path(path),
GlobMatchMode::FilenameOnly => self.matches_filename(path),
}
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
fn pattern_description(&self) -> String {
format!("glob:{}", self.raw)
}
}
#[derive(Debug, Clone)]
pub struct RegexMatcher {
regex: Regex,
raw: String,
}
impl RegexMatcher {
const DEFAULT_SIZE_LIMIT: usize = 1024 * 1024;
pub fn new(pattern: &str) -> Result<Self> {
let regex = RegexBuilder::new(pattern)
.size_limit(Self::DEFAULT_SIZE_LIMIT)
.dfa_size_limit(Self::DEFAULT_SIZE_LIMIT)
.build()
.with_context(|| format!("Invalid regex pattern: {}", pattern))?;
Ok(Self {
regex,
raw: pattern.to_string(),
})
}
pub fn with_size_limit(pattern: &str, size_limit: usize) -> Result<Self> {
let regex = RegexBuilder::new(pattern)
.size_limit(size_limit)
.dfa_size_limit(size_limit)
.build()
.with_context(|| format!("Invalid regex pattern: {}", pattern))?;
Ok(Self {
regex,
raw: pattern.to_string(),
})
}
pub fn pattern(&self) -> &str {
&self.raw
}
}
impl Matcher for RegexMatcher {
fn matches(&self, path: &Path) -> bool {
self.regex.is_match(&path.to_string_lossy())
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
fn pattern_description(&self) -> String {
format!("regex:{}", self.raw)
}
}
#[derive(Debug, Clone)]
pub struct CombinedMatcher {
matchers: Vec<Box<dyn Matcher>>,
}
impl CombinedMatcher {
pub fn new(matchers: Vec<Box<dyn Matcher>>) -> Self {
Self { matchers }
}
pub fn with_matcher(mut self, matcher: Box<dyn Matcher>) -> Self {
self.matchers.push(matcher);
self
}
pub fn len(&self) -> usize {
self.matchers.len()
}
pub fn is_empty(&self) -> bool {
self.matchers.is_empty()
}
}
impl Matcher for CombinedMatcher {
fn matches(&self, path: &Path) -> bool {
self.matchers.iter().any(|m| m.matches(path))
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
fn pattern_description(&self) -> String {
let descriptions: Vec<_> = self
.matchers
.iter()
.map(|m| m.pattern_description())
.collect();
format!("any_of:[{}]", descriptions.join(", "))
}
}
#[derive(Debug, Clone)]
pub struct NotMatcher {
inner: Box<dyn Matcher>,
}
impl NotMatcher {
pub fn new(inner: Box<dyn Matcher>) -> Self {
Self { inner }
}
}
impl Matcher for NotMatcher {
fn matches(&self, path: &Path) -> bool {
!self.inner.matches(path)
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
fn pattern_description(&self) -> String {
format!("not({})", self.inner.pattern_description())
}
}
#[derive(Debug, Clone)]
pub struct AllMatcher {
matchers: Vec<Box<dyn Matcher>>,
}
impl AllMatcher {
pub fn new(matchers: Vec<Box<dyn Matcher>>) -> Self {
Self { matchers }
}
pub fn with_matcher(mut self, matcher: Box<dyn Matcher>) -> Self {
self.matchers.push(matcher);
self
}
pub fn len(&self) -> usize {
self.matchers.len()
}
pub fn is_empty(&self) -> bool {
self.matchers.is_empty()
}
}
impl Matcher for AllMatcher {
fn matches(&self, path: &Path) -> bool {
if self.matchers.is_empty() {
return false;
}
self.matchers.iter().all(|m| m.matches(path))
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
fn pattern_description(&self) -> String {
let descriptions: Vec<_> = self
.matchers
.iter()
.map(|m| m.pattern_description())
.collect();
format!("all_of:[{}]", descriptions.join(", "))
}
}
#[derive(Debug, Clone)]
pub enum CompositeMatcher {
And(Vec<Box<dyn Matcher>>),
Or(Vec<Box<dyn Matcher>>),
Not(Box<dyn Matcher>),
}
impl CompositeMatcher {
pub fn and(matchers: Vec<Box<dyn Matcher>>) -> Self {
CompositeMatcher::And(matchers)
}
pub fn or(matchers: Vec<Box<dyn Matcher>>) -> Self {
CompositeMatcher::Or(matchers)
}
pub fn not(matcher: Box<dyn Matcher>) -> Self {
CompositeMatcher::Not(matcher)
}
}
impl Matcher for CompositeMatcher {
fn matches(&self, path: &Path) -> bool {
match self {
CompositeMatcher::And(matchers) => {
if matchers.is_empty() {
return false;
}
matchers.iter().all(|m| m.matches(path))
}
CompositeMatcher::Or(matchers) => matchers.iter().any(|m| m.matches(path)),
CompositeMatcher::Not(matcher) => !matcher.matches(path),
}
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
fn pattern_description(&self) -> String {
match self {
CompositeMatcher::And(matchers) => {
let descriptions: Vec<_> =
matchers.iter().map(|m| m.pattern_description()).collect();
format!("and:[{}]", descriptions.join(", "))
}
CompositeMatcher::Or(matchers) => {
let descriptions: Vec<_> =
matchers.iter().map(|m| m.pattern_description()).collect();
format!("or:[{}]", descriptions.join(", "))
}
CompositeMatcher::Not(matcher) => {
format!("not({})", matcher.pattern_description())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_matcher_basic() {
let matcher = GlobMatcher::new("*.key").unwrap();
assert!(matcher.matches(Path::new("secret.key")));
assert!(matcher.matches(Path::new("/etc/ssl/private.key")));
assert!(!matcher.matches(Path::new("keyboard.txt")));
assert!(!matcher.matches(Path::new("key")));
}
#[test]
fn test_glob_matcher_extensions() {
let tar_matcher = GlobMatcher::new("*.tar").unwrap();
let zip_matcher = GlobMatcher::new("*.zip").unwrap();
let gz_matcher = GlobMatcher::new("*.gz").unwrap();
assert!(tar_matcher.matches(Path::new("archive.tar")));
assert!(zip_matcher.matches(Path::new("archive.zip")));
assert!(gz_matcher.matches(Path::new("archive.gz")));
assert!(!tar_matcher.matches(Path::new("archive.rar")));
}
#[test]
fn test_glob_matcher_character_class() {
let matcher = GlobMatcher::new("file[0-9].txt").unwrap();
assert!(matcher.matches(Path::new("file1.txt")));
assert!(matcher.matches(Path::new("file9.txt")));
assert!(!matcher.matches(Path::new("fileA.txt")));
assert!(!matcher.matches(Path::new("file.txt")));
}
#[test]
fn test_glob_matcher_question_mark() {
let matcher = GlobMatcher::new("test?.log").unwrap();
assert!(matcher.matches(Path::new("test1.log")));
assert!(matcher.matches(Path::new("testA.log")));
assert!(!matcher.matches(Path::new("test12.log")));
assert!(!matcher.matches(Path::new("test.log")));
}
#[test]
fn test_glob_matcher_invalid_pattern() {
assert!(GlobMatcher::new("[").is_err());
}
#[test]
fn test_glob_matcher_clone() {
let matcher = GlobMatcher::new("*.pem").unwrap();
let cloned = matcher.clone_box();
assert!(cloned.matches(Path::new("cert.pem")));
assert_eq!(cloned.pattern_description(), "glob:*.pem");
}
#[test]
fn test_regex_matcher_basic() {
let matcher = RegexMatcher::new(r"\.key$").unwrap();
assert!(matcher.matches(Path::new("/etc/secret.key")));
assert!(matcher.matches(Path::new("private.key")));
assert!(!matcher.matches(Path::new("keyboard.txt")));
}
#[test]
fn test_regex_matcher_case_insensitive() {
let matcher = RegexMatcher::new(r"(?i)\.exe$").unwrap();
assert!(matcher.matches(Path::new("program.exe")));
assert!(matcher.matches(Path::new("PROGRAM.EXE")));
assert!(matcher.matches(Path::new("Program.Exe")));
}
#[test]
fn test_regex_matcher_complex() {
let matcher = RegexMatcher::new(r".*-v\d+\.\d+\.\d+\.tar\.gz$").unwrap();
assert!(matcher.matches(Path::new("/releases/app-v1.2.3.tar.gz")));
assert!(matcher.matches(Path::new("lib-v10.20.30.tar.gz")));
assert!(!matcher.matches(Path::new("app.tar.gz")));
assert!(!matcher.matches(Path::new("app-v1.tar.gz")));
}
#[test]
fn test_regex_matcher_with_size_limit() {
let matcher = RegexMatcher::with_size_limit(r"test", 1024 * 1024);
assert!(matcher.is_ok());
let _result = RegexMatcher::with_size_limit(r"(a+)+", 10);
}
#[test]
fn test_regex_matcher_invalid_pattern() {
assert!(RegexMatcher::new(r"[").is_err());
}
#[test]
fn test_regex_matcher_clone() {
let matcher = RegexMatcher::new(r"test").unwrap();
let cloned = matcher.clone_box();
assert!(cloned.matches(Path::new("/test/file")));
assert_eq!(cloned.pattern_description(), "regex:test");
}
#[test]
fn test_combined_matcher_basic() {
let matcher = CombinedMatcher::new(vec![
Box::new(GlobMatcher::new("*.key").unwrap()),
Box::new(GlobMatcher::new("*.pem").unwrap()),
]);
assert!(matcher.matches(Path::new("secret.key")));
assert!(matcher.matches(Path::new("cert.pem")));
assert!(!matcher.matches(Path::new("document.txt")));
}
#[test]
fn test_combined_matcher_add() {
let matcher = CombinedMatcher::new(vec![Box::new(GlobMatcher::new("*.key").unwrap())])
.with_matcher(Box::new(GlobMatcher::new("*.pem").unwrap()));
assert_eq!(matcher.len(), 2);
assert!(matcher.matches(Path::new("cert.pem")));
}
#[test]
fn test_combined_matcher_empty() {
let matcher = CombinedMatcher::new(vec![]);
assert!(matcher.is_empty());
assert!(!matcher.matches(Path::new("anything")));
}
#[test]
fn test_combined_matcher_clone() {
let matcher = CombinedMatcher::new(vec![
Box::new(GlobMatcher::new("*.a").unwrap()),
Box::new(GlobMatcher::new("*.b").unwrap()),
]);
let cloned = matcher.clone_box();
assert!(cloned.matches(Path::new("file.a")));
assert!(cloned.matches(Path::new("file.b")));
assert!(cloned.pattern_description().contains("any_of:"));
}
#[test]
fn test_not_matcher_basic() {
let matcher = NotMatcher::new(Box::new(GlobMatcher::new("*.key").unwrap()));
assert!(!matcher.matches(Path::new("secret.key")));
assert!(matcher.matches(Path::new("document.txt")));
}
#[test]
fn test_not_matcher_clone() {
let matcher = NotMatcher::new(Box::new(GlobMatcher::new("*.log").unwrap()));
let cloned = matcher.clone_box();
assert!(!cloned.matches(Path::new("app.log")));
assert!(cloned.matches(Path::new("app.txt")));
assert!(cloned.pattern_description().starts_with("not("));
}
#[test]
fn test_nested_matchers() {
let inner = CombinedMatcher::new(vec![
Box::new(GlobMatcher::new("*.key").unwrap()),
Box::new(GlobMatcher::new("*.pem").unwrap()),
]);
let matcher = NotMatcher::new(Box::new(inner));
assert!(!matcher.matches(Path::new("secret.key")));
assert!(!matcher.matches(Path::new("cert.pem")));
assert!(matcher.matches(Path::new("document.txt")));
}
#[test]
fn test_glob_matcher_with_paths() {
let matcher = GlobMatcher::new("/etc/**").unwrap();
assert!(matcher.matches(Path::new("/etc/passwd")));
assert!(matcher.matches(Path::new("/etc/ssh/sshd_config")));
}
#[test]
fn test_regex_matcher_path_separators() {
let matcher = RegexMatcher::new(r"/tmp/.*\.tmp$").unwrap();
assert!(matcher.matches(Path::new("/tmp/file.tmp")));
assert!(matcher.matches(Path::new("/tmp/subdir/file.tmp")));
assert!(!matcher.matches(Path::new("/var/file.tmp")));
}
#[test]
fn test_glob_match_mode_full_path_only() {
let matcher = GlobMatcher::with_mode("*.key", GlobMatchMode::FullPathOnly).unwrap();
assert!(matcher.matches(Path::new("secret.key"))); assert!(matcher.matches(Path::new("/etc/secret.key")));
}
#[test]
fn test_glob_match_mode_filename_only() {
let matcher = GlobMatcher::with_mode("secret*", GlobMatchMode::FilenameOnly).unwrap();
assert!(matcher.matches(Path::new("/etc/secret.key")));
assert!(matcher.matches(Path::new("secret_file.txt")));
assert!(!matcher.matches(Path::new("/secret/other.txt")));
}
#[test]
fn test_glob_match_mode_default() {
let matcher = GlobMatcher::new("*.key").unwrap();
assert!(matcher.matches(Path::new("secret.key")));
assert!(matcher.matches(Path::new("/etc/ssl/private.key"))); assert_eq!(matcher.mode(), GlobMatchMode::PathOrFilename);
}
#[test]
fn test_glob_matcher_pattern_accessor() {
let matcher = GlobMatcher::new("*.{key,pem}").unwrap();
assert_eq!(matcher.pattern(), "*.{key,pem}");
}
#[test]
fn test_regex_matcher_pattern_accessor() {
let matcher = RegexMatcher::new(r"(?i)\.key$").unwrap();
assert_eq!(matcher.pattern(), r"(?i)\.key$");
}
#[test]
fn test_combined_matcher_len_and_is_empty() {
let empty = CombinedMatcher::new(vec![]);
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let non_empty = CombinedMatcher::new(vec![
Box::new(GlobMatcher::new("*.key").unwrap()),
Box::new(GlobMatcher::new("*.pem").unwrap()),
]);
assert!(!non_empty.is_empty());
assert_eq!(non_empty.len(), 2);
}
#[test]
fn test_glob_match_mode_enum() {
assert_eq!(GlobMatchMode::default(), GlobMatchMode::PathOrFilename);
assert_ne!(GlobMatchMode::PathOrFilename, GlobMatchMode::FullPathOnly);
assert_ne!(GlobMatchMode::PathOrFilename, GlobMatchMode::FilenameOnly);
assert_ne!(GlobMatchMode::FullPathOnly, GlobMatchMode::FilenameOnly);
}
#[test]
fn test_all_matcher_basic() {
use crate::server::filter::path::PrefixMatcher;
let matcher = AllMatcher::new(vec![
Box::new(GlobMatcher::new("*.env").unwrap()),
Box::new(PrefixMatcher::new("/home")),
]);
assert!(matcher.matches(Path::new("/home/user/.env")));
assert!(!matcher.matches(Path::new("/etc/.env"))); assert!(!matcher.matches(Path::new("/home/user/config.txt"))); }
#[test]
fn test_all_matcher_empty() {
let matcher = AllMatcher::new(vec![]);
assert!(matcher.is_empty());
assert_eq!(matcher.len(), 0);
assert!(!matcher.matches(Path::new("/any/path")));
}
#[test]
fn test_all_matcher_single() {
let matcher = AllMatcher::new(vec![Box::new(GlobMatcher::new("*.key").unwrap())]);
assert_eq!(matcher.len(), 1);
assert!(matcher.matches(Path::new("secret.key")));
assert!(!matcher.matches(Path::new("secret.txt")));
}
#[test]
fn test_all_matcher_with_matcher() {
let matcher = AllMatcher::new(vec![Box::new(GlobMatcher::new("*.key").unwrap())])
.with_matcher(Box::new(GlobMatcher::new("secret*").unwrap()));
assert_eq!(matcher.len(), 2);
assert!(matcher.matches(Path::new("secret.key"))); assert!(!matcher.matches(Path::new("public.key"))); assert!(!matcher.matches(Path::new("secret.txt"))); }
#[test]
fn test_all_matcher_clone() {
use crate::server::filter::path::PrefixMatcher;
let matcher = AllMatcher::new(vec![
Box::new(GlobMatcher::new("*.log").unwrap()),
Box::new(PrefixMatcher::new("/var/log")),
]);
let cloned = matcher.clone_box();
assert!(cloned.matches(Path::new("/var/log/app.log")));
assert!(cloned.pattern_description().contains("all_of:"));
}
#[test]
fn test_composite_matcher_and() {
use crate::server::filter::path::PrefixMatcher;
let matcher = CompositeMatcher::and(vec![
Box::new(GlobMatcher::new("*.env").unwrap()),
Box::new(PrefixMatcher::new("/home")),
]);
assert!(matcher.matches(Path::new("/home/user/.env")));
assert!(!matcher.matches(Path::new("/etc/.env")));
assert!(matcher.pattern_description().contains("and:"));
}
#[test]
fn test_composite_matcher_or() {
let matcher = CompositeMatcher::or(vec![
Box::new(GlobMatcher::new("*.key").unwrap()),
Box::new(GlobMatcher::new("*.pem").unwrap()),
]);
assert!(matcher.matches(Path::new("secret.key")));
assert!(matcher.matches(Path::new("cert.pem")));
assert!(!matcher.matches(Path::new("document.txt")));
assert!(matcher.pattern_description().contains("or:"));
}
#[test]
fn test_composite_matcher_not() {
use crate::server::filter::path::PrefixMatcher;
let matcher = CompositeMatcher::not(Box::new(PrefixMatcher::new("/home")));
assert!(!matcher.matches(Path::new("/home/user/file")));
assert!(matcher.matches(Path::new("/etc/passwd")));
assert!(matcher.pattern_description().contains("not("));
}
#[test]
fn test_composite_matcher_empty_and() {
let matcher = CompositeMatcher::And(vec![]);
assert!(!matcher.matches(Path::new("/any/path")));
}
#[test]
fn test_composite_matcher_empty_or() {
let matcher = CompositeMatcher::Or(vec![]);
assert!(!matcher.matches(Path::new("/any/path")));
}
#[test]
fn test_composite_matcher_complex() {
use crate::server::filter::path::PrefixMatcher;
let env_not_home = CompositeMatcher::and(vec![
Box::new(GlobMatcher::new("*.env").unwrap()),
Box::new(CompositeMatcher::not(Box::new(PrefixMatcher::new("/home")))),
]);
let key_files = GlobMatcher::new("*.key").unwrap();
let matcher = CompositeMatcher::or(vec![Box::new(env_not_home), Box::new(key_files)]);
assert!(matcher.matches(Path::new("/etc/.env"))); assert!(!matcher.matches(Path::new("/home/user/.env"))); assert!(matcher.matches(Path::new("/home/user/secret.key"))); }
#[test]
fn test_composite_matcher_clone() {
let matcher = CompositeMatcher::and(vec![
Box::new(GlobMatcher::new("*.a").unwrap()),
Box::new(GlobMatcher::new("test*").unwrap()),
]);
let cloned = matcher.clone_box();
assert!(cloned.matches(Path::new("test.a")));
assert!(!cloned.matches(Path::new("test.b")));
}
}