1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
//! The Hash Table Parsing
//!
//! Instead of storing file names, for quick access MoPaQs use a fixed,
//! power of two-size hash table of files in the archive. A file is uniquely
//! identified by its file path, its language, and its platform.
//! The home entry for a file in the hash table is computed as a hash of the
//! file path. In the event of a collision (the home entry is occupied by
//! another file), progressive overflow is used, and the file is placed in the
//! next available hash table entry. Searches for a desired file in the hash
//! table proceed from the home entry for the file until either the file is
//! found, the entire hash table is searched, or an empty hash table entry
//! (FileBlockIndex of 0xffffffff) is encountered.
//! The hash table is always encrypted, using the hash of "(hash table)" as the
//! key.
//! Prior to Starcraft 2, the hash table is stored uncompressed.
//! In Starcraft 2, however, the table may optionally be compressed.
//! If the offset of the block table is not equal to the offset of the
//! hash table plus the uncompressed size, Starcraft 2 interprets the
//! hash table as being compressed (not imploded).
//! This calculation assumes that the block table immediately follows the
//! hash table, and will fail or crash otherwise.
//! NOTES:
//! - MPyQ uses struct_format: '2I2HI'
//! - The format above claims the [`MPQHashTableEntry.platform`] is a u16.
//! - The devklog.net website claims the [`MPQHashTableEntry.platform`] field is u8
//! - In this implementation the u16 MPyQ version is honored.
use super::LITTLE_ENDIAN;
use nom::error::dbg_dmp;
use nom::number::complete::{u16, u32};
use nom::*;
/// The hash table entry definition
#[derive(Debug, PartialEq, Default, Clone)]
pub struct MPQHashTableEntry {
/// The hash of the file path, using method A.
pub hash_a: u32,
/// The hash of the file path, using method B.
pub hash_b: u32,
/// The language of the file.
/// See [`MPQHashTableEntry::parse_locale`] for more information.
pub locale: u16,
/// The platform the file is used for.
/// See [`MPQHashTableEntry::parse_platform`] for more information.
pub platform: u16,
/// Index into the block table of the file.
/// See [`MPQHashTableEntry::parse_block_table_index`] for more information.
pub block_table_index: u32,
}
impl MPQHashTableEntry {
/// This method is not related to parsing but for testing, maybe we should consider further
/// splitting this into a MPQHashTableEntryParser, maybe overkill.
pub fn new(
hash_a: u32,
hash_b: u32,
locale: u16,
platform: u16,
block_table_index: u32,
) -> Self {
Self {
hash_a,
hash_b,
locale,
platform,
block_table_index,
}
}
/// Parses all the fields in the expected order
pub fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (tail, hash_a) = Self::parse_hash_a(input)?;
let (tail, hash_b) = Self::parse_hash_b(tail)?;
let (tail, locale) = Self::parse_locale(tail)?;
let (tail, platform) = Self::parse_platform(tail)?;
let (tail, block_table_index) = Self::parse_block_table_index(tail)?;
Ok((
tail,
MPQHashTableEntry {
hash_a,
hash_b,
locale,
platform,
block_table_index,
},
))
}
/// `Offset 0x00`: int32 FilePathHashA
///
/// The hash of the file path, using method A.
pub fn parse_hash_a(input: &[u8]) -> IResult<&[u8], u32> {
dbg_dmp(u32(LITTLE_ENDIAN), "hash_a")(input)
}
/// `Offset 0x04`: int32 FilePathHashB
///
/// The hash of the file path, using method B.
pub fn parse_hash_b(input: &[u8]) -> IResult<&[u8], u32> {
dbg_dmp(u32(LITTLE_ENDIAN), "hash_b")(input)
}
/// `Offset 0x08`: int16 Language
///
/// The language of the file. This is a Windows LANGID data type, and uses
/// the same values.
/// 0 indicates the default language (American English), or that the file
/// is language-neutral.
pub fn parse_locale(input: &[u8]) -> IResult<&[u8], u16> {
dbg_dmp(u16(LITTLE_ENDIAN), "locale")(input)
}
/// `Offset 0x0a`: int16 Platform
///
/// The platform the file is used for. 0 indicates the default platform.
/// No other values have been observed.
pub fn parse_platform(input: &[u8]) -> IResult<&[u8], u16> {
dbg_dmp(u16(LITTLE_ENDIAN), "platform")(input)
}
/// `Offset 0x0c`: int32 FileBlockIndex
///
/// If the hash table entry is valid, this is the index into the
/// block table of the file.
/// Otherwise, one of the following two values:
///
/// * `0xffffffff` - Hash table entry is empty, and has always been empty.
/// Terminates searches for a given file.
/// * `0xfffffffe` - Hash table entry is empty, but was valid at some point
/// (in other words, the file was deleted). Does not terminate searches
/// for a given file.
pub fn parse_block_table_index(input: &[u8]) -> IResult<&[u8], u32> {
dbg_dmp(u32(LITTLE_ENDIAN), "block_table_index")(input)
}
}