pub mod path;
pub mod pattern;
pub mod policy;
use std::fmt;
use std::path::Path;
pub use self::path::{
normalize_path, ComponentMatcher, ExactMatcher, ExtensionMatcher, MultiExtensionMatcher,
PrefixMatcher, SizeMatcher,
};
pub use self::pattern::{
AllMatcher, CombinedMatcher, CompositeMatcher, GlobMatcher, NotMatcher, RegexMatcher,
};
pub use self::policy::{FilterPolicy, FilterRule, Matcher, SharedFilterPolicy};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Operation {
Upload,
Download,
Delete,
Rename,
CreateDir,
ListDir,
Stat,
SetStat,
Symlink,
ReadLink,
}
impl Operation {
pub fn all() -> &'static [Operation] {
&[
Operation::Upload,
Operation::Download,
Operation::Delete,
Operation::Rename,
Operation::CreateDir,
Operation::ListDir,
Operation::Stat,
Operation::SetStat,
Operation::Symlink,
Operation::ReadLink,
]
}
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Operation::Upload => write!(f, "upload"),
Operation::Download => write!(f, "download"),
Operation::Delete => write!(f, "delete"),
Operation::Rename => write!(f, "rename"),
Operation::CreateDir => write!(f, "createdir"),
Operation::ListDir => write!(f, "listdir"),
Operation::Stat => write!(f, "stat"),
Operation::SetStat => write!(f, "setstat"),
Operation::Symlink => write!(f, "symlink"),
Operation::ReadLink => write!(f, "readlink"),
}
}
}
impl std::str::FromStr for Operation {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"upload" => Ok(Operation::Upload),
"download" => Ok(Operation::Download),
"delete" => Ok(Operation::Delete),
"rename" => Ok(Operation::Rename),
"createdir" | "mkdir" => Ok(Operation::CreateDir),
"listdir" | "readdir" => Ok(Operation::ListDir),
"stat" => Ok(Operation::Stat),
"setstat" => Ok(Operation::SetStat),
"symlink" => Ok(Operation::Symlink),
"readlink" => Ok(Operation::ReadLink),
_ => Err(format!("unknown operation: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilterResult {
#[default]
Allow,
Deny,
Log,
}
impl fmt::Display for FilterResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FilterResult::Allow => write!(f, "allow"),
FilterResult::Deny => write!(f, "deny"),
FilterResult::Log => write!(f, "log"),
}
}
}
pub trait TransferFilter: Send + Sync {
fn check(&self, path: &Path, operation: Operation, user: &str) -> FilterResult;
fn check_with_dest(
&self,
src: &Path,
dest: &Path,
operation: Operation,
user: &str,
) -> FilterResult {
let src_result = self.check(src, operation, user);
let dest_result = self.check(dest, operation, user);
match (src_result, dest_result) {
(FilterResult::Deny, _) | (_, FilterResult::Deny) => FilterResult::Deny,
(FilterResult::Log, _) | (_, FilterResult::Log) => FilterResult::Log,
_ => FilterResult::Allow,
}
}
fn is_enabled(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Default)]
pub struct NoOpFilter;
impl TransferFilter for NoOpFilter {
fn check(&self, _path: &Path, _operation: Operation, _user: &str) -> FilterResult {
FilterResult::Allow
}
fn is_enabled(&self) -> bool {
false
}
}
pub trait SizeAwareFilter: TransferFilter {
fn check_with_size(
&self,
path: &Path,
size: u64,
operation: Operation,
user: &str,
) -> FilterResult;
fn check_with_size_dest(
&self,
src: &Path,
src_size: u64,
dest: &Path,
operation: Operation,
user: &str,
) -> FilterResult {
let src_result = self.check_with_size(src, src_size, operation, user);
let dest_result = self.check(dest, operation, user);
match (src_result, dest_result) {
(FilterResult::Deny, _) | (_, FilterResult::Deny) => FilterResult::Deny,
(FilterResult::Log, _) | (_, FilterResult::Log) => FilterResult::Log,
_ => FilterResult::Allow,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operation_display() {
assert_eq!(Operation::Upload.to_string(), "upload");
assert_eq!(Operation::Download.to_string(), "download");
assert_eq!(Operation::Delete.to_string(), "delete");
assert_eq!(Operation::Rename.to_string(), "rename");
assert_eq!(Operation::CreateDir.to_string(), "createdir");
assert_eq!(Operation::ListDir.to_string(), "listdir");
}
#[test]
fn test_operation_parse() {
assert_eq!("upload".parse::<Operation>().unwrap(), Operation::Upload);
assert_eq!(
"DOWNLOAD".parse::<Operation>().unwrap(),
Operation::Download
);
assert_eq!("mkdir".parse::<Operation>().unwrap(), Operation::CreateDir);
assert_eq!("readdir".parse::<Operation>().unwrap(), Operation::ListDir);
assert!("invalid".parse::<Operation>().is_err());
}
#[test]
fn test_filter_result_default() {
assert_eq!(FilterResult::default(), FilterResult::Allow);
}
#[test]
fn test_filter_result_display() {
assert_eq!(FilterResult::Allow.to_string(), "allow");
assert_eq!(FilterResult::Deny.to_string(), "deny");
assert_eq!(FilterResult::Log.to_string(), "log");
}
#[test]
fn test_noop_filter() {
let filter = NoOpFilter;
assert!(!filter.is_enabled());
assert_eq!(
filter.check(Path::new("/any/path"), Operation::Upload, "user"),
FilterResult::Allow
);
}
#[test]
fn test_check_with_dest_deny_takes_precedence() {
struct DenyDownload;
impl TransferFilter for DenyDownload {
fn check(&self, path: &Path, _operation: Operation, _user: &str) -> FilterResult {
if path.to_string_lossy().contains("secret") {
FilterResult::Deny
} else {
FilterResult::Allow
}
}
}
let filter = DenyDownload;
assert_eq!(
filter.check_with_dest(
Path::new("/safe/src"),
Path::new("/safe/dest"),
Operation::Rename,
"user"
),
FilterResult::Allow
);
assert_eq!(
filter.check_with_dest(
Path::new("/secret/src"),
Path::new("/safe/dest"),
Operation::Rename,
"user"
),
FilterResult::Deny
);
assert_eq!(
filter.check_with_dest(
Path::new("/safe/src"),
Path::new("/secret/dest"),
Operation::Rename,
"user"
),
FilterResult::Deny
);
}
#[test]
fn test_check_with_dest_log_priority() {
struct LogSensitive;
impl TransferFilter for LogSensitive {
fn check(&self, path: &Path, _operation: Operation, _user: &str) -> FilterResult {
if path.to_string_lossy().contains("sensitive") {
FilterResult::Log
} else {
FilterResult::Allow
}
}
}
let filter = LogSensitive;
assert_eq!(
filter.check_with_dest(
Path::new("/sensitive/src"),
Path::new("/safe/dest"),
Operation::Rename,
"user"
),
FilterResult::Log
);
assert_eq!(
filter.check_with_dest(
Path::new("/safe/src"),
Path::new("/sensitive/dest"),
Operation::Rename,
"user"
),
FilterResult::Log
);
}
#[test]
fn test_operation_all() {
let all_ops = Operation::all();
assert_eq!(all_ops.len(), 10);
assert!(all_ops.contains(&Operation::Upload));
assert!(all_ops.contains(&Operation::Download));
assert!(all_ops.contains(&Operation::Delete));
assert!(all_ops.contains(&Operation::Rename));
assert!(all_ops.contains(&Operation::CreateDir));
assert!(all_ops.contains(&Operation::ListDir));
assert!(all_ops.contains(&Operation::Stat));
assert!(all_ops.contains(&Operation::SetStat));
assert!(all_ops.contains(&Operation::Symlink));
assert!(all_ops.contains(&Operation::ReadLink));
}
#[test]
fn test_operation_display_all() {
assert_eq!(Operation::Stat.to_string(), "stat");
assert_eq!(Operation::SetStat.to_string(), "setstat");
assert_eq!(Operation::Symlink.to_string(), "symlink");
assert_eq!(Operation::ReadLink.to_string(), "readlink");
}
#[test]
fn test_operation_parse_all_variants() {
assert_eq!("stat".parse::<Operation>().unwrap(), Operation::Stat);
assert_eq!("setstat".parse::<Operation>().unwrap(), Operation::SetStat);
assert_eq!("symlink".parse::<Operation>().unwrap(), Operation::Symlink);
assert_eq!(
"readlink".parse::<Operation>().unwrap(),
Operation::ReadLink
);
assert_eq!("STAT".parse::<Operation>().unwrap(), Operation::Stat);
assert_eq!("SetStat".parse::<Operation>().unwrap(), Operation::SetStat);
}
#[test]
fn test_noop_filter_default() {
let filter = NoOpFilter;
assert!(!filter.is_enabled());
}
#[test]
fn test_noop_filter_clone() {
let filter = NoOpFilter;
let cloned = filter.clone();
assert!(!cloned.is_enabled());
}
}