rom_analyzer/
error.rs

1//! Defines custom error types for ROM-Analyzer, providing a centralized way
2//! to handle and propagate errors throughout the application.
3
4use std::error::Error;
5use std::fmt;
6
7use zip::result::ZipError;
8
9#[derive(Debug)]
10pub enum RomAnalyzerError {
11    /// File format or extension is not supported
12    UnsupportedFormat(String),
13    /// ROM data is too small for analysis
14    DataTooSmall {
15        file_size: usize,
16        required_size: usize,
17        details: String,
18    },
19    /// Header data is invalid or corrupted
20    InvalidHeader(String),
21    /// Reserved for future parsing error handling
22    ParsingError(String),
23    /// Checksum validation failed
24    ChecksumMismatch(String),
25    /// Error processing archive files (ZIP, CHD, etc.)
26    ArchiveError(String),
27    /// I/O operation failed
28    IoError(std::io::Error),
29    /// ZIP archive operation failed
30    ZipError(ZipError),
31    /// CHD archive operation failed
32    ChdError(chd::Error),
33    /// File not found
34    FileNotFound(String),
35    /// Generic error with custom message
36    Generic(String),
37    /// Error with associated file path for better context
38    WithPath(String, Box<RomAnalyzerError>),
39}
40
41impl RomAnalyzerError {
42    /// Creates a new generic [`RomAnalyzerError`] with the given message.
43    ///
44    /// # Arguments
45    ///
46    /// * `msg` - A string slice that describes the error.
47    ///
48    /// # Returns
49    ///
50    /// A new [`RomAnalyzerError::Generic`] instance.
51    pub fn new(msg: &str) -> RomAnalyzerError {
52        RomAnalyzerError::Generic(msg.to_string())
53    }
54}
55
56impl fmt::Display for RomAnalyzerError {
57    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58        match self {
59            RomAnalyzerError::UnsupportedFormat(msg) => write!(f, "Unsupported format: {}", msg),
60            RomAnalyzerError::DataTooSmall {
61                file_size,
62                required_size,
63                details,
64            } => write!(
65                f,
66                "ROM data too small: {} bytes, requires at least {} bytes. {}",
67                file_size, required_size, details
68            ),
69            RomAnalyzerError::InvalidHeader(msg) => write!(f, "Invalid header: {}", msg),
70            RomAnalyzerError::ParsingError(msg) => write!(f, "Parsing error: {}", msg),
71            RomAnalyzerError::ChecksumMismatch(msg) => write!(f, "Checksum mismatch: {}", msg),
72            RomAnalyzerError::ArchiveError(msg) => write!(f, "Archive error: {}", msg),
73            RomAnalyzerError::IoError(err) => write!(f, "IO error: {}", err),
74            RomAnalyzerError::ZipError(err) => write!(f, "ZIP error: {}", err),
75            RomAnalyzerError::ChdError(err) => write!(f, "CHD error: {}", err),
76            RomAnalyzerError::FileNotFound(path) => write!(f, "File not found: {}", path),
77            RomAnalyzerError::Generic(msg) => write!(f, "{}", msg),
78            RomAnalyzerError::WithPath(path, err) => {
79                write!(f, "Error processing file {}: {}", path, err)
80            }
81        }
82    }
83}
84
85impl Error for RomAnalyzerError {
86    fn source(&self) -> Option<&(dyn Error + 'static)> {
87        match self {
88            RomAnalyzerError::IoError(err) => Some(err),
89            RomAnalyzerError::ZipError(err) => Some(err),
90            RomAnalyzerError::ChdError(err) => Some(err),
91            RomAnalyzerError::WithPath(_, err) => err.source(),
92            _ => None,
93        }
94    }
95}
96
97/// Converts a `zip::result::ZipError` into a [`RomAnalyzerError`].
98impl From<ZipError> for RomAnalyzerError {
99    fn from(err: ZipError) -> RomAnalyzerError {
100        RomAnalyzerError::ZipError(err)
101    }
102}
103
104/// Converts a `std::io::Error` into a [`RomAnalyzerError`].
105impl From<std::io::Error> for RomAnalyzerError {
106    fn from(err: std::io::Error) -> RomAnalyzerError {
107        RomAnalyzerError::IoError(err)
108    }
109}
110
111/// Converts a `Box<dyn Error>` into a [`RomAnalyzerError`].
112impl From<Box<dyn Error>> for RomAnalyzerError {
113    fn from(err: Box<dyn Error>) -> RomAnalyzerError {
114        RomAnalyzerError::Generic(err.to_string())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::io::{Error as IoError, ErrorKind};
122
123    #[test]
124    fn test_new_error() {
125        let error_msg = "Test error message";
126        let err = RomAnalyzerError::new(error_msg);
127        match err {
128            RomAnalyzerError::Generic(msg) => assert_eq!(msg, error_msg),
129            _ => panic!("Expected Generic variant"),
130        }
131    }
132
133    #[test]
134    fn test_display_trait() {
135        let error_msg = "Display test";
136        let err = RomAnalyzerError::Generic(error_msg.to_string());
137        assert_eq!(format!("{}", err), error_msg);
138    }
139
140    #[test]
141    fn test_display_unsupported_format() {
142        let err = RomAnalyzerError::UnsupportedFormat("test.ext".to_string());
143        assert_eq!(format!("{}", err), "Unsupported format: test.ext");
144    }
145
146    #[test]
147    fn test_display_data_too_small() {
148        let err = RomAnalyzerError::DataTooSmall {
149            file_size: 100,
150            required_size: 200,
151            details: "Header missing".to_string(),
152        };
153        assert_eq!(
154            format!("{}", err),
155            "ROM data too small: 100 bytes, requires at least 200 bytes. Header missing"
156        );
157    }
158
159    #[test]
160    fn test_display_file_not_found() {
161        let err = RomAnalyzerError::FileNotFound("test.nes".to_string());
162        assert_eq!(format!("{}", err), "File not found: test.nes");
163    }
164
165    #[test]
166    fn test_from_zip_error() {
167        let zip_err = ZipError::FileNotFound;
168        let zip_err_display = format!("{}", zip_err);
169        let err: RomAnalyzerError = zip_err.into();
170        match err {
171            RomAnalyzerError::ZipError(_) => assert_eq!(
172                format!("{}", err),
173                format!("ZIP error: {}", zip_err_display)
174            ),
175            _ => panic!("Expected ZipError variant"),
176        }
177    }
178
179    #[test]
180    fn test_from_io_error() {
181        let io_err = IoError::new(ErrorKind::NotFound, "File not found");
182        let err: RomAnalyzerError = io_err.into();
183        match err {
184            RomAnalyzerError::IoError(_) => assert!(format!("{}", err).contains("IO error")),
185            _ => panic!("Expected IoError variant"),
186        }
187    }
188
189    #[test]
190    fn test_error_source_method() {
191        // Test that source() returns the wrapped error for IoError
192        let io_err = IoError::new(ErrorKind::NotFound, "File not found");
193        let rom_err = RomAnalyzerError::IoError(io_err);
194        assert!(rom_err.source().is_some());
195        assert_eq!(rom_err.source().unwrap().to_string(), "File not found");
196
197        // Test that source() returns the wrapped error for ZipError
198        let zip_err = ZipError::FileNotFound;
199        let rom_err = RomAnalyzerError::ZipError(zip_err);
200        assert!(rom_err.source().is_some());
201
202        // Test that source() returns None for non-wrapped errors
203        let rom_err = RomAnalyzerError::Generic("test".to_string());
204        assert!(rom_err.source().is_none());
205
206        let rom_err = RomAnalyzerError::UnsupportedFormat("test".to_string());
207        assert!(rom_err.source().is_none());
208
209        let rom_err = RomAnalyzerError::DataTooSmall {
210            file_size: 100,
211            required_size: 200,
212            details: "test".to_string(),
213        };
214        assert!(rom_err.source().is_none());
215
216        let rom_err = RomAnalyzerError::InvalidHeader("test".to_string());
217        assert!(rom_err.source().is_none());
218
219        let rom_err = RomAnalyzerError::ParsingError("test".to_string());
220        assert!(rom_err.source().is_none());
221
222        let rom_err = RomAnalyzerError::FileNotFound("test".to_string());
223        assert!(rom_err.source().is_none());
224    }
225
226    #[test]
227    fn test_error_source_chd_error() {
228        // Test ChdError source by creating an invalid CHD and checking the error
229        use tempfile::tempdir;
230
231        let dir = tempdir().unwrap();
232        let chd_path = dir.path().join("test.chd");
233        std::fs::write(&chd_path, b"invalid chd data").unwrap();
234
235        // Try to analyze the invalid CHD file
236        let result = crate::archive::chd::analyze_chd_file(&chd_path);
237        assert!(result.is_err());
238
239        if let Err(RomAnalyzerError::ChdError(chd_err)) = result {
240            // If we get a ChdError, verify source() works
241            let rom_err = RomAnalyzerError::ChdError(chd_err);
242            assert!(rom_err.source().is_some(), "ChdError should have a source");
243        } else {
244            panic!("Expected ChdError, but got {:?}", result.unwrap_err());
245        }
246    }
247
248    #[test]
249    fn test_error_source_with_path() {
250        // Test that WithPath delegates source() to the inner error
251        let io_err = IoError::new(ErrorKind::NotFound, "File not found");
252        let inner_err = RomAnalyzerError::IoError(io_err);
253        let wrapped_err = RomAnalyzerError::WithPath("test.nes".to_string(), Box::new(inner_err));
254        assert!(wrapped_err.source().is_some());
255        assert_eq!(wrapped_err.source().unwrap().to_string(), "File not found");
256
257        // Test WithPath with an error that has no source
258        let inner_err_no_source = RomAnalyzerError::Generic("test".to_string());
259        let wrapped_err_no_source =
260            RomAnalyzerError::WithPath("test.nes".to_string(), Box::new(inner_err_no_source));
261        assert!(wrapped_err_no_source.source().is_none());
262    }
263}