wow_mpq/tables/
block.rs

1//! Block table implementation for MPQ archives
2
3use crate::crypto::{decrypt_block, hash_string, hash_type};
4use crate::{Error, Result};
5use byteorder::{LittleEndian, ReadBytesExt};
6use std::io::{Read, Seek, SeekFrom};
7
8/// Block table entry (16 bytes)
9#[repr(C)]
10#[derive(Debug, Clone, Copy)]
11pub struct BlockEntry {
12    /// Offset of the beginning of the file data, relative to the beginning of the archive
13    pub file_pos: u32,
14    /// Compressed file size
15    pub compressed_size: u32,
16    /// Size of uncompressed file
17    pub file_size: u32,
18    /// Flags for the file
19    pub flags: u32,
20}
21
22impl BlockEntry {
23    // Flag constants
24    /// File is compressed using PKWARE Data compression library
25    pub const FLAG_IMPLODE: u32 = 0x00000100;
26    /// File is compressed using one or more compression methods
27    pub const FLAG_COMPRESS: u32 = 0x00000200;
28    /// File is encrypted
29    pub const FLAG_ENCRYPTED: u32 = 0x00010000;
30    /// The decryption key for the file is adjusted by the block position
31    pub const FLAG_FIX_KEY: u32 = 0x00020000;
32    /// The file is a patch file
33    pub const FLAG_PATCH_FILE: u32 = 0x00100000;
34    /// File is stored as a single unit, not split into sectors
35    pub const FLAG_SINGLE_UNIT: u32 = 0x01000000;
36    /// File is a deletion marker
37    pub const FLAG_DELETE_MARKER: u32 = 0x02000000;
38    /// File has checksums for each sector
39    pub const FLAG_SECTOR_CRC: u32 = 0x04000000;
40    /// File exists in the archive
41    pub const FLAG_EXISTS: u32 = 0x80000000;
42
43    /// Check if the file is compressed
44    pub fn is_compressed(&self) -> bool {
45        (self.flags & (Self::FLAG_IMPLODE | Self::FLAG_COMPRESS)) != 0
46    }
47
48    /// Check if the file is encrypted
49    pub fn is_encrypted(&self) -> bool {
50        (self.flags & Self::FLAG_ENCRYPTED) != 0
51    }
52
53    /// Check if the file is stored as a single unit
54    pub fn is_single_unit(&self) -> bool {
55        (self.flags & Self::FLAG_SINGLE_UNIT) != 0
56    }
57
58    /// Check if the file has sector CRCs
59    pub fn has_sector_crc(&self) -> bool {
60        (self.flags & Self::FLAG_SECTOR_CRC) != 0
61    }
62
63    /// Check if the file exists
64    pub fn exists(&self) -> bool {
65        (self.flags & Self::FLAG_EXISTS) != 0
66    }
67
68    /// Check if the file uses fixed key encryption
69    pub fn has_fix_key(&self) -> bool {
70        (self.flags & Self::FLAG_FIX_KEY) != 0
71    }
72
73    /// Check if the file is a patch file
74    pub fn is_patch_file(&self) -> bool {
75        (self.flags & Self::FLAG_PATCH_FILE) != 0
76    }
77
78    /// Read a block entry from raw bytes
79    pub fn from_bytes(data: &[u8]) -> Result<Self> {
80        if data.len() < 16 {
81            return Err(Error::invalid_format("Block entry too small"));
82        }
83
84        let mut cursor = std::io::Cursor::new(data);
85        Ok(Self {
86            file_pos: cursor.read_u32::<LittleEndian>()?,
87            compressed_size: cursor.read_u32::<LittleEndian>()?,
88            file_size: cursor.read_u32::<LittleEndian>()?,
89            flags: cursor.read_u32::<LittleEndian>()?,
90        })
91    }
92}
93
94/// Block table
95#[derive(Debug)]
96pub struct BlockTable {
97    entries: Vec<BlockEntry>,
98}
99
100impl BlockTable {
101    /// Create a new empty block table
102    pub fn new(size: usize) -> Result<Self> {
103        let entries = vec![
104            BlockEntry {
105                file_pos: 0,
106                compressed_size: 0,
107                file_size: 0,
108                flags: 0,
109            };
110            size
111        ];
112        Ok(Self { entries })
113    }
114
115    /// Read and decrypt a block table from the archive
116    pub fn read<R: Read + Seek>(reader: &mut R, offset: u64, size: u32) -> Result<Self> {
117        // Seek to block table position
118        reader.seek(SeekFrom::Start(offset))?;
119
120        // Read raw data
121        let byte_size = size as usize * 16; // 16 bytes per entry
122        let mut raw_data = vec![0u8; byte_size];
123        reader.read_exact(&mut raw_data)?;
124
125        // Decrypt the table - SAFE VERSION
126        let key = hash_string("(block table)", hash_type::FILE_KEY);
127
128        // Convert to u32s, decrypt, then convert back
129        let mut u32_buffer: Vec<u32> = raw_data
130            .chunks_exact(4)
131            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
132            .collect();
133
134        decrypt_block(&mut u32_buffer, key);
135
136        // Write decrypted u32s back to bytes
137        for (chunk, &decrypted) in raw_data.chunks_exact_mut(4).zip(&u32_buffer) {
138            chunk.copy_from_slice(&decrypted.to_le_bytes());
139        }
140
141        // Parse entries
142        let mut entries = Vec::with_capacity(size as usize);
143        for i in 0..size as usize {
144            let offset = i * 16;
145            let entry = BlockEntry::from_bytes(&raw_data[offset..offset + 16])?;
146            entries.push(entry);
147        }
148
149        Ok(Self { entries })
150    }
151
152    /// Create a block table from bytes (needs decryption)
153    pub fn from_bytes(data: &[u8], size: u32) -> Result<Self> {
154        // Validate data size
155        let expected_size = size as usize * 16; // 16 bytes per entry
156        if data.len() < expected_size {
157            return Err(Error::block_table("Insufficient data for block table"));
158        }
159
160        // Copy data for decryption
161        let mut raw_data = data[..expected_size].to_vec();
162
163        // Decrypt the table
164        let key = hash_string("(block table)", hash_type::FILE_KEY);
165
166        // Convert to u32s, decrypt, then convert back
167        let mut u32_buffer: Vec<u32> = raw_data
168            .chunks_exact(4)
169            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
170            .collect();
171
172        decrypt_block(&mut u32_buffer, key);
173
174        // Write decrypted u32s back to bytes
175        for (chunk, &decrypted) in raw_data.chunks_exact_mut(4).zip(&u32_buffer) {
176            chunk.copy_from_slice(&decrypted.to_le_bytes());
177        }
178
179        // Parse entries
180        let mut entries = Vec::with_capacity(size as usize);
181        for i in 0..size as usize {
182            let offset = i * 16;
183            let entry = BlockEntry::from_bytes(&raw_data[offset..offset + 16])?;
184            entries.push(entry);
185        }
186
187        Ok(Self { entries })
188    }
189
190    /// Get all entries
191    pub fn entries(&self) -> &[BlockEntry] {
192        &self.entries
193    }
194
195    /// Get a specific entry
196    pub fn get(&self, index: usize) -> Option<&BlockEntry> {
197        self.entries.get(index)
198    }
199
200    /// Get the size of the block table
201    pub fn size(&self) -> usize {
202        self.entries.len()
203    }
204
205    /// Create a new block table with mutable entries
206    pub fn new_mut(size: usize) -> Result<Self> {
207        let entries = vec![
208            BlockEntry {
209                file_pos: 0,
210                compressed_size: 0,
211                file_size: 0,
212                flags: 0,
213            };
214            size
215        ];
216        Ok(Self { entries })
217    }
218
219    /// Get a mutable reference to a specific entry
220    pub fn get_mut(&mut self, index: usize) -> Option<&mut BlockEntry> {
221        self.entries.get_mut(index)
222    }
223
224    /// Get mutable access to all entries
225    pub fn entries_mut(&mut self) -> &mut [BlockEntry] {
226        &mut self.entries
227    }
228
229    /// Clear all entries
230    pub fn clear(&mut self) {
231        for entry in &mut self.entries {
232            *entry = BlockEntry {
233                file_pos: 0,
234                compressed_size: 0,
235                file_size: 0,
236                flags: 0,
237            };
238        }
239    }
240}
241
242/// Hi-block table for archives > 4GB (v2+)
243#[derive(Debug)]
244pub struct HiBlockTable {
245    entries: Vec<u16>,
246}
247
248impl HiBlockTable {
249    /// Read the hi-block table
250    pub fn read<R: Read + Seek>(reader: &mut R, offset: u64, size: u32) -> Result<Self> {
251        reader.seek(SeekFrom::Start(offset))?;
252
253        let mut entries = Vec::with_capacity(size as usize);
254        for _ in 0..size {
255            entries.push(reader.read_u16::<LittleEndian>()?);
256        }
257
258        Ok(Self { entries })
259    }
260
261    /// Get a hi-block entry
262    pub fn get(&self, index: usize) -> Option<u16> {
263        self.entries.get(index).copied()
264    }
265
266    /// Calculate full 64-bit file position
267    pub fn get_file_pos_high(&self, index: usize) -> u64 {
268        self.get(index).unwrap_or(0) as u64
269    }
270
271    /// Create a new Hi-block table with the given size
272    pub fn new(size: usize) -> Self {
273        Self {
274            entries: vec![0; size],
275        }
276    }
277
278    /// Set a hi-block entry
279    pub fn set(&mut self, index: usize, value: u16) {
280        if let Some(entry) = self.entries.get_mut(index) {
281            *entry = value;
282        }
283    }
284
285    /// Get all entries
286    pub fn entries(&self) -> &[u16] {
287        &self.entries
288    }
289
290    /// Check if any entry has a non-zero value (i.e., if the table is needed)
291    pub fn is_needed(&self) -> bool {
292        self.entries.iter().any(|&v| v != 0)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_block_entry_flags() {
302        let compressed = BlockEntry {
303            file_pos: 0,
304            compressed_size: 100,
305            file_size: 200,
306            flags: BlockEntry::FLAG_COMPRESS | BlockEntry::FLAG_EXISTS,
307        };
308        assert!(compressed.is_compressed());
309        assert!(!compressed.is_encrypted());
310        assert!(compressed.exists());
311
312        let encrypted = BlockEntry {
313            file_pos: 0,
314            compressed_size: 100,
315            file_size: 100,
316            flags: BlockEntry::FLAG_ENCRYPTED | BlockEntry::FLAG_FIX_KEY | BlockEntry::FLAG_EXISTS,
317        };
318        assert!(encrypted.is_encrypted());
319        assert!(encrypted.has_fix_key());
320        assert!(!encrypted.is_compressed());
321    }
322}