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