wow_adt/error.rs
1//! Error types for ADT file parsing and validation.
2//!
3//! This module implements a fail-fast error strategy where parsing halts at the first
4//! critical failure. This approach ensures data integrity and prevents cascading errors
5//! from corrupted or malformed ADT files.
6//!
7//! # Error Categories
8//!
9//! ## Critical Errors (halt parsing immediately)
10//!
11//! - [`AdtError::InvalidMagic`] - Chunk magic bytes don't match expected format
12//! - [`AdtError::MissingRequiredChunk`] - Required chunk (MVER, MHDR, MCIN, MCNK) is missing
13//! - [`AdtError::InvalidChunkCombination`] - Mutually exclusive chunks are present
14//! - [`AdtError::OffsetOutOfBounds`] - Chunk offset exceeds file boundaries
15//! - [`AdtError::InvalidChunkSize`] - Chunk size doesn't match format specification
16//! - [`AdtError::InvalidSubchunkOffset`] - Subchunk offset exceeds parent chunk size
17//! - [`AdtError::InvalidWaterStructure`] - Water data structure is malformed
18//! - [`AdtError::InvalidTextureReference`] - Texture index exceeds available textures
19//! - [`AdtError::InvalidModelReference`] - Model index exceeds available models
20//! - [`AdtError::InvalidMcinEntry`] - MCIN entry references non-existent MCNK chunk
21//! - [`AdtError::ChunkParseError`] - Generic chunk parsing failure with context
22//! - [`AdtError::MemoryLimitExceeded`] - Allocation exceeds safety limits
23//! - [`AdtError::VersionDetectionFailed`] - Cannot determine ADT format version
24//! - [`AdtError::Io`] - Underlying I/O error
25//! - [`AdtError::BinrwError`] - Binary parsing library error
26//!
27//! ## Warnings (logged but don't halt parsing)
28//!
29//! - [`AdtError::UnknownChunk`] - Unrecognized chunk encountered (skipped)
30//! - [`AdtError::Utf8Error`] - Invalid UTF-8 in string data (lossy conversion applied)
31//!
32//! # Examples
33//!
34//! ```
35//! use wow_adt::error::{AdtError, Result};
36//! use wow_adt::ChunkId;
37//!
38//! fn validate_magic(magic: [u8; 4], expected: ChunkId, offset: u64) -> Result<()> {
39//! let found = ChunkId(magic);
40//! if found != expected {
41//! return Err(AdtError::InvalidMagic {
42//! expected,
43//! found,
44//! offset,
45//! });
46//! }
47//! Ok(())
48//! }
49//! ```
50//!
51//! # Error Context
52//!
53//! All errors include contextual information to aid debugging:
54//!
55//! - **File offsets** - Exact position in file where error occurred
56//! - **Chunk context** - Which chunk was being parsed when error occurred
57//! - **Expected vs actual** - What was expected and what was found
58//! - **Index information** - Array indices for out-of-bounds references
59
60use thiserror::Error;
61
62use crate::ChunkId;
63
64/// Result type alias using [`AdtError`] as the error type.
65pub type Result<T> = std::result::Result<T, AdtError>;
66
67/// Errors that can occur during ADT file parsing and validation.
68///
69/// This enum uses the fail-fast strategy: parsing halts at the first critical error.
70/// This prevents cascading failures and ensures that subsequent operations don't work
71/// with corrupted or incomplete data.
72#[derive(Error, Debug)]
73pub enum AdtError {
74 /// Underlying I/O error occurred while reading ADT file.
75 ///
76 /// This is a critical error that halts parsing immediately.
77 #[error("IO error: {0}")]
78 Io(#[from] std::io::Error),
79
80 /// Chunk magic bytes don't match expected format specification.
81 ///
82 /// This is a critical error indicating the file is corrupted or not a valid ADT.
83 ///
84 /// # Example
85 ///
86 /// ```text
87 /// Expected MCNK chunk at offset 0x1000, but found MCLQ instead.
88 /// ```
89 #[error("Invalid magic bytes: expected {expected}, found {found} at offset {offset}")]
90 InvalidMagic {
91 /// Expected chunk identifier.
92 expected: ChunkId,
93 /// Actual chunk identifier found in file.
94 found: ChunkId,
95 /// File offset where invalid magic was encountered.
96 offset: u64,
97 },
98
99 /// Required chunk is missing from ADT file.
100 ///
101 /// This is a critical error. ADT files must contain certain chunks (MVER, MHDR, MCIN, MCNK)
102 /// to be considered valid.
103 ///
104 /// # Example
105 ///
106 /// ```text
107 /// ADT file is missing required MHDR chunk - cannot parse terrain metadata.
108 /// ```
109 #[error("Missing required chunk: {0:?}")]
110 MissingRequiredChunk(ChunkId),
111
112 /// Mutually exclusive chunks are present in the same file.
113 ///
114 /// This is a critical error. Some chunks cannot coexist because they represent
115 /// incompatible format versions or conflicting data structures.
116 ///
117 /// # Example
118 ///
119 /// ```text
120 /// File contains both MCLQ (old water) and MH2O (new water) chunks.
121 /// ```
122 #[error("Invalid chunk combination: {chunk1:?} and {chunk2:?} cannot coexist")]
123 InvalidChunkCombination {
124 /// First conflicting chunk.
125 chunk1: ChunkId,
126 /// Second conflicting chunk.
127 chunk2: ChunkId,
128 },
129
130 /// Chunk offset exceeds file boundaries.
131 ///
132 /// This is a critical error indicating corrupted offset data or truncated file.
133 ///
134 /// # Example
135 ///
136 /// ```text
137 /// MCIN entry references MCNK at offset 0x50000, but file is only 0x40000 bytes.
138 /// ```
139 #[error(
140 "Offset out of bounds for chunk {chunk:?}: offset {offset} at file position {file_position}"
141 )]
142 OffsetOutOfBounds {
143 /// Chunk being accessed.
144 chunk: ChunkId,
145 /// Offset value that exceeds bounds.
146 offset: u32,
147 /// File position where invalid offset was read.
148 file_position: u64,
149 },
150
151 /// Chunk size doesn't match format specification.
152 ///
153 /// This is a critical error. Fixed-size chunks must have exact expected sizes.
154 ///
155 /// # Example
156 ///
157 /// ```text
158 /// MVER chunk must be exactly 4 bytes, found 8 bytes instead.
159 /// ```
160 #[error("Invalid chunk size for {chunk:?}: expected {expected}, got {actual}")]
161 InvalidChunkSize {
162 /// Chunk with invalid size.
163 chunk: ChunkId,
164 /// Expected size in bytes.
165 expected: usize,
166 /// Actual size in bytes.
167 actual: usize,
168 },
169
170 /// Subchunk offset exceeds parent chunk boundaries.
171 ///
172 /// This is a critical error. Subchunk offsets must be relative to parent chunk start
173 /// and cannot exceed parent chunk size.
174 ///
175 /// # Example
176 ///
177 /// ```text
178 /// MCNK subchunk offset 0x2000 exceeds MCNK chunk size of 0x1FC0.
179 /// ```
180 #[error(
181 "Invalid subchunk offset for {parent:?}: offset {offset} exceeds chunk size {chunk_size}"
182 )]
183 InvalidSubchunkOffset {
184 /// Parent chunk containing the subchunk.
185 parent: ChunkId,
186 /// Subchunk offset that exceeds bounds.
187 offset: u32,
188 /// Total size of parent chunk.
189 chunk_size: u32,
190 },
191
192 /// Water data structure is malformed or contains invalid data.
193 ///
194 /// This is a critical error for tiles with water features.
195 ///
196 /// # Example
197 ///
198 /// ```text
199 /// MH2O chunk has water layers but missing height data.
200 /// ```
201 #[error("Invalid water structure: {0}")]
202 InvalidWaterStructure(String),
203
204 /// Texture index exceeds available texture count.
205 ///
206 /// This is a critical error. All texture references must be valid indices into
207 /// the texture list defined in MTEX chunk.
208 ///
209 /// # Example
210 ///
211 /// ```text
212 /// Layer references texture index 15, but only 10 textures are defined in MTEX.
213 /// ```
214 #[error("Invalid texture reference: index {index} exceeds texture count {count}")]
215 InvalidTextureReference {
216 /// Invalid texture index.
217 index: u32,
218 /// Total number of available textures.
219 count: u32,
220 },
221
222 /// Model index exceeds available model count.
223 ///
224 /// This is a critical error. All model references must be valid indices into
225 /// the model lists defined in MMDX/MMID or MWMO/MWID chunks.
226 ///
227 /// # Example
228 ///
229 /// ```text
230 /// MCRF references M2 model index 50, but only 30 models defined in MMDX.
231 /// ```
232 #[error("Invalid model reference: index {index} exceeds model count {count}")]
233 InvalidModelReference {
234 /// Invalid model index.
235 index: u32,
236 /// Total number of available models.
237 count: u32,
238 },
239
240 /// MCIN entry references non-existent MCNK chunk.
241 ///
242 /// This is a critical error. MCIN (chunk index) entries must reference valid MCNK chunks.
243 /// ADT files should have exactly 256 MCNK chunks (16x16 grid).
244 ///
245 /// # Example
246 ///
247 /// ```text
248 /// MCIN entry 100 has non-zero offset but corresponding MCNK chunk is missing.
249 /// ```
250 #[error("Invalid MCIN entry at index {index}: references non-existent MCNK")]
251 InvalidMcinEntry {
252 /// Index of invalid MCIN entry.
253 index: usize,
254 },
255
256 /// Generic chunk parsing error with context.
257 ///
258 /// This is a critical error used when more specific error types don't apply.
259 ///
260 /// # Example
261 ///
262 /// ```text
263 /// Failed to parse MCLY chunk at offset 0x3000: layer count mismatch.
264 /// ```
265 #[error("Chunk parse error for {chunk:?} at offset {offset}: {details}")]
266 ChunkParseError {
267 /// Chunk that failed to parse.
268 chunk: ChunkId,
269 /// File offset where error occurred.
270 offset: u64,
271 /// Detailed error description.
272 details: String,
273 },
274
275 /// Binary parsing library error.
276 ///
277 /// This is a critical error from the underlying binrw library.
278 #[error("binrw error: {0}")]
279 BinrwError(String),
280
281 /// UTF-8 conversion error encountered in string data.
282 ///
283 /// This is a WARNING, not a critical error. When invalid UTF-8 is encountered,
284 /// lossy conversion is applied (invalid sequences replaced with �) and parsing continues.
285 ///
286 /// # Example
287 ///
288 /// ```text
289 /// Texture filename contains invalid UTF-8 byte 0xFF, using lossy conversion.
290 /// ```
291 #[error("UTF-8 conversion error: {0} (using lossy conversion)")]
292 Utf8Error(String),
293
294 /// Unknown chunk encountered during parsing.
295 ///
296 /// This is a WARNING, not a critical error. Unknown chunks are skipped and parsing continues.
297 /// This allows for forward compatibility with newer ADT format versions.
298 ///
299 /// # Example
300 ///
301 /// ```text
302 /// Unknown chunk 'MXYZ' at offset 0x5000, skipping to next chunk.
303 /// ```
304 #[error("Unknown chunk encountered: {magic:?} at offset {offset} (skipping)")]
305 UnknownChunk {
306 /// Raw magic bytes of unknown chunk.
307 magic: [u8; 4],
308 /// File offset where unknown chunk was found.
309 offset: u64,
310 },
311
312 /// Memory allocation exceeds safety limits.
313 ///
314 /// This is a critical error to prevent memory exhaustion attacks from malicious
315 /// or corrupted files claiming enormous allocation sizes.
316 ///
317 /// # Example
318 ///
319 /// ```text
320 /// Chunk claims to need 2GB allocation, exceeds 100MB limit.
321 /// ```
322 #[error("Memory limit exceeded: attempted to allocate {requested} bytes, limit is {limit}")]
323 MemoryLimitExceeded {
324 /// Number of bytes requested.
325 requested: usize,
326 /// Maximum allowed allocation size.
327 limit: usize,
328 },
329
330 /// Cannot determine ADT format version.
331 ///
332 /// This is a critical error. Version detection is required to parse format-specific chunks.
333 ///
334 /// # Example
335 ///
336 /// ```text
337 /// MVER chunk contains unknown version 19, expected 18 (WotLK).
338 /// ```
339 #[error("Version detection failed: {0}")]
340 VersionDetectionFailed(String),
341}
342
343impl From<binrw::Error> for AdtError {
344 fn from(err: binrw::Error) -> Self {
345 AdtError::BinrwError(format!("{err}"))
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn error_display_formats_correctly() {
355 let err = AdtError::InvalidMagic {
356 expected: ChunkId::MCNK,
357 found: ChunkId::MCLQ,
358 offset: 0x1000,
359 };
360 let display = format!("{err}");
361 assert!(display.contains("MCNK"));
362 assert!(display.contains("MCLQ"));
363 assert!(display.contains("4096"));
364 }
365
366 #[test]
367 fn error_context_preserved() {
368 let err = AdtError::InvalidTextureReference {
369 index: 15,
370 count: 10,
371 };
372 assert!(matches!(err, AdtError::InvalidTextureReference { .. }));
373 }
374
375 #[test]
376 fn io_error_conversion() {
377 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
378 let adt_err: AdtError = io_err.into();
379 assert!(matches!(adt_err, AdtError::Io(_)));
380 }
381
382 #[test]
383 fn binrw_error_conversion() {
384 let binrw_err = binrw::Error::AssertFail {
385 pos: 0x100,
386 message: "test assertion failed".into(),
387 };
388 let adt_err: AdtError = binrw_err.into();
389 assert!(matches!(adt_err, AdtError::BinrwError(_)));
390 }
391}