use crate::Result;
use crate::error::LibmagicError;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct EvaluationConfig {
pub max_recursion_depth: u32,
pub max_string_length: usize,
pub stop_at_first_match: bool,
pub enable_mime_types: bool,
pub timeout_ms: Option<u64>,
}
impl Default for EvaluationConfig {
fn default() -> Self {
Self {
max_recursion_depth: 20,
max_string_length: 8192,
stop_at_first_match: true,
enable_mime_types: false,
timeout_ms: None,
}
}
}
impl EvaluationConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn performance() -> Self {
Self {
max_recursion_depth: 10,
max_string_length: 1024,
stop_at_first_match: true,
enable_mime_types: false,
timeout_ms: Some(1000), }
}
#[must_use]
pub const fn comprehensive() -> Self {
Self {
max_recursion_depth: 50,
max_string_length: 32768,
stop_at_first_match: false,
enable_mime_types: true,
timeout_ms: Some(30000), }
}
#[must_use]
pub const fn with_max_recursion_depth(mut self, depth: u32) -> Self {
self.max_recursion_depth = depth;
self
}
#[must_use]
pub const fn with_max_string_length(mut self, length: usize) -> Self {
self.max_string_length = length;
self
}
#[must_use]
pub const fn with_stop_at_first_match(mut self, stop: bool) -> Self {
self.stop_at_first_match = stop;
self
}
#[must_use]
pub const fn with_mime_types(mut self, enable: bool) -> Self {
self.enable_mime_types = enable;
self
}
#[must_use]
pub const fn with_timeout_ms(mut self, timeout_ms: Option<u64>) -> Self {
self.timeout_ms = timeout_ms;
self
}
pub fn validate(&self) -> Result<()> {
self.validate_recursion_depth()?;
self.validate_string_length()?;
self.validate_timeout()?;
self.validate_resource_combination()?;
Ok(())
}
fn validate_recursion_depth(&self) -> Result<()> {
const MAX_SAFE_RECURSION_DEPTH: u32 = 1000;
if self.max_recursion_depth == 0 {
return Err(LibmagicError::ConfigError {
reason: "max_recursion_depth must be greater than 0".to_string(),
});
}
if self.max_recursion_depth > MAX_SAFE_RECURSION_DEPTH {
return Err(LibmagicError::ConfigError {
reason: format!(
"max_recursion_depth must not exceed {MAX_SAFE_RECURSION_DEPTH} to prevent stack overflow"
),
});
}
Ok(())
}
fn validate_string_length(&self) -> Result<()> {
const MAX_SAFE_STRING_LENGTH: usize = 1_048_576;
if self.max_string_length == 0 {
return Err(LibmagicError::ConfigError {
reason: "max_string_length must be greater than 0".to_string(),
});
}
if self.max_string_length > MAX_SAFE_STRING_LENGTH {
return Err(LibmagicError::ConfigError {
reason: format!(
"max_string_length must not exceed {MAX_SAFE_STRING_LENGTH} bytes to prevent memory exhaustion"
),
});
}
Ok(())
}
fn validate_timeout(&self) -> Result<()> {
const MAX_SAFE_TIMEOUT_MS: u64 = 300_000;
if let Some(timeout) = self.timeout_ms {
if timeout == 0 {
return Err(LibmagicError::ConfigError {
reason: "timeout_ms must be greater than 0 if specified".to_string(),
});
}
if timeout > MAX_SAFE_TIMEOUT_MS {
return Err(LibmagicError::ConfigError {
reason: format!(
"timeout_ms must not exceed {MAX_SAFE_TIMEOUT_MS} (5 minutes) to prevent denial of service"
),
});
}
}
Ok(())
}
fn validate_resource_combination(&self) -> Result<()> {
const HIGH_RECURSION_THRESHOLD: u32 = 100;
const LARGE_STRING_THRESHOLD: usize = 65536;
if self.max_recursion_depth > HIGH_RECURSION_THRESHOLD
&& self.max_string_length > LARGE_STRING_THRESHOLD
{
return Err(LibmagicError::ConfigError {
reason: format!(
"High recursion depth (>{HIGH_RECURSION_THRESHOLD}) combined with large string length (>{LARGE_STRING_THRESHOLD}) may cause resource exhaustion"
),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_validates() {
assert!(EvaluationConfig::default().validate().is_ok());
}
#[test]
fn test_performance_validates() {
assert!(EvaluationConfig::performance().validate().is_ok());
}
#[test]
fn test_comprehensive_validates() {
assert!(EvaluationConfig::comprehensive().validate().is_ok());
}
#[test]
fn test_recursion_depth_zero_rejected() {
let cfg = EvaluationConfig {
max_recursion_depth: 0,
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_recursion_depth_one_accepted() {
let cfg = EvaluationConfig {
max_recursion_depth: 1,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_recursion_depth_at_max_accepted() {
let cfg = EvaluationConfig {
max_recursion_depth: 1000,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_recursion_depth_above_max_rejected() {
let cfg = EvaluationConfig {
max_recursion_depth: 1001,
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_string_length_zero_rejected() {
let cfg = EvaluationConfig {
max_string_length: 0,
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_string_length_one_accepted() {
let cfg = EvaluationConfig {
max_string_length: 1,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_string_length_at_max_accepted() {
let cfg = EvaluationConfig {
max_string_length: 1_048_576,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_string_length_above_max_rejected() {
let cfg = EvaluationConfig {
max_string_length: 1_048_577,
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_timeout_none_accepted() {
let cfg = EvaluationConfig {
timeout_ms: None,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_timeout_zero_rejected() {
let cfg = EvaluationConfig {
timeout_ms: Some(0),
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_timeout_one_accepted() {
let cfg = EvaluationConfig {
timeout_ms: Some(1),
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_timeout_at_max_accepted() {
let cfg = EvaluationConfig {
timeout_ms: Some(300_000),
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_timeout_above_max_rejected() {
let cfg = EvaluationConfig {
timeout_ms: Some(300_001),
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_high_recursion_with_large_string_rejected() {
let cfg = EvaluationConfig {
max_recursion_depth: 101,
max_string_length: 65537,
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn test_high_recursion_with_normal_string_accepted() {
let cfg = EvaluationConfig {
max_recursion_depth: 101,
max_string_length: 65536,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_normal_recursion_with_large_string_accepted() {
let cfg = EvaluationConfig {
max_recursion_depth: 100,
max_string_length: 65537,
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn test_evaluate_rules_with_config_rejects_invalid() {
use crate::evaluator::evaluate_rules_with_config;
let invalid_cfg = EvaluationConfig {
max_recursion_depth: 0,
..Default::default()
};
let result = evaluate_rules_with_config(&[], &[], &invalid_cfg);
assert!(result.is_err());
}
}