Skip to main content

zipatch_rs/chunk/
fhdr.rs

1use binrw::{BinRead, BinResult, Endian};
2use std::io::Cursor;
3use tracing::debug;
4
5/// `FHDR` v2 body: minimal patch metadata used by older patch files.
6#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
7#[br(big)]
8pub struct FileHeaderV2 {
9    /// 4-byte patch type tag (e.g. `b"D000"`).
10    pub patch_type: [u8; 4],
11    /// Number of entry files declared in the patch.
12    pub entry_files: u32,
13}
14
15/// `FHDR` v3 body: full patch metadata for modern patch files.
16#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
17#[br(big)]
18pub struct FileHeaderV3 {
19    /// 4-byte patch type tag (e.g. `b"D000"`).
20    pub patch_type: [u8; 4],
21    /// Number of entry files declared in the patch.
22    pub entry_files: u32,
23    /// Count of `ADIR` chunks in the patch.
24    pub add_directories: u32,
25    /// Count of `DELD` chunks in the patch.
26    pub delete_directories: u32,
27    /// Total bytes that will be removed by SQPK delete commands.
28    // Wire: lo_u32_be then hi_u32_be, combined as lo | (hi << 32)
29    #[br(parse_with = read_split_u64)]
30    pub delete_data_size: u64,
31    /// Minor version number.
32    pub minor_version: u32,
33    /// Repository identifier the patch targets.
34    pub repository_name: u32,
35    /// Total command count across all SQPK sub-commands.
36    pub commands: u32,
37    /// Count of SQPK `A` (add data) commands.
38    pub sqpk_add_commands: u32,
39    /// Count of SQPK `D` (delete data) commands.
40    pub sqpk_delete_commands: u32,
41    /// Count of SQPK `E` (expand data) commands.
42    pub sqpk_expand_commands: u32,
43    /// Count of SQPK `H` (header) commands.
44    pub sqpk_header_commands: u32,
45    /// Count of SQPK `F` (file) commands.
46    pub sqpk_file_commands: u32,
47}
48
49fn read_split_u64<R: std::io::Read + std::io::Seek>(
50    reader: &mut R,
51    endian: Endian,
52    (): (),
53) -> BinResult<u64> {
54    let lo = <u32 as BinRead>::read_options(reader, endian, ())? as u64;
55    let hi = <u32 as BinRead>::read_options(reader, endian, ())? as u64;
56    Ok(lo | (hi << 32))
57}
58
59/// `FHDR` chunk: patch file header with a version-specific body.
60#[non_exhaustive]
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum FileHeader {
63    /// Version 2 header body.
64    V2(FileHeaderV2),
65    /// Version 3 header body.
66    V3(FileHeaderV3),
67}
68
69impl FileHeader {
70    /// Returns the format version: 2 for V2 headers, 3 for V3 headers.
71    #[must_use]
72    pub fn version(&self) -> u8 {
73        match self {
74            FileHeader::V2(_) => 2,
75            FileHeader::V3(_) => 3,
76        }
77    }
78
79    /// Returns the 4-byte patch type tag (e.g. `b"D000"`).
80    #[must_use]
81    pub fn patch_type(&self) -> &[u8; 4] {
82        match self {
83            FileHeader::V2(h) => &h.patch_type,
84            FileHeader::V3(h) => &h.patch_type,
85        }
86    }
87}
88
89pub(crate) fn parse(body: &[u8]) -> crate::Result<FileHeader> {
90    let mut c = Cursor::new(body);
91    // C# ReadUInt32() is LE; version is bits 16-23 of that word
92    let version_word = <i32 as BinRead>::read_le(&mut c)?;
93    let version = (version_word as u32 >> 16) as u8;
94    if version == 3 {
95        let v3 = FileHeaderV3::read_be(&mut c)?;
96        // 0xB8 bytes of trailing unknowns — ignored, cursor is bounded by body slice
97        debug!(version = 3, entry_files = v3.entry_files, "file header");
98        Ok(FileHeader::V3(v3))
99    } else {
100        let v2 = FileHeaderV2::read_be(&mut c)?;
101        // 0x08 bytes of trailing zeros — ignored
102        debug!(version = 2, entry_files = v2.entry_files, "file header");
103        Ok(FileHeader::V2(v2))
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn v2() -> FileHeader {
112        FileHeader::V2(FileHeaderV2 {
113            patch_type: *b"D000",
114            entry_files: 1,
115        })
116    }
117
118    fn v3() -> FileHeader {
119        FileHeader::V3(FileHeaderV3 {
120            patch_type: *b"H000",
121            entry_files: 5,
122            add_directories: 0,
123            delete_directories: 0,
124            delete_data_size: 0,
125            minor_version: 0,
126            repository_name: 0,
127            commands: 0,
128            sqpk_add_commands: 0,
129            sqpk_delete_commands: 0,
130            sqpk_expand_commands: 0,
131            sqpk_header_commands: 0,
132            sqpk_file_commands: 0,
133        })
134    }
135
136    #[test]
137    fn version_returns_2_for_v2() {
138        assert_eq!(v2().version(), 2);
139    }
140
141    #[test]
142    fn version_returns_3_for_v3() {
143        assert_eq!(v3().version(), 3);
144    }
145
146    #[test]
147    fn patch_type_returns_v2_tag() {
148        assert_eq!(v2().patch_type(), b"D000");
149    }
150
151    #[test]
152    fn patch_type_returns_v3_tag() {
153        assert_eq!(v3().patch_type(), b"H000");
154    }
155}