use crate::crypto::{decrypt_block, hash_string, hash_type};
use crate::{Error, Result};
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::{Read, Seek, SeekFrom};
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct HashEntry {
pub name_1: u32,
pub name_2: u32,
pub locale: u16,
pub platform: u16,
pub block_index: u32,
}
impl HashEntry {
pub const EMPTY_NEVER_USED: u32 = 0xFFFFFFFF;
pub const EMPTY_DELETED: u32 = 0xFFFFFFFE;
pub fn empty() -> Self {
Self {
name_1: 0,
name_2: 0,
locale: 0,
platform: 0,
block_index: Self::EMPTY_NEVER_USED,
}
}
pub fn is_empty(&self) -> bool {
self.block_index == Self::EMPTY_NEVER_USED
}
pub fn is_deleted(&self) -> bool {
self.block_index == Self::EMPTY_DELETED
}
pub fn is_valid(&self) -> bool {
self.block_index < Self::EMPTY_DELETED
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 16 {
return Err(Error::invalid_format("Hash entry too small"));
}
let mut cursor = std::io::Cursor::new(data);
Ok(Self {
name_1: cursor.read_u32::<LittleEndian>()?,
name_2: cursor.read_u32::<LittleEndian>()?,
locale: cursor.read_u16::<LittleEndian>()?,
platform: cursor.read_u16::<LittleEndian>()?,
block_index: cursor.read_u32::<LittleEndian>()?,
})
}
}
#[derive(Debug)]
pub struct HashTable {
entries: Vec<HashEntry>,
mask: usize,
}
impl HashTable {
pub fn new(size: usize) -> Result<Self> {
if !crate::is_power_of_two(size as u32) {
return Err(Error::hash_table("Hash table size must be power of 2"));
}
let entries = vec![HashEntry::empty(); size];
Ok(Self {
entries,
mask: size - 1,
})
}
pub fn read<R: Read + Seek>(reader: &mut R, offset: u64, size: u32) -> Result<Self> {
if !crate::is_power_of_two(size) {
return Err(Error::hash_table("Hash table size must be power of 2"));
}
reader.seek(SeekFrom::Start(offset))?;
let byte_size = size as usize * 16; let mut raw_data = vec![0u8; byte_size];
reader.read_exact(&mut raw_data)?;
let key = hash_string("(hash table)", hash_type::FILE_KEY);
let mut u32_buffer: Vec<u32> = raw_data
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
decrypt_block(&mut u32_buffer, key);
for (chunk, &decrypted) in raw_data.chunks_exact_mut(4).zip(&u32_buffer) {
chunk.copy_from_slice(&decrypted.to_le_bytes());
}
let mut entries = Vec::with_capacity(size as usize);
for i in 0..size as usize {
let offset = i * 16;
let entry = HashEntry::from_bytes(&raw_data[offset..offset + 16])?;
entries.push(entry);
}
Ok(Self {
entries,
mask: size as usize - 1,
})
}
pub fn from_bytes(data: &[u8], size: u32) -> Result<Self> {
if !crate::is_power_of_two(size) {
return Err(Error::hash_table("Hash table size must be power of 2"));
}
let expected_size = size as usize * 16; if data.len() < expected_size {
return Err(Error::hash_table("Insufficient data for hash table"));
}
let mut raw_data = data[..expected_size].to_vec();
let key = hash_string("(hash table)", hash_type::FILE_KEY);
let mut u32_buffer: Vec<u32> = raw_data
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
decrypt_block(&mut u32_buffer, key);
for (chunk, &decrypted) in raw_data.chunks_exact_mut(4).zip(&u32_buffer) {
chunk.copy_from_slice(&decrypted.to_le_bytes());
}
let mut entries = Vec::with_capacity(size as usize);
for i in 0..size as usize {
let offset = i * 16;
let entry = HashEntry::from_bytes(&raw_data[offset..offset + 16])?;
entries.push(entry);
}
Ok(Self {
entries,
mask: size as usize - 1,
})
}
pub fn entries(&self) -> &[HashEntry] {
&self.entries
}
pub fn get(&self, index: usize) -> Option<&HashEntry> {
self.entries.get(index)
}
pub fn size(&self) -> usize {
self.entries.len()
}
pub fn find_file(&self, filename: &str, locale: u16) -> Option<(usize, &HashEntry)> {
let name_a = hash_string(filename, hash_type::NAME_A);
let name_b = hash_string(filename, hash_type::NAME_B);
let start_index = hash_string(filename, hash_type::TABLE_OFFSET) as usize;
let mut index = start_index & self.mask;
let end_index = index;
loop {
let entry = &self.entries[index];
if entry.name_1 == name_a && entry.name_2 == name_b {
if (locale == 0 || entry.locale == 0 || entry.locale == locale) && entry.is_valid()
{
return Some((index, entry));
}
}
if entry.is_empty() {
return None;
}
index = (index + 1) & self.mask;
if index == end_index {
return None;
}
}
}
pub fn new_mut(size: usize) -> Result<Self> {
if !crate::is_power_of_two(size as u32) {
return Err(Error::hash_table("Hash table size must be power of 2"));
}
let entries = vec![HashEntry::empty(); size];
Ok(Self {
entries,
mask: size - 1,
})
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut HashEntry> {
self.entries.get_mut(index)
}
pub fn entries_mut(&mut self) -> &mut [HashEntry] {
&mut self.entries
}
pub fn clear(&mut self) {
for entry in &mut self.entries {
*entry = HashEntry::empty();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_entry_states() {
let empty = HashEntry::empty();
assert!(empty.is_empty());
assert!(!empty.is_deleted());
assert!(!empty.is_valid());
let deleted = HashEntry {
name_1: 0,
name_2: 0,
locale: 0,
platform: 0,
block_index: HashEntry::EMPTY_DELETED,
};
assert!(!deleted.is_empty());
assert!(deleted.is_deleted());
assert!(!deleted.is_valid());
let valid = HashEntry {
name_1: 0x12345678,
name_2: 0x9ABCDEF0,
locale: 0,
platform: 0,
block_index: 0,
};
assert!(!valid.is_empty());
assert!(!valid.is_deleted());
assert!(valid.is_valid());
}
#[test]
fn test_hash_table_size_validation() {
assert!(HashTable::new(16).is_ok());
assert!(HashTable::new(256).is_ok());
assert!(HashTable::new(4096).is_ok());
assert!(HashTable::new(15).is_err());
assert!(HashTable::new(100).is_err());
assert!(HashTable::new(0).is_err());
}
}