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}