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("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
68pub type Result<T> = std::result::Result<T, AuditError>;
70
71pub type CcResult<T> = std::result::Result<T, CcAuditError>;
73
74impl 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 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}