wow_mpq/tables/
hash.rs

1//! Hash 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/// Hash table entry (16 bytes)
9#[repr(C)]
10#[derive(Debug, Clone, Copy)]
11pub struct HashEntry {
12    /// The hash of the full file name (part A)
13    pub name_1: u32,
14    /// The hash of the full file name (part B)
15    pub name_2: u32,
16    /// The language of the file (Windows LANGID)
17    pub locale: u16,
18    /// The platform the file is used for (vestigial - always 0 in practice)
19    pub platform: u16,
20    /// Block table index or special value
21    pub block_index: u32,
22}
23
24impl HashEntry {
25    /// Value indicating the hash entry has never been used
26    pub const EMPTY_NEVER_USED: u32 = 0xFFFFFFFF;
27    /// Value indicating the hash entry was deleted
28    pub const EMPTY_DELETED: u32 = 0xFFFFFFFE;
29
30    /// Create an empty hash entry
31    pub fn empty() -> Self {
32        Self {
33            name_1: 0,
34            name_2: 0,
35            locale: 0,
36            platform: 0,
37            block_index: Self::EMPTY_NEVER_USED,
38        }
39    }
40
41    /// Check if this entry has never been used
42    pub fn is_empty(&self) -> bool {
43        self.block_index == Self::EMPTY_NEVER_USED
44    }
45
46    /// Check if this entry was deleted
47    pub fn is_deleted(&self) -> bool {
48        self.block_index == Self::EMPTY_DELETED
49    }
50
51    /// Check if this entry contains valid file information
52    pub fn is_valid(&self) -> bool {
53        self.block_index < Self::EMPTY_DELETED
54    }
55
56    /// Read a hash entry from raw bytes
57    pub fn from_bytes(data: &[u8]) -> Result<Self> {
58        if data.len() < 16 {
59            return Err(Error::invalid_format("Hash entry too small"));
60        }
61
62        let mut cursor = std::io::Cursor::new(data);
63        Ok(Self {
64            name_1: cursor.read_u32::<LittleEndian>()?,
65            name_2: cursor.read_u32::<LittleEndian>()?,
66            locale: cursor.read_u16::<LittleEndian>()?,
67            platform: cursor.read_u16::<LittleEndian>()?,
68            block_index: cursor.read_u32::<LittleEndian>()?,
69        })
70    }
71}
72
73/// Hash table
74#[derive(Debug)]
75pub struct HashTable {
76    entries: Vec<HashEntry>,
77    mask: usize,
78}
79
80impl HashTable {
81    /// Create a new empty hash table
82    pub fn new(size: usize) -> Result<Self> {
83        // Validate size is power of 2
84        if !crate::is_power_of_two(size as u32) {
85            return Err(Error::hash_table("Hash table size must be power of 2"));
86        }
87
88        let entries = vec![HashEntry::empty(); size];
89        Ok(Self {
90            entries,
91            mask: size - 1,
92        })
93    }
94
95    /// Read and decrypt a hash table from the archive
96    pub fn read<R: Read + Seek>(reader: &mut R, offset: u64, size: u32) -> Result<Self> {
97        // Validate size
98        if !crate::is_power_of_two(size) {
99            return Err(Error::hash_table("Hash table size must be power of 2"));
100        }
101
102        // Seek to hash table position
103        reader.seek(SeekFrom::Start(offset))?;
104
105        // Read raw data
106        let byte_size = size as usize * 16; // 16 bytes per entry
107        let mut raw_data = vec![0u8; byte_size];
108        reader.read_exact(&mut raw_data)?;
109
110        // Decrypt the table - SAFE VERSION
111        let key = hash_string("(hash table)", hash_type::FILE_KEY);
112
113        // Convert to u32s, decrypt, then convert back
114        let mut u32_buffer: Vec<u32> = raw_data
115            .chunks_exact(4)
116            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
117            .collect();
118
119        decrypt_block(&mut u32_buffer, key);
120
121        // Write decrypted u32s back to bytes
122        for (chunk, &decrypted) in raw_data.chunks_exact_mut(4).zip(&u32_buffer) {
123            chunk.copy_from_slice(&decrypted.to_le_bytes());
124        }
125
126        // Parse entries
127        let mut entries = Vec::with_capacity(size as usize);
128        for i in 0..size as usize {
129            let offset = i * 16;
130            let entry = HashEntry::from_bytes(&raw_data[offset..offset + 16])?;
131            entries.push(entry);
132        }
133
134        Ok(Self {
135            entries,
136            mask: size as usize - 1,
137        })
138    }
139
140    /// Create a hash table from bytes (needs decryption)
141    pub fn from_bytes(data: &[u8], size: u32) -> Result<Self> {
142        // Validate size
143        if !crate::is_power_of_two(size) {
144            return Err(Error::hash_table("Hash table size must be power of 2"));
145        }
146
147        // Validate data size
148        let expected_size = size as usize * 16; // 16 bytes per entry
149        if data.len() < expected_size {
150            return Err(Error::hash_table("Insufficient data for hash table"));
151        }
152
153        // Copy data for decryption
154        let mut raw_data = data[..expected_size].to_vec();
155
156        // Decrypt the table
157        let key = hash_string("(hash table)", hash_type::FILE_KEY);
158
159        // Convert to u32s, decrypt, then convert back
160        let mut u32_buffer: Vec<u32> = raw_data
161            .chunks_exact(4)
162            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
163            .collect();
164
165        decrypt_block(&mut u32_buffer, key);
166
167        // Write decrypted u32s back to bytes
168        for (chunk, &decrypted) in raw_data.chunks_exact_mut(4).zip(&u32_buffer) {
169            chunk.copy_from_slice(&decrypted.to_le_bytes());
170        }
171
172        // Parse entries
173        let mut entries = Vec::with_capacity(size as usize);
174        for i in 0..size as usize {
175            let offset = i * 16;
176            let entry = HashEntry::from_bytes(&raw_data[offset..offset + 16])?;
177            entries.push(entry);
178        }
179
180        Ok(Self {
181            entries,
182            mask: size as usize - 1,
183        })
184    }
185
186    /// Get all entries
187    pub fn entries(&self) -> &[HashEntry] {
188        &self.entries
189    }
190
191    /// Get a specific entry
192    pub fn get(&self, index: usize) -> Option<&HashEntry> {
193        self.entries.get(index)
194    }
195
196    /// Get the size of the hash table
197    pub fn size(&self) -> usize {
198        self.entries.len()
199    }
200
201    /// Find a file in the hash table
202    pub fn find_file(&self, filename: &str, locale: u16) -> Option<(usize, &HashEntry)> {
203        // Calculate hash values
204        let name_a = hash_string(filename, hash_type::NAME_A);
205        let name_b = hash_string(filename, hash_type::NAME_B);
206        let start_index = hash_string(filename, hash_type::TABLE_OFFSET) as usize;
207
208        let mut index = start_index & self.mask;
209        let end_index = index;
210
211        // Linear probing to find the file
212        loop {
213            let entry = &self.entries[index];
214
215            // Check if this is our file
216            if entry.name_1 == name_a && entry.name_2 == name_b {
217                // Check locale (0 = default/any locale)
218                if (locale == 0 || entry.locale == 0 || entry.locale == locale) && entry.is_valid()
219                {
220                    return Some((index, entry));
221                }
222            }
223
224            // If we hit an empty entry that was never used, file doesn't exist
225            if entry.is_empty() {
226                return None;
227            }
228
229            // Continue to next entry
230            index = (index + 1) & self.mask;
231
232            // If we've wrapped around to where we started, file doesn't exist
233            if index == end_index {
234                return None;
235            }
236        }
237    }
238
239    /// Create a new hash table with mutable entries
240    pub fn new_mut(size: usize) -> Result<Self> {
241        // Validate size is power of 2
242        if !crate::is_power_of_two(size as u32) {
243            return Err(Error::hash_table("Hash table size must be power of 2"));
244        }
245
246        let entries = vec![HashEntry::empty(); size];
247        Ok(Self {
248            entries,
249            mask: size - 1,
250        })
251    }
252
253    /// Get a mutable reference to a specific entry
254    pub fn get_mut(&mut self, index: usize) -> Option<&mut HashEntry> {
255        self.entries.get_mut(index)
256    }
257
258    /// Get mutable access to all entries
259    pub fn entries_mut(&mut self) -> &mut [HashEntry] {
260        &mut self.entries
261    }
262
263    /// Clear all entries to empty state
264    pub fn clear(&mut self) {
265        for entry in &mut self.entries {
266            *entry = HashEntry::empty();
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_hash_entry_states() {
277        let empty = HashEntry::empty();
278        assert!(empty.is_empty());
279        assert!(!empty.is_deleted());
280        assert!(!empty.is_valid());
281
282        let deleted = HashEntry {
283            name_1: 0,
284            name_2: 0,
285            locale: 0,
286            platform: 0,
287            block_index: HashEntry::EMPTY_DELETED,
288        };
289        assert!(!deleted.is_empty());
290        assert!(deleted.is_deleted());
291        assert!(!deleted.is_valid());
292
293        let valid = HashEntry {
294            name_1: 0x12345678,
295            name_2: 0x9ABCDEF0,
296            locale: 0,
297            platform: 0,
298            block_index: 0,
299        };
300        assert!(!valid.is_empty());
301        assert!(!valid.is_deleted());
302        assert!(valid.is_valid());
303    }
304
305    #[test]
306    fn test_hash_table_size_validation() {
307        // Valid sizes (powers of 2)
308        assert!(HashTable::new(16).is_ok());
309        assert!(HashTable::new(256).is_ok());
310        assert!(HashTable::new(4096).is_ok());
311
312        // Invalid sizes
313        assert!(HashTable::new(15).is_err());
314        assert!(HashTable::new(100).is_err());
315        assert!(HashTable::new(0).is_err());
316    }
317}