#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AllowedFeatures {
pub symlinks: bool,
pub hardlinks: bool,
pub absolute_paths: bool,
pub world_writable: bool,
}
#[derive(Debug, Clone)]
pub struct SecurityConfig {
pub max_file_size: u64,
pub max_total_size: u64,
pub max_compression_ratio: f64,
pub max_file_count: usize,
pub max_path_depth: usize,
pub allowed: AllowedFeatures,
pub preserve_permissions: bool,
pub allowed_extensions: Vec<String>,
pub banned_path_components: Vec<String>,
pub allow_solid_archives: bool,
pub max_solid_block_memory: u64,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
max_file_size: 50 * 1024 * 1024, max_total_size: 500 * 1024 * 1024, max_compression_ratio: 100.0,
max_file_count: 10_000,
max_path_depth: 32,
allowed: AllowedFeatures::default(), preserve_permissions: false,
allowed_extensions: Vec::new(),
banned_path_components: vec![
".git".to_string(),
".ssh".to_string(),
".gnupg".to_string(),
".aws".to_string(),
".kube".to_string(),
".docker".to_string(),
".env".to_string(),
],
allow_solid_archives: false,
max_solid_block_memory: 512 * 1024 * 1024, }
}
}
impl SecurityConfig {
#[must_use]
pub fn permissive() -> Self {
Self {
allowed: AllowedFeatures {
symlinks: true,
hardlinks: true,
absolute_paths: true,
world_writable: true,
},
preserve_permissions: true,
max_compression_ratio: 1000.0,
banned_path_components: Vec::new(),
allow_solid_archives: true,
max_solid_block_memory: 1024 * 1024 * 1024, ..Default::default()
}
}
#[must_use]
pub fn is_path_component_allowed(&self, component: &str) -> bool {
!self
.banned_path_components
.iter()
.any(|banned| banned.eq_ignore_ascii_case(component))
}
#[must_use]
pub fn is_extension_allowed(&self, extension: &str) -> bool {
if self.allowed_extensions.is_empty() {
return true;
}
self.allowed_extensions
.iter()
.any(|ext| ext.eq_ignore_ascii_case(extension))
}
}
#[derive(Debug, Clone)]
pub struct ExtractionOptions {
pub atomic: bool,
pub skip_duplicates: bool,
}
impl Default for ExtractionOptions {
fn default() -> Self {
Self {
atomic: false,
skip_duplicates: true,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SecurityConfig::default();
assert!(!config.allowed.symlinks);
assert!(!config.allowed.hardlinks);
assert!(!config.allowed.absolute_paths);
assert_eq!(config.max_file_size, 50 * 1024 * 1024);
}
#[test]
fn test_permissive_config() {
let config = SecurityConfig::permissive();
assert!(config.allowed.symlinks);
assert!(config.allowed.hardlinks);
assert!(config.allowed.absolute_paths);
}
#[test]
fn test_extension_allowed_empty_list() {
let config = SecurityConfig::default();
assert!(config.is_extension_allowed("txt"));
assert!(config.is_extension_allowed("pdf"));
}
#[test]
fn test_extension_allowed_with_list() {
let mut config = SecurityConfig::default();
config.allowed_extensions = vec!["txt".to_string(), "pdf".to_string()];
assert!(config.is_extension_allowed("txt"));
assert!(config.is_extension_allowed("TXT"));
assert!(!config.is_extension_allowed("exe"));
}
#[test]
fn test_path_component_allowed() {
let config = SecurityConfig::default();
assert!(config.is_path_component_allowed("src"));
assert!(!config.is_path_component_allowed(".git"));
assert!(!config.is_path_component_allowed(".ssh"));
assert!(!config.is_path_component_allowed(".Git"));
assert!(!config.is_path_component_allowed(".GIT"));
assert!(!config.is_path_component_allowed(".SSH"));
assert!(!config.is_path_component_allowed(".Gnupg"));
}
#[test]
fn test_config_default_security_flags() {
let config = SecurityConfig::default();
assert!(
!config.allowed.symlinks,
"symlinks should be denied by default"
);
assert!(
!config.allowed.hardlinks,
"hardlinks should be denied by default"
);
assert!(
!config.allowed.absolute_paths,
"absolute paths should be denied by default"
);
assert!(
!config.preserve_permissions,
"permissions should not be preserved by default"
);
assert!(
!config.allowed.world_writable,
"world-writable should be denied by default"
);
}
#[test]
fn test_config_permissive_security_flags() {
let config = SecurityConfig::permissive();
assert!(config.allowed.symlinks, "permissive allows symlinks");
assert!(config.allowed.hardlinks, "permissive allows hardlinks");
assert!(
config.allowed.absolute_paths,
"permissive allows absolute paths"
);
assert!(
config.preserve_permissions,
"permissive preserves permissions"
);
assert!(
config.allowed.world_writable,
"permissive allows world-writable"
);
}
#[test]
fn test_config_quota_limits() {
let config = SecurityConfig::default();
assert_eq!(config.max_file_size, 50 * 1024 * 1024, "50 MB file limit");
assert_eq!(
config.max_total_size,
500 * 1024 * 1024,
"500 MB total limit"
);
assert_eq!(config.max_file_count, 10_000, "10k file count limit");
assert_eq!(config.max_path_depth, 32, "32 level depth limit");
#[allow(clippy::float_cmp)]
{
assert_eq!(
config.max_compression_ratio, 100.0,
"100x compression ratio limit"
);
}
}
#[test]
fn test_config_banned_components_not_empty() {
let config = SecurityConfig::default();
assert!(
!config.banned_path_components.is_empty(),
"should have banned components by default"
);
assert!(
config.banned_path_components.contains(&".git".to_string()),
"should ban .git"
);
assert!(
config.banned_path_components.contains(&".ssh".to_string()),
"should ban .ssh"
);
}
#[test]
fn test_config_solid_archives_default() {
let config = SecurityConfig::default();
assert!(
!config.allow_solid_archives,
"solid archives should be denied by default"
);
assert_eq!(
config.max_solid_block_memory,
512 * 1024 * 1024,
"max solid block memory should be 512 MB"
);
}
#[test]
fn test_config_permissive_solid_archives() {
let config = SecurityConfig::permissive();
assert!(
config.allow_solid_archives,
"permissive config should allow solid archives"
);
assert_eq!(
config.max_solid_block_memory,
1024 * 1024 * 1024,
"permissive should have 1 GB solid block limit"
);
}
}