wow_mpq/
error.rs

1//! Error types for the MPQ library
2
3use std::io;
4use thiserror::Error;
5
6/// Result type alias for MPQ operations
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Main error type for MPQ operations
10#[derive(Error, Debug)]
11pub enum Error {
12    /// I/O error occurred
13    #[error("I/O error: {0}")]
14    Io(#[from] io::Error),
15
16    /// Invalid MPQ format or corrupted archive
17    #[error("Invalid MPQ format: {0}")]
18    InvalidFormat(String),
19
20    /// Unsupported MPQ version
21    #[error("Unsupported MPQ version: {0}")]
22    UnsupportedVersion(u16),
23
24    /// File not found in archive
25    #[error("File not found: {0}")]
26    FileNotFound(String),
27
28    /// File already exists in archive
29    #[error("File already exists: {0}")]
30    FileExists(String),
31
32    /// Hash table error
33    #[error("Hash table error: {0}")]
34    HashTable(String),
35
36    /// Block table error
37    #[error("Block table error: {0}")]
38    BlockTable(String),
39
40    /// Encryption/decryption error
41    #[error("Cryptography error: {0}")]
42    Crypto(String),
43
44    /// Compression/decompression error
45    #[error("Compression error: {0}")]
46    Compression(String),
47
48    /// Signature verification failed
49    #[error("Signature verification failed: {0}")]
50    SignatureVerification(String),
51
52    /// Invalid header location or alignment
53    #[error("Invalid header: {0}")]
54    InvalidHeader(String),
55
56    /// Archive is read-only
57    #[error("Archive is read-only")]
58    ReadOnly,
59
60    /// Operation not supported for this archive version
61    #[error("Operation not supported for MPQ version {version}: {operation}")]
62    OperationNotSupported {
63        /// The MPQ version
64        version: u16,
65        /// The unsupported operation
66        operation: String,
67    },
68
69    /// Invalid file size
70    #[error("Invalid file size: expected {expected}, got {actual}")]
71    InvalidFileSize {
72        /// Expected size
73        expected: u64,
74        /// Actual size
75        actual: u64,
76    },
77
78    /// Memory mapping error
79    #[error("Memory mapping error: {0}")]
80    MemoryMap(String),
81
82    /// Invalid UTF-8 in filename
83    #[error("Invalid UTF-8 in filename")]
84    InvalidUtf8,
85
86    /// Archive capacity exceeded
87    #[error("Archive capacity exceeded: {0}")]
88    CapacityExceeded(String),
89
90    /// Checksum mismatch
91    #[error("Checksum mismatch for {file}: expected {expected:08x}, got {actual:08x}")]
92    ChecksumMismatch {
93        /// File or table name
94        file: String,
95        /// Expected checksum
96        expected: u32,
97        /// Actual checksum
98        actual: u32,
99    },
100
101    /// MD5 hash mismatch (v4 archives)
102    #[error("MD5 hash mismatch for {table}")]
103    MD5Mismatch {
104        /// Table name
105        table: String,
106    },
107
108    /// Feature not yet implemented
109    #[error("Not implemented: {0}")]
110    NotImplemented(&'static str),
111
112    /// Security validation failed
113    #[error("Security validation failed: {0}")]
114    SecurityViolation(String),
115
116    /// Potential malicious content detected
117    #[error("Malicious content detected: {0}")]
118    MaliciousContent(String),
119
120    /// Resource exhaustion attempt detected
121    #[error("Resource exhaustion attempt: {0}")]
122    ResourceExhaustion(String),
123
124    /// Directory traversal attempt detected
125    #[error("Directory traversal attempt: {0}")]
126    DirectoryTraversal(String),
127
128    /// Compression bomb detected
129    #[error("Compression bomb detected: ratio {ratio}:1 exceeds limit of {limit}:1")]
130    CompressionBomb {
131        /// Actual compression ratio
132        ratio: u64,
133        /// Maximum allowed ratio
134        limit: u64,
135    },
136
137    /// Invalid bounds access
138    #[error("Invalid bounds access: {0}")]
139    InvalidBounds(String),
140
141    /// Unsupported feature
142    #[error("Unsupported feature: {0}")]
143    UnsupportedFeature(String),
144
145    /// Decompression error
146    #[error("Decompression error: {0}")]
147    Decompression(String),
148}
149
150impl Error {
151    /// Create a new InvalidFormat error
152    pub fn invalid_format<S: Into<String>>(msg: S) -> Self {
153        Error::InvalidFormat(msg.into())
154    }
155
156    /// Create a new Crypto error
157    pub fn crypto<S: Into<String>>(msg: S) -> Self {
158        Error::Crypto(msg.into())
159    }
160
161    /// Create a new Compression error
162    pub fn compression<S: Into<String>>(msg: S) -> Self {
163        Error::Compression(msg.into())
164    }
165
166    /// Create a new HashTable error
167    pub fn hash_table<S: Into<String>>(msg: S) -> Self {
168        Error::HashTable(msg.into())
169    }
170
171    /// Create a new BlockTable error
172    pub fn block_table<S: Into<String>>(msg: S) -> Self {
173        Error::BlockTable(msg.into())
174    }
175
176    /// Create a new SecurityViolation error
177    pub fn security_violation<S: Into<String>>(msg: S) -> Self {
178        Error::SecurityViolation(msg.into())
179    }
180
181    /// Create a new MaliciousContent error
182    pub fn malicious_content<S: Into<String>>(msg: S) -> Self {
183        Error::MaliciousContent(msg.into())
184    }
185
186    /// Create a new ResourceExhaustion error
187    pub fn resource_exhaustion<S: Into<String>>(msg: S) -> Self {
188        Error::ResourceExhaustion(msg.into())
189    }
190
191    /// Create a new DirectoryTraversal error
192    pub fn directory_traversal<S: Into<String>>(msg: S) -> Self {
193        Error::DirectoryTraversal(msg.into())
194    }
195
196    /// Create a new CompressionBomb error
197    pub fn compression_bomb(ratio: u64, limit: u64) -> Self {
198        Error::CompressionBomb { ratio, limit }
199    }
200
201    /// Create a new InvalidBounds error
202    pub fn invalid_bounds<S: Into<String>>(msg: S) -> Self {
203        Error::InvalidBounds(msg.into())
204    }
205
206    /// Create a new UnsupportedFeature error
207    pub fn unsupported_feature<S: Into<String>>(msg: S) -> Self {
208        Error::UnsupportedFeature(msg.into())
209    }
210
211    /// Create a new I/O error
212    pub fn io_error<S: Into<String>>(msg: S) -> Self {
213        Error::Io(io::Error::other(msg.into()))
214    }
215
216    /// Create a new Decompression error
217    pub fn decompression<S: Into<String>>(msg: S) -> Self {
218        Error::Decompression(msg.into())
219    }
220
221    /// Check if this error indicates the archive is corrupted
222    pub fn is_corruption(&self) -> bool {
223        matches!(
224            self,
225            Error::InvalidFormat(_)
226                | Error::ChecksumMismatch { .. }
227                | Error::MD5Mismatch { .. }
228                | Error::SignatureVerification(_)
229                | Error::InvalidHeader(_)
230        )
231    }
232
233    /// Check if this error indicates a security threat
234    pub fn is_security_threat(&self) -> bool {
235        matches!(
236            self,
237            Error::SecurityViolation(_)
238                | Error::MaliciousContent(_)
239                | Error::ResourceExhaustion(_)
240                | Error::DirectoryTraversal(_)
241                | Error::CompressionBomb { .. }
242        )
243    }
244
245    /// Check if this error is recoverable
246    pub fn is_recoverable(&self) -> bool {
247        matches!(
248            self,
249            Error::FileNotFound(_) | Error::ReadOnly | Error::OperationNotSupported { .. }
250        ) && !self.is_security_threat() // Security threats are never recoverable
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_error_creation() {
260        let err = Error::invalid_format("bad header");
261        assert_eq!(err.to_string(), "Invalid MPQ format: bad header");
262
263        let err = Error::FileNotFound("test.txt".to_string());
264        assert_eq!(err.to_string(), "File not found: test.txt");
265    }
266
267    #[test]
268    fn test_error_classification() {
269        let corruption_err = Error::ChecksumMismatch {
270            file: "test".to_string(),
271            expected: 0x12345678,
272            actual: 0x87654321,
273        };
274        assert!(corruption_err.is_corruption());
275        assert!(!corruption_err.is_recoverable());
276
277        let recoverable_err = Error::FileNotFound("missing.txt".to_string());
278        assert!(!recoverable_err.is_corruption());
279        assert!(recoverable_err.is_recoverable());
280    }
281
282    #[test]
283    fn test_memory_mapping_errors() {
284        let err = Error::unsupported_feature("Memory mapping not available");
285        assert_eq!(
286            err.to_string(),
287            "Unsupported feature: Memory mapping not available"
288        );
289
290        let err = Error::invalid_bounds("Read beyond file end");
291        assert_eq!(
292            err.to_string(),
293            "Invalid bounds access: Read beyond file end"
294        );
295    }
296}