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
177    #[test]
178    fn test_json_parse_error() {
179        let json_str = "{ invalid }";
180        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
181        let err = CcAuditError::json_parse_error("/test.json", json_err);
182        assert!(err.to_string().contains("/test.json"));
183        assert!(err.to_string().contains("JSON"));
184    }
185
186    #[test]
187    fn test_yaml_parse_error() {
188        let yaml_str = "invalid: yaml: content";
189        let yaml_err = serde_yaml::from_str::<serde_yaml::Value>(yaml_str).unwrap_err();
190        let err = CcAuditError::yaml_parse_error("/test.yaml", yaml_err);
191        assert!(err.to_string().contains("/test.yaml"));
192        assert!(err.to_string().contains("YAML"));
193    }
194
195    #[test]
196    fn test_toml_parse_error() {
197        let toml_str = "invalid toml [";
198        let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
199        let err = CcAuditError::toml_parse_error("/test.toml", toml_err);
200        assert!(err.to_string().contains("/test.toml"));
201        assert!(err.to_string().contains("TOML"));
202    }
203
204    #[test]
205    fn test_not_a_directory() {
206        let err = CcAuditError::NotADirectory(PathBuf::from("/test/file"));
207        assert!(err.to_string().contains("/test/file"));
208    }
209
210    #[test]
211    fn test_not_a_file() {
212        let err = CcAuditError::NotAFile(PathBuf::from("/test/dir"));
213        assert!(err.to_string().contains("/test/dir"));
214    }
215
216    #[test]
217    fn test_invalid_format() {
218        let err = CcAuditError::InvalidFormat {
219            path: PathBuf::from("/test/file"),
220            message: "missing field".to_string(),
221        };
222        assert!(err.to_string().contains("/test/file"));
223        assert!(err.to_string().contains("missing field"));
224    }
225
226    #[test]
227    fn test_config_error() {
228        let err = CcAuditError::Config("invalid value".to_string());
229        assert!(err.to_string().contains("invalid value"));
230    }
231
232    #[test]
233    fn test_invalid_skill_format() {
234        let err = CcAuditError::InvalidSkillFormat("missing frontmatter".to_string());
235        assert!(err.to_string().contains("missing frontmatter"));
236    }
237
238    #[test]
239    fn test_error_debug() {
240        let err = CcAuditError::FileNotFound(PathBuf::from("/test"));
241        let debug_str = format!("{:?}", err);
242        assert!(debug_str.contains("FileNotFound"));
243    }
244
245    #[test]
246    fn test_root_cause_no_source() {
247        let err = CcAuditError::Config("test".to_string());
248        let root = err.root_cause();
249        // For errors without source, root cause is the error itself
250        assert!(root.to_string().contains("test"));
251    }
252}