use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ScanError {
#[error("engine '{engine}' is unavailable: {reason}")]
EngineUnavailable {
engine: String,
reason: String,
},
#[error("scan timed out after {elapsed:?} on engine '{engine}'")]
Timeout {
engine: String,
elapsed: Duration,
},
#[error("connection to engine '{engine}' failed: {message}")]
ConnectionFailed {
engine: String,
message: String,
},
#[error("malformed file: {reason}")]
MalformedFile {
reason: String,
},
#[error("file size {size} bytes exceeds maximum {max} bytes")]
FileTooLarge {
size: u64,
max: u64,
},
#[error("circuit breaker open for engine '{engine}'")]
CircuitOpen {
engine: String,
recovery_hint: Option<String>,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("file not found: {path}")]
FileNotFound {
path: String,
},
#[error("ambiguous response from engine '{engine}': {details}")]
AmbiguousResponse {
engine: String,
details: String,
},
#[error("rate limit exceeded for engine '{engine}': retry after {retry_after:?}")]
RateLimited {
engine: String,
retry_after: Option<Duration>,
},
#[error("authentication failed for engine '{engine}': {reason}")]
AuthenticationFailed {
engine: String,
reason: String,
},
#[error("scan was cancelled")]
Cancelled,
#[error("internal error: {message}")]
Internal {
message: String,
},
#[error("configuration error: {message}")]
Configuration {
message: String,
},
}
impl ScanError {
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::Timeout { .. }
| Self::ConnectionFailed { .. }
| Self::RateLimited { .. }
| Self::CircuitOpen { .. }
)
}
pub fn indicates_unhealthy_engine(&self) -> bool {
matches!(
self,
Self::EngineUnavailable { .. }
| Self::Timeout { .. }
| Self::ConnectionFailed { .. }
| Self::AuthenticationFailed { .. }
)
}
pub fn engine(&self) -> Option<&str> {
match self {
Self::EngineUnavailable { engine, .. }
| Self::Timeout { engine, .. }
| Self::ConnectionFailed { engine, .. }
| Self::CircuitOpen { engine, .. }
| Self::AmbiguousResponse { engine, .. }
| Self::RateLimited { engine, .. }
| Self::AuthenticationFailed { engine, .. } => Some(engine),
_ => None,
}
}
pub fn engine_unavailable(engine: impl Into<String>, reason: impl Into<String>) -> Self {
Self::EngineUnavailable {
engine: engine.into(),
reason: reason.into(),
}
}
pub fn timeout(engine: impl Into<String>, elapsed: Duration) -> Self {
Self::Timeout {
engine: engine.into(),
elapsed,
}
}
pub fn connection_failed(engine: impl Into<String>, message: impl Into<String>) -> Self {
Self::ConnectionFailed {
engine: engine.into(),
message: message.into(),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
}
}
pub fn configuration(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
}
}
}
#[derive(Debug, Error)]
pub enum QuarantineError {
#[error("failed to store file in quarantine: {reason}")]
StoreFailed {
reason: String,
},
#[error("quarantine record not found: {id}")]
NotFound {
id: String,
},
#[error("failed to retrieve file from quarantine: {reason}")]
RetrieveFailed {
reason: String,
},
#[error("failed to delete file from quarantine: {reason}")]
DeleteFailed {
reason: String,
},
#[error("quarantine storage is full")]
StorageFull,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("file integrity check failed: expected {expected}, got {actual}")]
IntegrityCheckFailed {
expected: String,
actual: String,
},
}
#[derive(Debug, Error)]
pub enum PolicyError {
#[error("no matching policy rule for the given context")]
NoMatchingRule,
#[error("invalid policy rule '{rule_id}': {reason}")]
InvalidRule {
rule_id: String,
reason: String,
},
#[error("policy evaluation failed: {reason}")]
EvaluationFailed {
reason: String,
},
}
pub type ScanResult<T> = Result<T, ScanError>;
pub type QuarantineResult<T> = Result<T, QuarantineError>;
pub type PolicyResult<T> = Result<T, PolicyError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_error_is_recoverable() {
let timeout = ScanError::timeout("test", Duration::from_secs(30));
assert!(timeout.is_recoverable());
let malformed = ScanError::MalformedFile {
reason: "corrupt header".into(),
};
assert!(!malformed.is_recoverable());
}
#[test]
fn test_scan_error_engine() {
let err = ScanError::engine_unavailable("clamav", "service not running");
assert_eq!(err.engine(), Some("clamav"));
let io_err = ScanError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"test error",
));
assert_eq!(io_err.engine(), None);
}
#[test]
fn test_scan_error_display() {
let err = ScanError::FileTooLarge {
size: 100_000_000,
max: 50_000_000,
};
assert!(err.to_string().contains("100000000"));
assert!(err.to_string().contains("50000000"));
}
}