Skip to main content

cc_audit/error/
audit.rs

1//! Unified error type for cc-audit.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6use super::context::{IoOperation, ParseFormat};
7
8/// Unified error type for all cc-audit operations.
9#[derive(Error, Debug)]
10pub enum CcAuditError {
11    /// I/O operation failed.
12    #[error("Failed to {operation} {path}: {source}")]
13    Io {
14        path: PathBuf,
15        operation: IoOperation,
16        #[source]
17        source: std::io::Error,
18    },
19
20    /// Parse error with preserved source.
21    #[error("Failed to parse {format} in {path}")]
22    Parse {
23        path: PathBuf,
24        format: ParseFormat,
25        #[source]
26        source: Box<dyn std::error::Error + Send + Sync>,
27    },
28
29    /// File not found.
30    #[error("File not found: {0}")]
31    FileNotFound(PathBuf),
32
33    /// Path is not a directory.
34    #[error("Path is not a directory: {0}")]
35    NotADirectory(PathBuf),
36
37    /// Path is not a file.
38    #[error("Path is not a file: {0}")]
39    NotAFile(PathBuf),
40
41    /// Invalid format with message.
42    #[error("Invalid format in {path}: {message}")]
43    InvalidFormat { path: PathBuf, message: String },
44
45    /// Regex compilation error.
46    #[error("Regex error: {0}")]
47    Regex(#[from] regex::Error),
48
49    /// Hook operation failed.
50    #[error("Hook error: {0}")]
51    Hook(#[from] crate::hooks::HookError),
52
53    /// Malware database error.
54    #[error("Malware database error: {0}")]
55    MalwareDb(#[from] crate::malware_db::MalwareDbError),
56
57    /// File watch error.
58    #[error("Watch error: {0}")]
59    Watch(#[from] notify::Error),
60
61    /// Configuration error.
62    #[error("Configuration error: {0}")]
63    Config(String),
64
65    /// YAML parse error (legacy compatibility).
66    #[error("YAML parse error in {path}: {source}")]
67    YamlParse {
68        path: String,
69        #[source]
70        source: serde_yaml::Error,
71    },
72
73    /// Invalid skill format (legacy compatibility).
74    #[error("Invalid SKILL.md format: {0}")]
75    InvalidSkillFormat(String),
76
77    /// JSON error.
78    #[error("JSON error: {0}")]
79    Json(#[from] serde_json::Error),
80}
81
82impl CcAuditError {
83    /// Create an I/O read error.
84    pub fn read_error(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
85        Self::Io {
86            path: path.into(),
87            operation: IoOperation::Read,
88            source,
89        }
90    }
91
92    /// Create an I/O write error.
93    pub fn write_error(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
94        Self::Io {
95            path: path.into(),
96            operation: IoOperation::Write,
97            source,
98        }
99    }
100
101    /// Create a parse error with JSON format.
102    pub fn json_parse_error(path: impl Into<PathBuf>, source: serde_json::Error) -> Self {
103        Self::Parse {
104            path: path.into(),
105            format: ParseFormat::Json,
106            source: Box::new(source),
107        }
108    }
109
110    /// Create a parse error with YAML format.
111    pub fn yaml_parse_error(path: impl Into<PathBuf>, source: serde_yaml::Error) -> Self {
112        Self::Parse {
113            path: path.into(),
114            format: ParseFormat::Yaml,
115            source: Box::new(source),
116        }
117    }
118
119    /// Create a parse error with TOML format.
120    pub fn toml_parse_error(path: impl Into<PathBuf>, source: toml::de::Error) -> Self {
121        Self::Parse {
122            path: path.into(),
123            format: ParseFormat::Toml,
124            source: Box::new(source),
125        }
126    }
127
128    /// Get the root cause of the error chain.
129    pub fn root_cause(&self) -> &dyn std::error::Error {
130        let mut current: &dyn std::error::Error = self;
131        while let Some(source) = current.source() {
132            current = source;
133        }
134        current
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::io;
142
143    #[test]
144    fn test_read_error() {
145        let err = CcAuditError::read_error(
146            "/path/to/file",
147            io::Error::new(io::ErrorKind::NotFound, "not found"),
148        );
149        assert!(err.to_string().contains("/path/to/file"));
150        assert!(err.to_string().contains("read"));
151    }
152
153    #[test]
154    fn test_write_error() {
155        let err = CcAuditError::write_error(
156            "/path/to/file",
157            io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
158        );
159        assert!(err.to_string().contains("/path/to/file"));
160        assert!(err.to_string().contains("write"));
161    }
162
163    #[test]
164    fn test_file_not_found() {
165        let err = CcAuditError::FileNotFound(PathBuf::from("/missing/file"));
166        assert!(err.to_string().contains("/missing/file"));
167    }
168
169    #[test]
170    fn test_root_cause() {
171        let io_err = io::Error::new(io::ErrorKind::NotFound, "root cause");
172        let err = CcAuditError::read_error("/path", io_err);
173        let root = err.root_cause();
174        assert!(root.to_string().contains("root cause"));
175    }
176}