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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//! Nom Parsing The MPQ File Header
//! NOTES:
//! - MPyQ uses struct_format: '<4s2I2H4I'

use super::MPQFileHeaderExt;
use super::LITTLE_ENDIAN;
use nom::error::dbg_dmp;
use nom::number::complete::{u16, u32};
use nom::*;

/// The MPQ File Header
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub struct MPQFileHeader {
    /// Size of the archive header.
    pub header_size: u32,
    /// Size of the whole archive, including the header.
    pub archive_size: u32,
    /// MoPaQ format version.
    pub format_version: u16,
    /// Power of two exponent specifying the number of 512-byte
    /// disk sectors in each logical sector in the archive.
    pub sector_size_shift: u16,
    /// Offset to the beginning of the hash table,
    /// relative to the beginning of the archive header.
    /// To `seek` it we must add the [`MPQFileHeader::offset`]
    pub hash_table_offset: u32,
    /// Offset to the beginning of the hash table,
    /// relative to the beginning of the archive header.
    /// To `seek` it we must add the [`MPQFileHeader::offset`]
    pub block_table_offset: u32,
    /// Number of entries in the hash table.
    pub hash_table_entries: u32,
    /// Number of entries in the block table.
    pub block_table_entries: u32,
    /// Extended Block Table only present in Burning Crusade format and later:
    pub extended_file_header: Option<MPQFileHeaderExt>,
    /// Store the offset at which the FileHeader was found.
    /// this is done because other offsets are relative to this one.
    pub offset: usize,
}

impl MPQFileHeader {
    /// Parses the internal fields in the expected order.
    pub fn parse(input: &[u8], offset: usize) -> IResult<&[u8], Self> {
        let (input, header_size) = Self::parse_header_size(input)?;
        let (input, archive_size) = Self::parse_archive_size(input)?;
        let (input, format_version) = Self::parse_format_version(input)?;
        let (input, sector_size_shift) = Self::parse_sector_size_shift(input)?;
        let (input, hash_table_offset) = Self::parse_hash_table_offset(input)?;
        let (input, block_table_offset) = Self::parse_block_table_offset(input)?;
        let (input, hash_table_entries) = Self::parse_hash_table_entries(input)?;
        let (input, block_table_entries) = Self::parse_block_table_entries(input)?;
        let (input, extended_file_header) =
            Self::parse_extended_header_if_needed(input, format_version)?;
        Ok((
            input,
            MPQFileHeader {
                header_size,
                archive_size,
                format_version,
                sector_size_shift,
                hash_table_offset,
                block_table_offset,
                hash_table_entries,
                block_table_entries,
                extended_file_header,
                offset,
            },
        ))
    }

    /// `Offset 0x04`: int32 HeaderSize
    ///
    /// Size of the archive header.
    pub fn parse_header_size(input: &[u8]) -> IResult<&[u8], u32> {
        dbg_dmp(u32(LITTLE_ENDIAN), "header_size")(input)
    }

    /// `Offset: 0x08`: int32 ArchiveSize
    ///
    /// Size of the whole archive, including the header.
    /// Does not include the strong digital signature, if present.
    /// This size is used, among other things, for determining the
    /// region to hash in computing the digital signature.
    /// This field is deprecated in the Burning Crusade MoPaQ format,
    /// and the size of the archive
    /// is calculated as the size from the beginning of the archive to
    /// the end of the hash table, block table, or extended block table
    /// (whichever is largest).
    pub fn parse_archive_size(input: &[u8]) -> IResult<&[u8], u32> {
        dbg_dmp(u32(LITTLE_ENDIAN), "archive_size")(input)
    }

    /// `Offset 0x0c`: int16 FormatVersion
    ///
    /// MoPaQ format version. MPQAPI will not open archives where
    /// this is negative. Known versions:
    /// - `0x0000` Original format. HeaderSize should be `0x20`, and large
    ///          archives are not supported.
    /// - `0x0001` Burning Crusade format. Header size should be `0x2c`,
    ///          and large archives are supported.
    pub fn parse_format_version(input: &[u8]) -> IResult<&[u8], u16> {
        dbg_dmp(u16(LITTLE_ENDIAN), "format_version")(input)
    }

