use std::path::PathBuf;
use thiserror::Error;
use super::context::{IoOperation, ParseFormat};
#[derive(Error, Debug)]
pub enum CcAuditError {
#[error("Failed to {operation} {path}: {source}")]
Io {
path: PathBuf,
operation: IoOperation,
#[source]
source: std::io::Error,
},
#[error("Failed to parse {format} in {path}")]
Parse {
path: PathBuf,
format: ParseFormat,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("File not found: {0}")]
FileNotFound(PathBuf),
#[error("Path is not a directory: {0}")]
NotADirectory(PathBuf),
#[error("Path is not a file: {0}")]
NotAFile(PathBuf),
#[error("Invalid format in {path}: {message}")]
InvalidFormat { path: PathBuf, message: String },
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("Hook error: {0}")]
Hook(#[from] crate::hooks::HookError),
#[error("Malware database error: {0}")]
MalwareDb(#[from] crate::malware_db::MalwareDbError),
#[error("Watch error: {0}")]
Watch(#[from] notify::Error),
#[error("Configuration error: {0}")]
Config(String),
#[error("YAML parse error in {path}: {source}")]
YamlParse {
path: String,
#[source]
source: serde_norway::Error,
},
#[error("Invalid SKILL.md format: {0}")]
InvalidSkillFormat(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
impl CcAuditError {
pub fn read_error(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::Io {
path: path.into(),
operation: IoOperation::Read,
source,
}
}
pub fn write_error(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::Io {
path: path.into(),
operation: IoOperation::Write,
source,
}
}
pub fn json_parse_error(path: impl Into<PathBuf>, source: serde_json::Error) -> Self {
Self::Parse {
path: path.into(),
format: ParseFormat::Json,
source: Box::new(source),
}
}
pub fn yaml_parse_error(path: impl Into<PathBuf>, source: serde_norway::Error) -> Self {
Self::Parse {
path: path.into(),
format: ParseFormat::Yaml,
source: Box::new(source),
}
}
pub fn toml_parse_error(path: impl Into<PathBuf>, source: toml::de::Error) -> Self {
Self::Parse {
path: path.into(),
format: ParseFormat::Toml,
source: Box::new(source),
}
}
pub fn root_cause(&self) -> &dyn std::error::Error {
let mut current: &dyn std::error::Error = self;
while let Some(source) = current.source() {
current = source;
}
current
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_read_error() {
let err = CcAuditError::read_error(
"/path/to/file",
io::Error::new(io::ErrorKind::NotFound, "not found"),
);
assert!(err.to_string().contains("/path/to/file"));
assert!(err.to_string().contains("read"));
}
#[test]
fn test_write_error() {
let err = CcAuditError::write_error(
"/path/to/file",
io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
);
assert!(err.to_string().contains("/path/to/file"));
assert!(err.to_string().contains("write"));
}
#[test]
fn test_file_not_found() {
let err = CcAuditError::FileNotFound(PathBuf::from("/missing/file"));
assert!(err.to_string().contains("/missing/file"));
}
#[test]
fn test_root_cause() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "root cause");
let err = CcAuditError::read_error("/path", io_err);
let root = err.root_cause();
assert!(root.to_string().contains("root cause"));
}
#[test]
fn test_json_parse_error() {
let json_str = "{ invalid }";
let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
let err = CcAuditError::json_parse_error("/test.json", json_err);
assert!(err.to_string().contains("/test.json"));
assert!(err.to_string().contains("JSON"));
}
#[test]
fn test_yaml_parse_error() {
let yaml_str = "invalid: yaml: content";
let yaml_err = serde_norway::from_str::<serde_norway::Value>(yaml_str).unwrap_err();
let err = CcAuditError::yaml_parse_error("/test.yaml", yaml_err);
assert!(err.to_string().contains("/test.yaml"));
assert!(err.to_string().contains("YAML"));
}
#[test]
fn test_toml_parse_error() {
let toml_str = "invalid toml [";
let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
let err = CcAuditError::toml_parse_error("/test.toml", toml_err);
assert!(err.to_string().contains("/test.toml"));
assert!(err.to_string().contains("TOML"));
}
#[test]
fn test_not_a_directory() {
let err = CcAuditError::NotADirectory(PathBuf::from("/test/file"));
assert!(err.to_string().contains("/test/file"));
}
#[test]
fn test_not_a_file() {
let err = CcAuditError::NotAFile(PathBuf::from("/test/dir"));
assert!(err.to_string().contains("/test/dir"));
}
#[test]
fn test_invalid_format() {
let err = CcAuditError::InvalidFormat {
path: PathBuf::from("/test/file"),
message: "missing field".to_string(),
};
assert!(err.to_string().contains("/test/file"));
assert!(err.to_string().contains("missing field"));
}
#[test]
fn test_config_error() {
let err = CcAuditError::Config("invalid value".to_string());
assert!(err.to_string().contains("invalid value"));
}
#[test]
fn test_invalid_skill_format() {
let err = CcAuditError::InvalidSkillFormat("missing frontmatter".to_string());
assert!(err.to_string().contains("missing frontmatter"));
}
#[test]
fn test_error_debug() {
let err = CcAuditError::FileNotFound(PathBuf::from("/test"));
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("FileNotFound"));
}
#[test]
fn test_root_cause_no_source() {
let err = CcAuditError::Config("test".to_string());
let root = err.root_cause();
assert!(root.to_string().contains("test"));
}
}