1use std::io;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9#[derive(Error, Debug)]
11pub enum Error {
12 #[error("I/O error: {0}")]
14 Io(#[from] io::Error),
15
16 #[error("Invalid MPQ format: {0}")]
18 InvalidFormat(String),
19
20 #[error("Unsupported MPQ version: {0}")]
22 UnsupportedVersion(u16),
23
24 #[error("File not found: {0}")]
26 FileNotFound(String),
27
28 #[error("File already exists: {0}")]
30 FileExists(String),
31
32 #[error("Hash table error: {0}")]
34 HashTable(String),
35
36 #[error("Block table error: {0}")]
38 BlockTable(String),
39
40 #[error("Cryptography error: {0}")]
42 Crypto(String),
43
44 #[error("Compression error: {0}")]
46 Compression(String),
47
48 #[error("Signature verification failed: {0}")]
50 SignatureVerification(String),
51
52 #[error("Invalid header: {0}")]
54 InvalidHeader(String),
55
56 #[error("Archive is read-only")]
58 ReadOnly,
59
60 #[error("Operation not supported for MPQ version {version}: {operation}")]
62 OperationNotSupported {
63 version: u16,
65 operation: String,
67 },
68
69 #[error("Invalid file size: expected {expected}, got {actual}")]
71 InvalidFileSize {
72 expected: u64,
74 actual: u64,
76 },
77
78 #[error("Memory mapping error: {0}")]
80 MemoryMap(String),
81
82 #[error("Invalid UTF-8 in filename")]
84 InvalidUtf8,
85
86 #[error("Archive capacity exceeded: {0}")]
88 CapacityExceeded(String),
89
90 #[error("Checksum mismatch for {file}: expected {expected:08x}, got {actual:08x}")]
92 ChecksumMismatch {
93 file: String,
95 expected: u32,
97 actual: u32,
99 },
100
101 #[error("MD5 hash mismatch for {table}")]
103 MD5Mismatch {
104 table: String,
106 },
107
108 #[error("Not implemented: {0}")]
110 NotImplemented(&'static str),
111
112 #[error("Security validation failed: {0}")]
114 SecurityViolation(String),
115
116 #[error("Malicious content detected: {0}")]
118 MaliciousContent(String),
119
120 #[error("Resource exhaustion attempt: {0}")]
122 ResourceExhaustion(String),
123
124 #[error("Directory traversal attempt: {0}")]
126 DirectoryTraversal(String),
127
128 #[error("Compression bomb detected: ratio {ratio}:1 exceeds limit of {limit}:1")]
130 CompressionBomb {
131 ratio: u64,
133 limit: u64,
135 },
136
137 #[error("Invalid bounds access: {0}")]
139 InvalidBounds(String),
140
141 #[error("Unsupported feature: {0}")]
143 UnsupportedFeature(String),
144
145 #[error("Decompression error: {0}")]
147 Decompression(String),
148}
149
150impl Error {
151 pub fn invalid_format<S: Into<String>>(msg: S) -> Self {
153 Error::InvalidFormat(msg.into())
154 }
155
156 pub fn crypto<S: Into<String>>(msg: S) -> Self {
158 Error::Crypto(msg.into())
159 }
160
161 pub fn compression<S: Into<String>>(msg: S) -> Self {
163 Error::Compression(msg.into())
164 }
165
166 pub fn hash_table<S: Into<String>>(msg: S) -> Self {
168 Error::HashTable(msg.into())
169 }
170
171 pub fn block_table<S: Into<String>>(msg: S) -> Self {
173 Error::BlockTable(msg.into())
174 }
175
176 pub fn security_violation<S: Into<String>>(msg: S) -> Self {
178 Error::SecurityViolation(msg.into())
179 }
180
181 pub fn malicious_content<S: Into<String>>(msg: S) -> Self {
183 Error::MaliciousContent(msg.into())
184 }
185
186 pub fn resource_exhaustion<S: Into<String>>(msg: S) -> Self {
188 Error::ResourceExhaustion(msg.into())
189 }
190
191 pub fn directory_traversal<S: Into<String>>(msg: S) -> Self {
193 Error::DirectoryTraversal(msg.into())
194 }
195
196 pub fn compression_bomb(ratio: u64, limit: u64) -> Self {
198 Error::CompressionBomb { ratio, limit }
199 }
200
201 pub fn invalid_bounds<S: Into<String>>(msg: S) -> Self {
203 Error::InvalidBounds(msg.into())
204 }
205
206 pub fn unsupported_feature<S: Into<String>>(msg: S) -> Self {
208 Error::UnsupportedFeature(msg.into())
209 }
210
211 pub fn io_error<S: Into<String>>(msg: S) -> Self {
213 Error::Io(io::Error::other(msg.into()))
214 }
215
216 pub fn decompression<S: Into<String>>(msg: S) -> Self {
218 Error::Decompression(msg.into())
219 }
220
221 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 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 pub fn is_recoverable(&self) -> bool {
247 matches!(
248 self,
249 Error::FileNotFound(_) | Error::ReadOnly | Error::OperationNotSupported { .. }
250 ) && !self.is_security_threat() }
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}