Skip to main content

cc_audit/error/
mod.rs

1//! Error types for cc-audit.
2//!
3//! This module provides a unified error handling system with:
4//! - `CcAuditError`: The new unified error type with full context preservation
5//! - `AuditError`: Legacy error type for backwards compatibility
6//! - Context types for better error messages
7
8mod audit;
9mod context;
10
11pub use audit::CcAuditError;
12pub use context::{IoOperation, ParseFormat};
13
14use crate::hooks::HookError;
15use crate::malware_db::MalwareDbError;
16use thiserror::Error;
17
18/// Legacy error type for backwards compatibility.
19///
20/// New code should prefer using `CcAuditError` for better error context.
21#[derive(Error, Debug)]
22pub enum AuditError {
23    #[error("File not found: {0}")]
24    FileNotFound(String),
25
26    #[error("Failed to read file: {path}")]
27    ReadError {
28        path: String,
29        #[source]
30        source: std::io::Error,
31    },
32
33    #[error("File too large to scan: {path} ({size} bytes exceeds limit of {limit} bytes)")]
34    FileTooLarge { path: String, size: u64, limit: u64 },
35
36    #[error("Failed to parse YAML frontmatter: {path}")]
37    YamlParseError {
38        path: String,
39        #[source]
40        source: serde_norway::Error,
41    },
42
43    #[error("Invalid SKILL.md format: {0}")]
44    InvalidSkillFormat(String),
45
46    #[error("Regex compilation error: {0}")]
47    RegexError(#[from] regex::Error),
48
49    #[error("Path is not a directory: {0}")]
50    NotADirectory(String),
51
52    #[error("JSON serialization error: {0}")]
53    JsonError(#[from] serde_json::Error),
54
55    #[error("Failed to parse file: {path} - {message}")]
56    ParseError { path: String, message: String },
57
58    #[error("Hook operation failed: {0}")]
59    Hook(#[from] HookError),
60
61    #[error("Malware database error: {0}")]
62    MalwareDb(#[from] MalwareDbError),
63
64    #[error("File watch error: {0}")]
65    Watch(#[from] notify::Error),
66
67    #[error("Configuration error: {0}")]
68    Config(String),
69}
70
71/// Result type alias for operations using the legacy AuditError.
72pub type Result<T> = std::result::Result<T, AuditError>;
73
74/// Result type alias for operations using the new CcAuditError.
75pub type CcResult<T> = std::result::Result<T, CcAuditError>;
76
77/// Convert from CcAuditError to AuditError for backwards compatibility.
78impl From<CcAuditError> for AuditError {
79    fn from(err: CcAuditError) -> Self {
80        match err {
81            CcAuditError::Io { path, source, .. } => AuditError::ReadError {
82                path: path.display().to_string(),
83                source,
84            },
85            CcAuditError::Parse { path, .. } => AuditError::ParseError {
86                path: path.display().to_string(),
87                message: "parse error".to_string(),
88            },
89            CcAuditError::FileNotFound(path) => {
90                AuditError::FileNotFound(path.display().to_string())
91            }
92            CcAuditError::NotADirectory(path) => {
93                AuditError::NotADirectory(path.display().to_string())
94            }
95            CcAuditError::NotAFile(path) => AuditError::NotADirectory(path.display().to_string()),
96            CcAuditError::InvalidFormat { path, message } => AuditError::ParseError {
97                path: path.display().to_string(),
98                message,
99            },
100            CcAuditError::Regex(e) => AuditError::RegexError(e),
101            CcAuditError::Hook(e) => AuditError::Hook(e),
102            CcAuditError::MalwareDb(e) => AuditError::MalwareDb(e),
103            CcAuditError::Watch(e) => AuditError::Watch(e),
104            CcAuditError::Config(s) => AuditError::Config(s),
105            CcAuditError::YamlParse { path, source } => AuditError::YamlParseError { path, source },
106            CcAuditError::InvalidSkillFormat(s) => AuditError::InvalidSkillFormat(s),
107            CcAuditError::Json(e) => AuditError::JsonError(e),
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_error_display_file_not_found() {
118        let err = AuditError::FileNotFound("/path/to/file".to_string());
119        assert_eq!(err.to_string(), "File not found: /path/to/file");
120    }
121
122    #[test]
123    fn test_error_display_read_error() {
124        let err = AuditError::ReadError {
125            path: "/path/to/file".to_string(),
126            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
127        };
128        assert_eq!(err.to_string(), "Failed to read file: /path/to/file");
129    }
130
131    #[test]
132    fn test_error_display_invalid_skill_format() {
133        let err = AuditError::InvalidSkillFormat("missing frontmatter".to_string());
134        assert_eq!(
135            err.to_string(),
136            "Invalid SKILL.md format: missing frontmatter"
137        );
138    }
139
140    #[test]
141    fn test_error_display_not_a_directory() {
142        let err = AuditError::NotADirectory("/path/to/file".to_string());
143        assert_eq!(err.to_string(), "Path is not a directory: /path/to/file");
144    }
145
146    #[test]
147    fn test_error_display_parse_error() {
148        let err = AuditError::ParseError {
149            path: "/path/to/file".to_string(),
150            message: "invalid JSON".to_string(),
151        };
152        assert_eq!(
153            err.to_string(),
154            "Failed to parse file: /path/to/file - invalid JSON"
155        );
156    }
157
158    #[test]
159    fn test_error_from_hook_error() {
160        let hook_error = HookError::NotAGitRepository;
161        let err: AuditError = hook_error.into();
162        assert!(err.to_string().contains("Hook operation failed"));
163    }
164
165    #[test]
166    fn test_error_from_malware_db_error() {
167        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
168        let malware_error = MalwareDbError::ReadFile(io_error);
169        let err: AuditError = malware_error.into();
170        assert!(err.to_string().contains("Malware database error"));
171    }
172
173    #[test]
174    fn test_error_display_config() {
175        let err = AuditError::Config("invalid value".to_string());
176        assert_eq!(err.to_string(), "Configuration error: invalid value");
177    }
178
179    #[test]
180    fn test_cc_audit_error_to_audit_error() {
181        let cc_err = CcAuditError::FileNotFound(std::path::PathBuf::from("/test/path"));
182        let audit_err: AuditError = cc_err.into();
183        assert!(audit_err.to_string().contains("/test/path"));
184    }
185
186    #[test]
187    fn test_cc_audit_error_io_to_audit_error() {
188        let cc_err = CcAuditError::Io {
189            path: std::path::PathBuf::from("/test/path"),
190            operation: IoOperation::Read,
191            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
192        };
193        let audit_err: AuditError = cc_err.into();
194        assert!(matches!(audit_err, AuditError::ReadError { .. }));
195    }
196
197    #[test]
198    fn test_cc_audit_error_parse_to_audit_error() {
199        let cc_err = CcAuditError::Parse {
200            path: std::path::PathBuf::from("/test/file.json"),
201            format: ParseFormat::Json,
202            source: Box::new(std::io::Error::new(
203                std::io::ErrorKind::InvalidData,
204                "syntax error",
205            )),
206        };
207        let audit_err: AuditError = cc_err.into();
208        assert!(matches!(audit_err, AuditError::ParseError { .. }));
209    }
210
211    #[test]
212    fn test_cc_audit_error_not_a_directory() {
213        let cc_err = CcAuditError::NotADirectory(std::path::PathBuf::from("/test/file"));
214        let audit_err: AuditError = cc_err.into();
215        assert!(matches!(audit_err, AuditError::NotADirectory(_)));
216    }
217
218    #[test]
219    fn test_cc_audit_error_not_a_file() {
220        let cc_err = CcAuditError::NotAFile(std::path::PathBuf::from("/test/dir"));
221        let audit_err: AuditError = cc_err.into();
222        // NotAFile maps to NotADirectory for backwards compat
223        assert!(matches!(audit_err, AuditError::NotADirectory(_)));
224    }
225
226    #[test]
227    fn test_cc_audit_error_invalid_format() {
228        let cc_err = CcAuditError::InvalidFormat {
229            path: std::path::PathBuf::from("/test/file"),
230            message: "invalid format".to_string(),
231        };
232        let audit_err: AuditError = cc_err.into();
233        assert!(matches!(audit_err, AuditError::ParseError { .. }));
234    }
235
236    #[test]
237    #[allow(clippy::invalid_regex)]
238    fn test_cc_audit_error_regex() {
239        let regex_err = regex::Regex::new(r"[invalid\[").unwrap_err();
240        let cc_err = CcAuditError::Regex(regex_err);
241        let audit_err: AuditError = cc_err.into();
242        assert!(matches!(audit_err, AuditError::RegexError(_)));
243    }
244
245    #[test]
246    fn test_cc_audit_error_config() {
247        let cc_err = CcAuditError::Config("bad config".to_string());
248        let audit_err: AuditError = cc_err.into();
249        assert!(matches!(audit_err, AuditError::Config(_)));
250    }
251
252    #[test]
253    fn test_cc_audit_error_invalid_skill_format() {
254        let cc_err = CcAuditError::InvalidSkillFormat("missing frontmatter".to_string());
255        let audit_err: AuditError = cc_err.into();
256        assert!(matches!(audit_err, AuditError::InvalidSkillFormat(_)));
257    }
258
259    #[test]
260    #[allow(clippy::invalid_regex)]
261    fn test_error_from_regex_error() {
262        let regex_err = regex::Regex::new(r"[invalid\[").unwrap_err();
263        let err: AuditError = regex_err.into();
264        assert!(err.to_string().contains("Regex compilation error"));
265    }
266
267    #[test]
268    fn test_error_debug_trait() {
269        let err = AuditError::FileNotFound("/test".to_string());
270        let debug_str = format!("{:?}", err);
271        assert!(debug_str.contains("FileNotFound"));
272    }
273}