1mod 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#[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
71pub type Result<T> = std::result::Result<T, AuditError>;
73
74pub type CcResult<T> = std::result::Result<T, CcAuditError>;
76
77impl 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 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}