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}