    /// `Offset 0x0e`: int8 SectorSizeShift
    ///
    /// Power of two exponent specifying the number of 512-byte
    /// disk sectors in each logical sector in the archive. The size
    /// of each logical sector in the archive is:
    /// `512` * `2^SectorSizeShift`.
    /// Bugs in the Storm library dictate that this shouldalways be:
    /// 3 (4096 byte sectors).
    pub fn parse_sector_size_shift(input: &[u8]) -> IResult<&[u8], u16> {
        dbg_dmp(u16(LITTLE_ENDIAN), "sector_size_shift")(input)
    }

    /// `Offset 0x10`: int32 HashTableOffset
    ///
    /// Offset to the beginning of the hash table,
    /// relative to the beginning of the archive.
    pub fn parse_hash_table_offset(input: &[u8]) -> IResult<&[u8], u32> {
        dbg_dmp(u32(LITTLE_ENDIAN), "hash_table_offset")(input)
    }

    /// `Offset 0x14`: int32 BlockTableOffset
    ///
    /// Offset to the beginning of the block table,
    /// relative to the beginning of the archive.
    pub fn parse_block_table_offset(input: &[u8]) -> IResult<&[u8], u32> {
        dbg_dmp(u32(LITTLE_ENDIAN), "block_table_offset")(input)
    }

    /// `Offset 0x18`: int32 HashTableEntries
    ///
    /// Number of entries in the hash table.
    /// Must be a power of two, and must be:
    ///   less than `2^16` for the original MoPaQ format,
    ///   or less than `2^20` for the Burning Crusade format.
    pub fn parse_hash_table_entries(input: &[u8]) -> IResult<&[u8], u32> {
        dbg_dmp(u32(LITTLE_ENDIAN), "hash_table_entries")(input)
    }

    /// `Offset 0x1c`: int32 BlockTableEntries
    ///
    /// Number of entries in the block table.
    pub fn parse_block_table_entries(input: &[u8]) -> IResult<&[u8], u32> {
        dbg_dmp(u32(LITTLE_ENDIAN), "block_table_entries")(input)
    }

    /// `Offset 0x20`: ExtendedBlockTable
    ///
    /// Extended Block Table only present in Burning Crusade format and later:
    pub fn parse_extended_header_if_needed(
        input: &[u8],
        format_version: u16,
    ) -> IResult<&[u8], Option<MPQFileHeaderExt>> {
        if format_version != 1u16 {
            return Ok((input, None));
        }
        let (input, extended_file_header) = MPQFileHeaderExt::parse(input)?;
        Ok((input, Some(extended_file_header)))
    }
}

#[cfg(test)]
pub mod tests {
    use crate::parser::*;

    pub fn basic_file_header() -> Vec<u8> {
        // struct_format: '<4s2I2H4I'
        vec![
            b'M',
            b'P',
            b'Q', // Magic
            MPQ_ARCHIVE_HEADER_TYPE,
            0xd0,
            0x00,
            0x00,
            0x00, // header_size
            0xcf,
            0xa3,
            0x03,
            0x00, // archive_size
            0x03,
            0x00, // format_version
            0x05,
            0x00, // sector_size_shift
            0xbf,
            0xa0,
            0x03,
            0x00, // hash_table_offset
            0xbf,
            0xa2,
            0x03,
            0x00, // block_table_offset
            0x01,
            0x00,
            0x00,
            0x00, // hash_table_entries
            0x02,
            0x00,
            0x00,
            0x00, // block_table_entries
        ]
    }

    #[test]
    fn it_parses_header() {
        // The archive header by itself
        let basic_file_header_input = basic_file_header();
        let (input, header_type) = get_header_type(&basic_file_header_input).unwrap();
        assert_eq!(header_type, MPQSectionType::Header);
        let (_input, header_data) = MPQFileHeader::parse(input, 0).unwrap();
        assert_eq!(header_data.hash_table_entries, 1);
        assert_eq!(header_data.block_table_entries, 2);
    }
}