nom_mpq/parser/
mpq_file_header.rs

1//! Nom Parsing The MPQ File Header
2//! NOTES:
3//! - MPyQ uses struct_format: '<4s2I2H4I'
4
5use super::LITTLE_ENDIAN;
6use super::MPQFileHeaderExt;
7use crate::dbg_dmp;
8use nom::number::complete::{u16, u32};
9use nom::*;
10
11/// The MPQ File Header
12#[derive(Debug, Default, PartialEq, Clone, Copy)]
13pub struct MPQFileHeader {
14    /// Size of the archive header.
15    pub header_size: u32,
16    /// Size of the whole archive, including the header.
17    pub archive_size: u32,
18    /// MoPaQ format version.
19    pub format_version: u16,
20    /// Power of two exponent specifying the number of 512-byte
21    /// disk sectors in each logical sector in the archive.
22    pub sector_size_shift: u16,
23    /// Offset to the beginning of the hash table,
24    /// relative to the beginning of the archive header.
25    /// To `seek` it we must add the [`MPQFileHeader::offset`]
26    pub hash_table_offset: u32,
27    /// Offset to the beginning of the hash table,
28    /// relative to the beginning of the archive header.
29    /// To `seek` it we must add the [`MPQFileHeader::offset`]
30    pub block_table_offset: u32,
31    /// Number of entries in the hash table.
32    pub hash_table_entries: u32,
33    /// Number of entries in the block table.
34    pub block_table_entries: u32,
35    /// Extended Block Table only present in Burning Crusade format and later:
36    pub extended_file_header: Option<MPQFileHeaderExt>,
37    /// Store the offset at which the FileHeader was found.
38    /// this is done because other offsets are relative to this one.
39    pub offset: usize,
40}
41
42impl MPQFileHeader {
43    /// Parses the internal fields in the expected order.
44    pub fn parse(input: &[u8], offset: usize) -> IResult<&[u8], Self> {
45        let (input, header_size) = Self::parse_header_size(input)?;
46        let (input, archive_size) = Self::parse_archive_size(input)?;
47        let (input, format_version) = Self::parse_format_version(input)?;
48        let (input, sector_size_shift) = Self::parse_sector_size_shift(input)?;
49        let (input, hash_table_offset) = Self::parse_hash_table_offset(input)?;
50        let (input, block_table_offset) = Self::parse_block_table_offset(input)?;
51        let (input, hash_table_entries) = Self::parse_hash_table_entries(input)?;
52        let (input, block_table_entries) = Self::parse_block_table_entries(input)?;
53        let (input, extended_file_header) =
54            Self::parse_extended_header_if_needed(input, format_version)?;
55        Ok((
56            input,
57            MPQFileHeader {
58                header_size,
59                archive_size,
60                format_version,
61                sector_size_shift,
62                hash_table_offset,
63                block_table_offset,
64                hash_table_entries,
65                block_table_entries,
66                extended_file_header,
67                offset,
68            },
69        ))
70    }
71
72    /// `Offset 0x04`: int32 HeaderSize
73    ///
74    /// Size of the archive header.
75    pub fn parse_header_size(input: &[u8]) -> IResult<&[u8], u32> {
76        dbg_dmp(u32(LITTLE_ENDIAN), "header_size")(input)
77    }
78
79    /// `Offset: 0x08`: int32 ArchiveSize
80    ///
81    /// Size of the whole archive, including the header.
82    /// Does not include the strong digital signature, if present.
83    /// This size is used, among other things, for determining the
84    /// region to hash in computing the digital signature.
85    /// This field is deprecated in the Burning Crusade MoPaQ format,
86    /// and the size of the archive
87    /// is calculated as the size from the beginning of the archive to
88    /// the end of the hash table, block table, or extended block table
89    /// (whichever is largest).
90    pub fn parse_archive_size(input: &[u8]) -> IResult<&[u8], u32> {
91        dbg_dmp(u32(LITTLE_ENDIAN), "archive_size")(input)
92    }
93
94    /// `Offset 0x0c`: int16 FormatVersion
95    ///
96    /// MoPaQ format version. MPQAPI will not open archives where
97    /// this is negative. Known versions:
98    /// - `0x0000` Original format. HeaderSize should be `0x20`, and large
99    ///   archives are not supported.
100    /// - `0x0001` Burning Crusade format. Header size should be `0x2c`,
101    ///   and large archives are supported.
102    pub fn parse_format_version(input: &[u8]) -> IResult<&[u8], u16> {
103        dbg_dmp(u16(LITTLE_ENDIAN), "format_version")(input)
104    }
105
106    /// `Offset 0x0e`: int8 SectorSizeShift
107    ///
108    /// Power of two exponent specifying the number of 512-byte
109    /// disk sectors in each logical sector in the archive. The size
110    /// of each logical sector in the archive is:
111    /// `512` * `2^SectorSizeShift`.
112    /// Bugs in the Storm library dictate that this shouldalways be:
113    /// 3 (4096 byte sectors).
114    pub fn parse_sector_size_shift(input: &[u8]) -> IResult<&[u8], u16> {
115        dbg_dmp(u16(LITTLE_ENDIAN), "sector_size_shift")(input)
116    }
117
118    /// `Offset 0x10`: int32 HashTableOffset
119    ///
120    /// Offset to the beginning of the hash table,
121    /// relative to the beginning of the archive.
122    pub fn parse_hash_table_offset(input: &[u8]) -> IResult<&[u8], u32> {
123        dbg_dmp(u32(LITTLE_ENDIAN), "hash_table_offset")(input)
124    }
125
126    /// `Offset 0x14`: int32 BlockTableOffset
127    ///
128    /// Offset to the beginning of the block table,
129    /// relative to the beginning of the archive.
130    pub fn parse_block_table_offset(input: &[u8]) -> IResult<&[u8], u32> {
131        dbg_dmp(u32(LITTLE_ENDIAN), "block_table_offset")(input)
132    }
133
134    /// `Offset 0x18`: int32 HashTableEntries
135    ///
136    /// Number of entries in the hash table.
137    /// Must be a power of two, and must be:
138    ///   less than `2^16` for the original MoPaQ format,
139    ///   or less than `2^20` for the Burning Crusade format.
140    pub fn parse_hash_table_entries(input: &[u8]) -> IResult<&[u8], u32> {
141        dbg_dmp(u32(LITTLE_ENDIAN), "hash_table_entries")(input)
142    }
143
144    /// `Offset 0x1c`: int32 BlockTableEntries
145    ///
146    /// Number of entries in the block table.
147    pub fn parse_block_table_entries(input: &[u8]) -> IResult<&[u8], u32> {
148        dbg_dmp(u32(LITTLE_ENDIAN), "block_table_entries")(input)
149    }
150
151    /// `Offset 0x20`: ExtendedBlockTable
152    ///
153    /// Extended Block Table only present in Burning Crusade format and later:
154    pub fn parse_extended_header_if_needed(
155        input: &[u8],
156        format_version: u16,
157    ) -> IResult<&[u8], Option<MPQFileHeaderExt>> {
158        if format_version != 1u16 {
159            return Ok((input, None));
160        }
161        let (input, extended_file_header) = MPQFileHeaderExt::parse(input)?;
162        Ok((input, Some(extended_file_header)))
163    }
164}
165
166#[cfg(test)]
167/// MPQ File Header Tests
168pub mod tests {
169    use crate::parser::*;
170
171    /// Generates a valid basic file header
172    pub fn basic_file_header() -> Vec<u8> {
173        // struct_format: '<4s2I2H4I'
174        vec![
175            b'M',
176            b'P',
177            b'Q', // Magic
178            MPQ_ARCHIVE_HEADER_TYPE,
179            0xd0,
180            0x00,
181            0x00,
182            0x00, // header_size
183            0xcf,
184            0xa3,
185            0x03,
186            0x00, // archive_size
187            0x03,
188            0x00, // format_version
189            0x05,
190            0x00, // sector_size_shift
191            0xbf,
192            0xa0,
193            0x03,
194            0x00, // hash_table_offset
195            0xbf,
196            0xa2,
197            0x03,
198            0x00, // block_table_offset
199            0x01,
200            0x00,
201            0x00,
202            0x00, // hash_table_entries
203            0x02,
204            0x00,
205            0x00,
206            0x00, // block_table_entries
207        ]
208    }
209
210    #[test]
211    fn it_parses_header() {
212        // The archive header by itself
213        let basic_file_header_input = basic_file_header();
214        let (input, header_type) = get_header_type(&basic_file_header_input).unwrap();
215        assert_eq!(header_type, MPQSectionType::Header);
216        let (_input, header_data) = MPQFileHeader::parse(input, 0).unwrap();
217        assert_eq!(header_data.hash_table_entries, 1);
218        assert_eq!(header_data.block_table_entries, 2);
219    }
220}