Skip to main content

cc_audit/engine/scanners/
error.rs

1//! Scanner-specific error types.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Error type for scanner operations.
7#[derive(Error, Debug)]
8pub enum ScanError {
9    #[error("File not found: {0}")]
10    FileNotFound(PathBuf),
11
12    #[error("Path is not a file: {0}")]
13    NotAFile(PathBuf),
14
15    #[error("Path is not a directory: {0}")]
16    NotADirectory(PathBuf),
17
18    #[error("Failed to read file: {path}")]
19    ReadError {
20        path: PathBuf,
21        #[source]
22        source: std::io::Error,
23    },
24
25    #[error("Failed to parse YAML in {path}")]
26    YamlParseError {
27        path: PathBuf,
28        #[source]
29        source: serde_yaml::Error,
30    },
31
32    #[error("Failed to parse JSON in {path}")]
33    JsonParseError {
34        path: PathBuf,
35        #[source]
36        source: serde_json::Error,
37    },
38
39    #[error("Invalid format in {path}: {message}")]
40    InvalidFormat { path: PathBuf, message: String },
41
42    #[error("Regex compilation error: {0}")]
43    RegexError(#[from] regex::Error),
44}
45
46impl ScanError {
47    /// Create a FileNotFound error from a path-like type.
48    pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
49        Self::FileNotFound(path.into())
50    }
51
52    /// Create a NotADirectory error from a path-like type.
53    pub fn not_a_directory(path: impl Into<PathBuf>) -> Self {
54        Self::NotADirectory(path.into())
55    }
56
57    /// Create a ReadError from a path and IO error.
58    pub fn read_error(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
59        Self::ReadError {
60            path: path.into(),
61            source,
62        }
63    }
64
65    /// Create a YamlParseError from a path and YAML error.
66    pub fn yaml_error(path: impl Into<PathBuf>, source: serde_yaml::Error) -> Self {
67        Self::YamlParseError {
68            path: path.into(),
69            source,
70        }
71    }
72
73    /// Create a JsonParseError from a path and JSON error.
74    pub fn json_error(path: impl Into<PathBuf>, source: serde_json::Error) -> Self {
75        Self::JsonParseError {
76            path: path.into(),
77            source,
78        }
79    }
80
81    /// Create an InvalidFormat error.
82    pub fn invalid_format(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
83        Self::InvalidFormat {
84            path: path.into(),
85            message: message.into(),
86        }
87    }
88}
89
90/// Result type alias for scanner operations.
91pub type ScanResult<T> = std::result::Result<T, ScanError>;
92
93/// Convert from ScanError to the legacy AuditError for backwards compatibility.
94impl From<ScanError> for crate::error::AuditError {
95    fn from(err: ScanError) -> Self {
96        match err {
97            ScanError::FileNotFound(path) => {
98                crate::error::AuditError::FileNotFound(path.display().to_string())
99            }
100            ScanError::NotAFile(path) => {
101                crate::error::AuditError::NotADirectory(path.display().to_string())
102            }
103            ScanError::NotADirectory(path) => {
104                crate::error::AuditError::NotADirectory(path.display().to_string())
105            }
106            ScanError::ReadError { path, source } => crate::error::AuditError::ReadError {
107                path: path.display().to_string(),
108                source,
109            },
110            ScanError::YamlParseError { path, source } => {
111                crate::error::AuditError::YamlParseError {
112                    path: path.display().to_string(),
113                    source,
114                }
115            }
116            ScanError::JsonParseError { path, .. } => crate::error::AuditError::ParseError {
117                path: path.display().to_string(),
118                message: "JSON parse error".to_string(),
119            },
120            ScanError::InvalidFormat { path, message } => crate::error::AuditError::ParseError {
121                path: path.display().to_string(),
122                message,
123            },
124            ScanError::RegexError(e) => crate::error::AuditError::RegexError(e),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::path::Path;
133
134    #[test]
135    fn test_file_not_found() {
136        let err = ScanError::file_not_found("/path/to/file");
137        assert!(err.to_string().contains("/path/to/file"));
138    }
139
140    #[test]
141    fn test_not_a_directory() {
142        let err = ScanError::not_a_directory(Path::new("/path/to/file"));
143        assert!(err.to_string().contains("/path/to/file"));
144    }
145
146    #[test]
147    fn test_read_error() {
148        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
149        let err = ScanError::read_error("/path/to/file", io_err);
150        assert!(err.to_string().contains("/path/to/file"));
151    }
152
153    #[test]
154    fn test_invalid_format() {
155        let err = ScanError::invalid_format("/path/to/file", "missing field");
156        assert!(err.to_string().contains("/path/to/file"));
157        assert!(err.to_string().contains("missing field"));
158    }
159
160    #[test]
161    fn test_conversion_to_audit_error() {
162        let scan_err = ScanError::file_not_found("/test/path");
163        let audit_err: crate::error::AuditError = scan_err.into();
164        assert!(audit_err.to_string().contains("/test/path"));
165    }
166
167    #[test]
168    fn test_yaml_error() {
169        let yaml_str = "invalid: yaml: content";
170        let yaml_err = serde_yaml::from_str::<serde_yaml::Value>(yaml_str).unwrap_err();
171        let err = ScanError::yaml_error("/path/to/file.yaml", yaml_err);
172        assert!(err.to_string().contains("/path/to/file.yaml"));
173    }
174
175    #[test]
176    fn test_json_error() {
177        let json_str = "{ invalid json }";
178        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
179        let err = ScanError::json_error("/path/to/file.json", json_err);
180        assert!(err.to_string().contains("/path/to/file.json"));
181    }
182
183    #[test]
184    fn test_not_a_file() {
185        let err = ScanError::NotAFile(PathBuf::from("/path/to/dir"));
186        assert!(err.to_string().contains("/path/to/dir"));
187    }
188
189    #[test]
190    fn test_conversion_not_a_file_to_audit_error() {
191        let scan_err = ScanError::NotAFile(PathBuf::from("/test/dir"));
192        let audit_err: crate::error::AuditError = scan_err.into();
193        assert!(audit_err.to_string().contains("/test/dir"));
194    }
195
196    #[test]
197    fn test_conversion_not_a_directory_to_audit_error() {
198        let scan_err = ScanError::not_a_directory("/test/file");
199        let audit_err: crate::error::AuditError = scan_err.into();
200        assert!(audit_err.to_string().contains("/test/file"));
201    }
202
203    #[test]
204    fn test_conversion_read_error_to_audit_error() {
205        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
206        let scan_err = ScanError::read_error("/test/path", io_err);
207        let audit_err: crate::error::AuditError = scan_err.into();
208        assert!(audit_err.to_string().contains("/test/path"));
209    }
210
211    #[test]
212    fn test_conversion_yaml_error_to_audit_error() {
213        let yaml_str = "invalid: yaml: content";
214        let yaml_err = serde_yaml::from_str::<serde_yaml::Value>(yaml_str).unwrap_err();
215        let scan_err = ScanError::yaml_error("/test.yaml", yaml_err);
216        let audit_err: crate::error::AuditError = scan_err.into();
217        assert!(audit_err.to_string().contains("/test.yaml"));
218    }
219
220    #[test]
221    fn test_conversion_json_error_to_audit_error() {
222        let json_str = "{ invalid }";
223        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
224        let scan_err = ScanError::json_error("/test.json", json_err);
225        let audit_err: crate::error::AuditError = scan_err.into();
226        assert!(audit_err.to_string().contains("/test.json"));
227    }
228
229    #[test]
230    fn test_conversion_invalid_format_to_audit_error() {
231        let scan_err = ScanError::invalid_format("/test/file", "bad format");
232        let audit_err: crate::error::AuditError = scan_err.into();
233        assert!(audit_err.to_string().contains("bad format"));
234    }
235
236    #[test]
237    #[allow(clippy::invalid_regex)]
238    fn test_conversion_regex_error_to_audit_error() {
239        let regex_err = regex::Regex::new(r"[invalid\[").unwrap_err();
240        let scan_err = ScanError::RegexError(regex_err);
241        let audit_err: crate::error::AuditError = scan_err.into();
242        assert!(audit_err.to_string().contains("Regex"));
243    }
244
245    #[test]
246    fn test_scan_error_debug() {
247        let err = ScanError::file_not_found("/test");
248        let debug_str = format!("{:?}", err);
249        assert!(debug_str.contains("FileNotFound"));
250    }
251}