1use binrw::{BinRead, BinResult, Endian};
2use std::io::Cursor;
3use tracing::debug;
4
5#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
7#[br(big)]
8pub struct FileHeaderV2 {
9 pub patch_type: [u8; 4],
11 pub entry_files: u32,
13}
14
15#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
17#[br(big)]
18pub struct FileHeaderV3 {
19 pub patch_type: [u8; 4],
21 pub entry_files: u32,
23 pub add_directories: u32,
25 pub delete_directories: u32,
27 #[br(parse_with = read_split_u64)]
30 pub delete_data_size: u64,
31 pub minor_version: u32,
33 pub repository_name: u32,
35 pub commands: u32,
37 pub sqpk_add_commands: u32,
39 pub sqpk_delete_commands: u32,
41 pub sqpk_expand_commands: u32,
43 pub sqpk_header_commands: u32,
45 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#[non_exhaustive]
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum FileHeader {
63 V2(FileHeaderV2),
65 V3(FileHeaderV3),
67}
68
69impl FileHeader {
70 #[must_use]
72 pub fn version(&self) -> u8 {
73 match self {
74 FileHeader::V2(_) => 2,
75 FileHeader::V3(_) => 3,
76 }
77 }
78
79 #[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 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 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 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}