Skip to main content

zipatch_rs/chunk/sqpk/
mod.rs

1pub(crate) mod add_data;
2pub(crate) mod delete_data;
3pub(crate) mod expand_data;
4pub(crate) mod file;
5pub(crate) mod header;
6pub(crate) mod index;
7pub(crate) mod target_info;
8
9pub use add_data::SqpkAddData;
10pub use delete_data::SqpkDeleteData;
11pub use expand_data::SqpkExpandData;
12pub use file::{SqpkCompressedBlock, SqpkFile, SqpkFileOperation};
13pub use header::{SqpkHeader, SqpkHeaderTarget, TargetFileKind, TargetHeaderKind};
14pub use index::{IndexCommand, SqpkIndex, SqpkPatchInfo};
15pub use target_info::SqpkTargetInfo;
16
17use crate::reader::ReadExt;
18use crate::{Result, ZiPatchError};
19use binrw::BinRead;
20use std::io::Cursor;
21
22/// Identifier of a `SqPack` file targeted by a SQPK command.
23#[derive(BinRead, Debug, Clone, PartialEq, Eq, Hash)]
24#[br(big)]
25pub struct SqpackFile {
26    /// `SqPack` repository / category ID (`main` segment of the filename).
27    pub main_id: u16,
28    /// `SqPack` sub-category ID; the high byte encodes the expansion folder.
29    pub sub_id: u16,
30    /// Per-file index used to derive the `.datN`/`.indexN` suffix.
31    pub file_id: u32,
32}
33
34/// Sub-command of a `SQPK` chunk; the variant is selected by the command byte.
35#[non_exhaustive]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum SqpkCommand {
38    /// SQPK `A` — write data at a block offset in a `.dat` file.
39    AddData(Box<SqpkAddData>),
40    /// SQPK `D` — overwrite a block range with empty-block markers.
41    DeleteData(SqpkDeleteData),
42    /// SQPK `E` — expand a block range with empty-block markers.
43    ExpandData(SqpkExpandData),
44    /// SQPK `H` — write a 1024-byte header at offset 0 or 1024.
45    Header(SqpkHeader),
46    /// SQPK `T` — target platform / region metadata.
47    TargetInfo(SqpkTargetInfo),
48    /// SQPK `F` — add, delete, or otherwise mutate a whole file.
49    File(Box<SqpkFile>),
50    /// SQPK `I` — index entry add/delete metadata; not applied directly.
51    Index(SqpkIndex),
52    /// SQPK `X` — patch install info metadata; not applied directly.
53    PatchInfo(SqpkPatchInfo),
54}
55
56/// Parse a SQPK chunk body into a [`SqpkCommand`] variant.
57pub fn parse_sqpk(body: &[u8]) -> Result<SqpkCommand> {
58    let mut c = Cursor::new(body);
59    let inner_size = c.read_i32_be()? as usize;
60    if inner_size != body.len() {
61        return Err(ZiPatchError::InvalidField {
62            context: "SQPK inner size mismatch",
63        });
64    }
65    let command = c.read_u8()?;
66    let cmd_body = &body[5..];
67
68    match command {
69        b'T' => Ok(SqpkCommand::TargetInfo(target_info::parse(cmd_body)?)),
70        b'I' => Ok(SqpkCommand::Index(index::parse_index(cmd_body)?)),
71        b'X' => Ok(SqpkCommand::PatchInfo(index::parse_patch_info(cmd_body)?)),
72        b'A' => Ok(SqpkCommand::AddData(Box::new(add_data::parse(cmd_body)?))),
73        b'D' => Ok(SqpkCommand::DeleteData(delete_data::parse(cmd_body)?)),
74        b'E' => Ok(SqpkCommand::ExpandData(expand_data::parse(cmd_body)?)),
75        b'H' => Ok(SqpkCommand::Header(header::parse(cmd_body)?)),
76        b'F' => Ok(SqpkCommand::File(Box::new(file::parse(cmd_body)?))),
77        _ => Err(ZiPatchError::UnknownSqpkCommand(command)),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::{SqpkCommand, parse_sqpk};
84
85    fn make_sqpk_body(command: u8, cmd_body: &[u8]) -> Vec<u8> {
86        let total = 5 + cmd_body.len();
87        let mut out = Vec::with_capacity(total);
88        out.extend_from_slice(&(total as i32).to_be_bytes());
89        out.push(command);
90        out.extend_from_slice(cmd_body);
91        out
92    }
93
94    #[test]
95    fn parses_target_info() {
96        let mut cmd_body = Vec::new();
97        cmd_body.extend_from_slice(&[0u8; 3]); // reserved
98        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // platform Win32
99        cmd_body.extend_from_slice(&(-1i16).to_be_bytes()); // region Global
100        cmd_body.extend_from_slice(&0i16.to_be_bytes()); // not debug
101        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // version
102        cmd_body.extend_from_slice(&1234u64.to_le_bytes()); // deleted_data_size
103        cmd_body.extend_from_slice(&5678u64.to_le_bytes()); // seek_count
104
105        let body = make_sqpk_body(b'T', &cmd_body);
106        match parse_sqpk(&body).unwrap() {
107            SqpkCommand::TargetInfo(t) => {
108                assert_eq!(t.platform_id, 0);
109                assert_eq!(t.region, -1);
110                assert!(!t.is_debug);
111                assert_eq!(t.deleted_data_size, 1234);
112                assert_eq!(t.seek_count, 5678);
113            }
114            other => panic!("expected SqpkCommand::TargetInfo, got {other:?}"),
115        }
116    }
117
118    #[test]
119    fn rejects_inner_size_mismatch() {
120        let mut body = Vec::new();
121        body.extend_from_slice(&999i32.to_be_bytes()); // wrong inner_size
122        body.push(b'T');
123        assert!(parse_sqpk(&body).is_err());
124    }
125
126    #[test]
127    fn rejects_unknown_command() {
128        let body = make_sqpk_body(b'Z', &[]);
129        assert!(parse_sqpk(&body).is_err());
130    }
131
132    fn index_cmd_body() -> Vec<u8> {
133        let mut v = Vec::new();
134        v.push(b'A'); // IndexCommand::Add
135        v.push(0u8); // is_synonym = false
136        v.push(0u8); // alignment
137        v.extend_from_slice(&0u16.to_be_bytes()); // main_id
138        v.extend_from_slice(&0u16.to_be_bytes()); // sub_id
139        v.extend_from_slice(&0u32.to_be_bytes()); // file_id
140        v.extend_from_slice(&0u64.to_be_bytes()); // file_hash
141        v.extend_from_slice(&0u32.to_be_bytes()); // block_offset
142        v.extend_from_slice(&0u32.to_be_bytes()); // block_number
143        v
144    }
145
146    #[test]
147    fn parses_index_command() {
148        let body = make_sqpk_body(b'I', &index_cmd_body());
149        assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::Index(_)));
150    }
151
152    #[test]
153    fn parses_patch_info_command() {
154        let mut cmd_body = Vec::new();
155        cmd_body.push(0u8); // status
156        cmd_body.push(0u8); // version
157        cmd_body.push(0u8); // alignment
158        cmd_body.extend_from_slice(&0u64.to_be_bytes()); // install_size
159        let body = make_sqpk_body(b'X', &cmd_body);
160        assert!(matches!(
161            parse_sqpk(&body).unwrap(),
162            SqpkCommand::PatchInfo(_)
163        ));
164    }
165
166    #[test]
167    fn index_command_truncated_body_returns_error() {
168        // Empty `I` body — index::parse_index must error, exercising the `?` arm.
169        let body = make_sqpk_body(b'I', &[]);
170        assert!(parse_sqpk(&body).is_err());
171    }
172
173    #[test]
174    fn patch_info_command_truncated_body_returns_error() {
175        // Empty `X` body — index::parse_patch_info must error, exercising the `?` arm.
176        let body = make_sqpk_body(b'X', &[]);
177        assert!(parse_sqpk(&body).is_err());
178    }
179
180    #[test]
181    fn parses_add_data_command() {
182        let mut cmd_body = Vec::new();
183        cmd_body.extend_from_slice(&[0u8; 3]); // pad
184        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // main_id
185        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // sub_id
186        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // file_id
187        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // block_offset_raw
188        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // data_bytes_raw = 0 → no data
189        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // block_delete_number_raw
190        let body = make_sqpk_body(b'A', &cmd_body);
191        assert!(matches!(
192            parse_sqpk(&body).unwrap(),
193            SqpkCommand::AddData(_)
194        ));
195    }
196
197    #[test]
198    fn parses_delete_data_command() {
199        let mut cmd_body = Vec::new();
200        cmd_body.extend_from_slice(&[0u8; 3]); // pad
201        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // main_id
202        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // sub_id
203        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // file_id
204        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // block_offset_raw
205        cmd_body.extend_from_slice(&1u32.to_be_bytes()); // block_count
206        cmd_body.extend_from_slice(&[0u8; 4]); // reserved
207        let body = make_sqpk_body(b'D', &cmd_body);
208        assert!(matches!(
209            parse_sqpk(&body).unwrap(),
210            SqpkCommand::DeleteData(_)
211        ));
212    }
213
214    #[test]
215    fn parses_expand_data_command() {
216        let mut cmd_body = Vec::new();
217        cmd_body.extend_from_slice(&[0u8; 3]); // pad
218        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // main_id
219        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // sub_id
220        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // file_id
221        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // block_offset_raw
222        cmd_body.extend_from_slice(&1u32.to_be_bytes()); // block_count
223        cmd_body.extend_from_slice(&[0u8; 4]); // reserved
224        let body = make_sqpk_body(b'E', &cmd_body);
225        assert!(matches!(
226            parse_sqpk(&body).unwrap(),
227            SqpkCommand::ExpandData(_)
228        ));
229    }
230
231    #[test]
232    fn parses_header_command() {
233        let mut cmd_body = Vec::new();
234        cmd_body.push(b'D'); // file_kind = Dat
235        cmd_body.push(b'V'); // header_kind = Version
236        cmd_body.push(0u8); // alignment
237        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // main_id
238        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // sub_id
239        cmd_body.extend_from_slice(&0u32.to_be_bytes()); // file_id
240        cmd_body.extend_from_slice(&[0u8; 1024]); // header_data
241        let body = make_sqpk_body(b'H', &cmd_body);
242        assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::Header(_)));
243    }
244
245    #[test]
246    fn parses_file_command() {
247        let mut cmd_body = Vec::new();
248        cmd_body.push(b'A'); // operation = AddFile
249        cmd_body.extend_from_slice(&[0u8; 2]); // alignment
250        cmd_body.extend_from_slice(&0u64.to_be_bytes()); // file_offset
251        cmd_body.extend_from_slice(&0u64.to_be_bytes()); // file_size
252        cmd_body.extend_from_slice(&1u32.to_be_bytes()); // path_len = 1
253        cmd_body.extend_from_slice(&0u16.to_be_bytes()); // expansion_id
254        cmd_body.extend_from_slice(&[0u8; 2]); // padding
255        cmd_body.push(b'\0'); // path = ""
256        let body = make_sqpk_body(b'F', &cmd_body);
257        assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::File(_)));
258    }
259}