wow_adt/
chunk_header.rs

1//! ADT chunk header parsing
2//!
3//! All ADT chunks follow a standard 8-byte header structure consisting of a 4-byte
4//! magic identifier and a 4-byte size field. This module provides the core type for
5//! parsing these headers.
6
7use binrw::{BinRead, BinWrite};
8
9use crate::chunk_id::ChunkId;
10
11/// Standard ADT chunk header (8 bytes)
12///
13/// All chunks in ADT files follow this structure:
14/// - 4 bytes: Magic identifier (reversed string, e.g., "MVER" stored as [0x52, 0x45, 0x56, 0x4D])
15/// - 4 bytes: Data size (little-endian u32, excludes the 8-byte header)
16///
17/// # Binary Layout
18///
19/// ```text
20/// Offset | Size | Field | Description
21/// -------|------|-------|------------------------------------------
22/// 0x00   |  4   | id    | Chunk magic identifier (reversed)
23/// 0x04   |  4   | size  | Data size in bytes (excludes header)
24/// ```
25///
26/// # Example
27///
28/// ```text
29/// File bytes: [0x52, 0x45, 0x56, 0x4D] [0x04, 0x00, 0x00, 0x00] [0x12, 0x00, 0x00, 0x00]
30///             └────── "REVM" ────────┘ └──── size: 4 ────────┘ └─── version data ──┘
31///             (displays as "MVER")
32/// ```
33///
34/// # Size Field Semantics
35///
36/// The size field represents the byte count of chunk data EXCLUDING the 8-byte header.
37/// This is a common source of off-by-8 errors when calculating file offsets.
38///
39/// If size = 100, then:
40/// - Chunk data occupies bytes [8..108] relative to chunk start
41/// - Total chunk size (header + data) = 108 bytes
42/// - Next chunk starts at offset 108
43///
44/// # Usage with binrw
45///
46/// ```rust,no_run
47/// use binrw::BinRead;
48/// use std::io::Cursor;
49/// use wow_adt::chunk_header::ChunkHeader;
50///
51/// # fn example() -> binrw::BinResult<()> {
52/// let data = [0x52, 0x45, 0x56, 0x4D, 0x04, 0x00, 0x00, 0x00];
53/// let mut cursor = Cursor::new(&data);
54///
55/// let header = ChunkHeader::read(&mut cursor)?;
56/// assert_eq!(header.size, 4);
57/// assert_eq!(header.total_size(), 12); // 8-byte header + 4 bytes data
58/// # Ok(())
59/// # }
60/// ```
61#[derive(Debug, Clone, Copy, BinRead, BinWrite)]
62#[brw(little)]
63pub struct ChunkHeader {
64    /// Chunk magic identifier (4 bytes, reversed)
65    ///
66    /// Identifiers are stored in reverse byte order. For example, the "MVER" chunk
67    /// is stored as [0x52, 0x45, 0x56, 0x4D] ("REVM" in ASCII).
68    pub id: ChunkId,
69
70    /// Size of chunk data in bytes (excludes 8-byte header)
71    ///
72    /// This field specifies the size of the chunk data following the header.
73    /// The total chunk size is `size + 8` bytes.
74    pub size: u32,
75}
76
77impl ChunkHeader {
78    /// Total size including header (size + 8)
79    ///
80    /// Returns the complete chunk size including the 8-byte header.
81    /// Use this when calculating file offsets to the next chunk.
82    ///
83    /// # Example
84    ///
85    /// ```rust
86    /// use wow_adt::chunk_header::ChunkHeader;
87    /// use wow_adt::chunk_id::ChunkId;
88    ///
89    /// let header = ChunkHeader {
90    ///     id: ChunkId::from_str("MVER").unwrap(),
91    ///     size: 100,
92    /// };
93    ///
94    /// assert_eq!(header.total_size(), 108); // 100 + 8
95    /// ```
96    #[must_use]
97    pub const fn total_size(&self) -> u64 {
98        self.size as u64 + 8
99    }
100
101    /// Check if chunk ID matches expected value
102    ///
103    /// Convenience method for validating chunk types during parsing.
104    ///
105    /// # Example
106    ///
107    /// ```rust
108    /// use wow_adt::chunk_header::ChunkHeader;
109    /// use wow_adt::chunk_id::ChunkId;
110    ///
111    /// let header = ChunkHeader {
112    ///     id: ChunkId::from_str("MVER").unwrap(),
113    ///     size: 4,
114    /// };
115    ///
116    /// assert!(header.is_chunk(ChunkId::from_str("MVER").unwrap()));
117    /// assert!(!header.is_chunk(ChunkId::from_str("MHDR").unwrap()));
118    /// ```
119    #[must_use]
120    pub fn is_chunk(&self, expected: ChunkId) -> bool {
121        self.id.0 == expected.0
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use binrw::BinRead;
129    use std::io::Cursor;
130
131    #[test]
132    fn parse_chunk_header() {
133        let data = [
134            0x52, 0x45, 0x56, 0x4D, // "MVER" reversed
135            0x04, 0x00, 0x00, 0x00, // size: 4
136        ];
137
138        let mut cursor = Cursor::new(&data);
139        let header = ChunkHeader::read(&mut cursor).expect("parse chunk header");
140
141        assert_eq!(header.id, ChunkId::from_str("MVER").unwrap());
142        assert_eq!(header.size, 4);
143        assert_eq!(header.total_size(), 12);
144    }
145
146    #[test]
147    fn total_size_calculation() {
148        let header = ChunkHeader {
149            id: ChunkId::from_str("TEST").unwrap(),
150            size: 100,
151        };
152
153        assert_eq!(header.total_size(), 108);
154    }
155
156    #[test]
157    fn is_chunk_validation() {
158        let header = ChunkHeader {
159            id: ChunkId::from_str("MVER").unwrap(),
160            size: 4,
161        };
162
163        assert!(header.is_chunk(ChunkId::from_str("MVER").unwrap()));
164        assert!(!header.is_chunk(ChunkId::from_str("MHDR").unwrap()));
165    }
166
167    #[test]
168    fn zero_size_chunk() {
169        let data = [
170            0x54, 0x53, 0x45, 0x54, // "TEST" reversed
171            0x00, 0x00, 0x00, 0x00, // size: 0
172        ];
173
174        let mut cursor = Cursor::new(&data);
175        let header = ChunkHeader::read(&mut cursor).expect("parse zero size chunk");
176
177        assert_eq!(header.size, 0);
178        assert_eq!(header.total_size(), 8);
179    }
180}