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}