wow_m2/chunks/
infrastructure.rs

1//! Chunk infrastructure for M2 chunked format (MD21+)
2//!
3//! This module provides the core infrastructure for parsing chunked M2 files
4//! introduced in Legion. Chunks allow for modular data storage and external
5//! file references using FileDataIDs.
6
7use std::io::{Read, Seek, SeekFrom};
8
9use crate::error::{M2Error, Result};
10use crate::io_ext::ReadExt;
11
12/// A chunk header containing magic and size information
13#[derive(Debug, Clone)]
14pub struct ChunkHeader {
15    /// 4-byte magic identifier for the chunk type
16    pub magic: [u8; 4],
17    /// Size of the chunk data in bytes (excluding header)
18    pub size: u32,
19}
20
21impl ChunkHeader {
22    /// Read a chunk header from a reader
23    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
24        let mut magic = [0u8; 4];
25        reader.read_exact(&mut magic)?;
26        let size = reader.read_u32_le()?;
27
28        Ok(ChunkHeader { magic, size })
29    }
30
31    /// Get the magic as a string for debugging
32    pub fn magic_str(&self) -> String {
33        String::from_utf8_lossy(&self.magic).to_string()
34    }
35
36    /// Check if this chunk has the specified magic
37    pub fn has_magic(&self, magic: &[u8; 4]) -> bool {
38        &self.magic == magic
39    }
40}
41
42/// A chunk reader that manages chunk boundaries and offset calculations
43pub struct ChunkReader<R> {
44    inner: R,
45    chunk_start: u64,
46    chunk_size: u32,
47}
48
49impl<R: Read + Seek> ChunkReader<R> {
50    /// Create a new chunk reader from a reader and chunk header
51    /// The reader should be positioned at the start of chunk data (after header)
52    pub fn new(mut reader: R, header: ChunkHeader) -> Result<Self> {
53        let chunk_start = reader.stream_position()?;
54        Ok(ChunkReader {
55            inner: reader,
56            chunk_start,
57            chunk_size: header.size,
58        })
59    }
60
61    /// Resolve a chunk-relative offset to an absolute file position
62    pub fn resolve_offset(&self, offset: u32) -> u64 {
63        self.chunk_start + offset as u64
64    }
65
66    /// Get the current position within the chunk (relative to chunk start)
67    pub fn chunk_position(&mut self) -> Result<u32> {
68        let current = self.inner.stream_position()?;
69        Ok((current - self.chunk_start) as u32)
70    }
71
72    /// Get the remaining bytes in the chunk
73    pub fn remaining(&mut self) -> Result<u32> {
74        let pos = self.chunk_position()?;
75        Ok(self.chunk_size.saturating_sub(pos))
76    }
77
78    /// Check if we've reached the end of the chunk
79    pub fn is_at_end(&mut self) -> Result<bool> {
80        Ok(self.remaining()? == 0)
81    }
82
83    /// Seek to a chunk-relative position
84    pub fn seek_to_chunk_offset(&mut self, offset: u32) -> Result<()> {
85        if offset > self.chunk_size {
86            return Err(M2Error::ParseError(format!(
87                "Chunk offset {} exceeds chunk size {}",
88                offset, self.chunk_size
89            )));
90        }
91
92        let absolute_pos = self.chunk_start + offset as u64;
93        self.inner.seek(SeekFrom::Start(absolute_pos))?;
94        Ok(())
95    }
96
97    /// Skip to the end of the chunk
98    pub fn skip_to_end(&mut self) -> Result<()> {
99        let end_pos = self.chunk_start + self.chunk_size as u64;
100        self.inner.seek(SeekFrom::Start(end_pos))?;
101        Ok(())
102    }
103
104    /// Get the chunk size
105    pub fn chunk_size(&self) -> u32 {
106        self.chunk_size
107    }
108
109    /// Get a reference to the underlying reader
110    pub fn inner(&mut self) -> &mut R {
111        &mut self.inner
112    }
113
114    /// Get the current absolute position in the file
115    pub fn current_position(&mut self) -> Result<u64> {
116        Ok(self.inner.stream_position()?)
117    }
118
119    /// Seek to an absolute position in the file
120    pub fn seek_to_position(&mut self, position: u64) -> Result<()> {
121        self.inner.seek(SeekFrom::Start(position))?;
122        Ok(())
123    }
124}
125
126impl<R: Read + Seek> Read for ChunkReader<R> {
127    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
128        // Limit reads to the chunk boundary
129        let remaining = match self.remaining() {
130            Ok(r) => r as usize,
131            Err(_) => {
132                return Err(std::io::Error::other("Failed to get remaining chunk bytes"));
133            }
134        };
135
136        if remaining == 0 {
137            return Ok(0);
138        }
139
140        let to_read = buf.len().min(remaining);
141        self.inner.read(&mut buf[..to_read])
142    }
143}
144
145impl<R: Seek> Seek for ChunkReader<R> {
146    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
147        match pos {
148            SeekFrom::Start(pos) => {
149                // Interpret as chunk-relative position
150                if pos > self.chunk_size as u64 {
151                    return Err(std::io::Error::new(
152                        std::io::ErrorKind::InvalidInput,
153                        format!(
154                            "Seek position {} exceeds chunk size {}",
155                            pos, self.chunk_size
156                        ),
157                    ));
158                }
159                let absolute_pos = self.chunk_start + pos;
160                self.inner.seek(SeekFrom::Start(absolute_pos))
161            }
162            SeekFrom::End(offset) => {
163                // Seek from end of chunk
164                let end_pos = self.chunk_start + self.chunk_size as u64;
165                let target = end_pos as i64 + offset;
166                if target < self.chunk_start as i64 || target > end_pos as i64 {
167                    return Err(std::io::Error::new(
168                        std::io::ErrorKind::InvalidInput,
169                        "Seek position outside chunk bounds",
170                    ));
171                }
172                self.inner.seek(SeekFrom::Start(target as u64))
173            }
174            SeekFrom::Current(offset) => {
175                // Seek relative to current position, but stay within chunk
176                let current = self.inner.stream_position()?;
177                let target = current as i64 + offset;
178                let chunk_end = self.chunk_start + self.chunk_size as u64;
179
180                if target < self.chunk_start as i64 || target > chunk_end as i64 {
181                    return Err(std::io::Error::new(
182                        std::io::ErrorKind::InvalidInput,
183                        "Seek position outside chunk bounds",
184                    ));
185                }
186                self.inner.seek(SeekFrom::Start(target as u64))
187            }
188        }
189    }
190
191    fn stream_position(&mut self) -> std::io::Result<u64> {
192        // Return position relative to chunk start
193        let absolute_pos = self.inner.stream_position()?;
194        Ok(absolute_pos - self.chunk_start)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use std::io::Cursor;
202
203    #[test]
204    fn test_chunk_header_read() {
205        let data = b"TEST\x10\x00\x00\x00"; // "TEST" magic with size 16
206        let mut cursor = Cursor::new(data);
207
208        let header = ChunkHeader::read(&mut cursor).unwrap();
209        assert_eq!(&header.magic, b"TEST");
210        assert_eq!(header.size, 16);
211        assert_eq!(header.magic_str(), "TEST");
212        assert!(header.has_magic(b"TEST"));
213        assert!(!header.has_magic(b"FAIL"));
214    }
215
216    #[test]
217    fn test_chunk_reader_basic() {
218        let data = b"TEST\x08\x00\x00\x00abcdefgh"; // Header + 8 bytes of data
219        let mut cursor = Cursor::new(data);
220
221        let header = ChunkHeader::read(&mut cursor).unwrap();
222        let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
223
224        assert_eq!(chunk_reader.chunk_size(), 8);
225        assert_eq!(chunk_reader.remaining().unwrap(), 8);
226        assert!(!chunk_reader.is_at_end().unwrap());
227
228        let mut buf = [0u8; 4];
229        assert_eq!(chunk_reader.read(&mut buf).unwrap(), 4);
230        assert_eq!(&buf, b"abcd");
231
232        assert_eq!(chunk_reader.remaining().unwrap(), 4);
233        assert_eq!(chunk_reader.chunk_position().unwrap(), 4);
234    }
235
236    #[test]
237    fn test_chunk_reader_seek() {
238        let data = b"TEST\x08\x00\x00\x00abcdefgh";
239        let mut cursor = Cursor::new(data);
240
241        let header = ChunkHeader::read(&mut cursor).unwrap();
242        let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
243
244        // Seek to position 2 within chunk
245        chunk_reader.seek_to_chunk_offset(2).unwrap();
246        assert_eq!(chunk_reader.chunk_position().unwrap(), 2);
247
248        let mut buf = [0u8; 2];
249        assert_eq!(chunk_reader.read(&mut buf).unwrap(), 2);
250        assert_eq!(&buf, b"cd");
251
252        // Test seek bounds
253        assert!(chunk_reader.seek_to_chunk_offset(10).is_err()); // Beyond chunk
254    }
255
256    #[test]
257    fn test_chunk_reader_offset_resolution() {
258        let data = b"TEST\x08\x00\x00\x00abcdefgh";
259        let mut cursor = Cursor::new(data);
260
261        let header = ChunkHeader::read(&mut cursor).unwrap();
262        let chunk_reader = ChunkReader::new(cursor, header).unwrap();
263
264        // Chunk starts at position 8 (after 8-byte header)
265        assert_eq!(chunk_reader.resolve_offset(0), 8);
266        assert_eq!(chunk_reader.resolve_offset(4), 12);
267    }
268}