zipatch_rs/chunk/sqpk/
mod.rs1pub(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#[derive(BinRead, Debug, Clone, PartialEq, Eq, Hash)]
24#[br(big)]
25pub struct SqpackFile {
26 pub main_id: u16,
28 pub sub_id: u16,
30 pub file_id: u32,
32}
33
34#[non_exhaustive]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum SqpkCommand {
38 AddData(Box<SqpkAddData>),
40 DeleteData(SqpkDeleteData),
42 ExpandData(SqpkExpandData),
44 Header(SqpkHeader),
46 TargetInfo(SqpkTargetInfo),
48 File(Box<SqpkFile>),
50 Index(SqpkIndex),
52 PatchInfo(SqpkPatchInfo),
54}
55
56pub 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]); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&(-1i16).to_be_bytes()); cmd_body.extend_from_slice(&0i16.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&1234u64.to_le_bytes()); cmd_body.extend_from_slice(&5678u64.to_le_bytes()); 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()); 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'); v.push(0u8); v.push(0u8); v.extend_from_slice(&0u16.to_be_bytes()); v.extend_from_slice(&0u16.to_be_bytes()); v.extend_from_slice(&0u32.to_be_bytes()); v.extend_from_slice(&0u64.to_be_bytes()); v.extend_from_slice(&0u32.to_be_bytes()); v.extend_from_slice(&0u32.to_be_bytes()); 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); cmd_body.push(0u8); cmd_body.push(0u8); cmd_body.extend_from_slice(&0u64.to_be_bytes()); 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 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 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]); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); 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]); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&1u32.to_be_bytes()); cmd_body.extend_from_slice(&[0u8; 4]); 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]); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&1u32.to_be_bytes()); cmd_body.extend_from_slice(&[0u8; 4]); 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'); cmd_body.push(b'V'); cmd_body.push(0u8); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u32.to_be_bytes()); cmd_body.extend_from_slice(&[0u8; 1024]); 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'); cmd_body.extend_from_slice(&[0u8; 2]); cmd_body.extend_from_slice(&0u64.to_be_bytes()); cmd_body.extend_from_slice(&0u64.to_be_bytes()); cmd_body.extend_from_slice(&1u32.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&[0u8; 2]); cmd_body.push(b'\0'); let body = make_sqpk_body(b'F', &cmd_body);
257 assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::File(_)));
258 }
259}