pub(crate) mod add_data;
pub(crate) mod delete_data;
pub(crate) mod expand_data;
pub(crate) mod file;
pub(crate) mod header;
pub(crate) mod index;
pub(crate) mod target_info;
pub use add_data::SqpkAddData;
pub use delete_data::SqpkDeleteData;
pub use expand_data::SqpkExpandData;
pub use file::{SqpkCompressedBlock, SqpkFile, SqpkFileOperation};
pub use header::{SqpkHeader, SqpkHeaderTarget, TargetFileKind, TargetHeaderKind};
pub use index::{IndexCommand, SqpkIndex, SqpkPatchInfo};
pub use target_info::SqpkTargetInfo;
use crate::reader::ReadExt;
use crate::{Result, ZiPatchError};
use binrw::BinRead;
use std::io::Cursor;
#[derive(BinRead, Debug, Clone, PartialEq, Eq, Hash)]
#[br(big)]
pub struct SqpackFile {
pub main_id: u16,
pub sub_id: u16,
pub file_id: u32,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SqpkCommand {
AddData(Box<SqpkAddData>),
DeleteData(SqpkDeleteData),
ExpandData(SqpkExpandData),
Header(SqpkHeader),
TargetInfo(SqpkTargetInfo),
File(Box<SqpkFile>),
Index(SqpkIndex),
PatchInfo(SqpkPatchInfo),
}
pub fn parse_sqpk(body: &[u8]) -> Result<SqpkCommand> {
let mut c = Cursor::new(body);
let inner_size = c.read_i32_be()? as usize;
if inner_size != body.len() {
return Err(ZiPatchError::InvalidField {
context: "SQPK inner size mismatch",
});
}
let command = c.read_u8()?;
let cmd_body = &body[5..];
match command {
b'T' => Ok(SqpkCommand::TargetInfo(target_info::parse(cmd_body)?)),
b'I' => Ok(SqpkCommand::Index(index::parse_index(cmd_body)?)),
b'X' => Ok(SqpkCommand::PatchInfo(index::parse_patch_info(cmd_body)?)),
b'A' => Ok(SqpkCommand::AddData(Box::new(add_data::parse(cmd_body)?))),
b'D' => Ok(SqpkCommand::DeleteData(delete_data::parse(cmd_body)?)),
b'E' => Ok(SqpkCommand::ExpandData(expand_data::parse(cmd_body)?)),
b'H' => Ok(SqpkCommand::Header(header::parse(cmd_body)?)),
b'F' => Ok(SqpkCommand::File(Box::new(file::parse(cmd_body)?))),
_ => Err(ZiPatchError::UnknownSqpkCommand(command)),
}
}
#[cfg(test)]
mod tests {
use super::{SqpkCommand, parse_sqpk};
fn make_sqpk_body(command: u8, cmd_body: &[u8]) -> Vec<u8> {
let total = 5 + cmd_body.len();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&(total as i32).to_be_bytes());
out.push(command);
out.extend_from_slice(cmd_body);
out
}
#[test]
fn parses_target_info() {
let mut cmd_body = Vec::new();
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);
match parse_sqpk(&body).unwrap() {
SqpkCommand::TargetInfo(t) => {
assert_eq!(t.platform_id, 0);
assert_eq!(t.region, -1);
assert!(!t.is_debug);
assert_eq!(t.deleted_data_size, 1234);
assert_eq!(t.seek_count, 5678);
}
other => panic!("expected SqpkCommand::TargetInfo, got {other:?}"),
}
}
#[test]
fn rejects_inner_size_mismatch() {
let mut body = Vec::new();
body.extend_from_slice(&999i32.to_be_bytes()); body.push(b'T');
assert!(parse_sqpk(&body).is_err());
}
#[test]
fn rejects_unknown_command() {
let body = make_sqpk_body(b'Z', &[]);
assert!(parse_sqpk(&body).is_err());
}
fn index_cmd_body() -> Vec<u8> {
let mut v = Vec::new();
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
}
#[test]
fn parses_index_command() {
let body = make_sqpk_body(b'I', &index_cmd_body());
assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::Index(_)));
}
#[test]
fn parses_patch_info_command() {
let mut cmd_body = Vec::new();
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);
assert!(matches!(
parse_sqpk(&body).unwrap(),
SqpkCommand::PatchInfo(_)
));
}
#[test]
fn index_command_truncated_body_returns_error() {
let body = make_sqpk_body(b'I', &[]);
assert!(parse_sqpk(&body).is_err());
}
#[test]
fn patch_info_command_truncated_body_returns_error() {
let body = make_sqpk_body(b'X', &[]);
assert!(parse_sqpk(&body).is_err());
}
#[test]
fn parses_add_data_command() {
let mut cmd_body = Vec::new();
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);
assert!(matches!(
parse_sqpk(&body).unwrap(),
SqpkCommand::AddData(_)
));
}
#[test]
fn parses_delete_data_command() {
let mut cmd_body = Vec::new();
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);
assert!(matches!(
parse_sqpk(&body).unwrap(),
SqpkCommand::DeleteData(_)
));
}
#[test]
fn parses_expand_data_command() {
let mut cmd_body = Vec::new();
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);
assert!(matches!(
parse_sqpk(&body).unwrap(),
SqpkCommand::ExpandData(_)
));
}
#[test]
fn parses_header_command() {
let mut cmd_body = Vec::new();
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);
assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::Header(_)));
}
#[test]
fn parses_file_command() {
let mut cmd_body = Vec::new();
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);
assert!(matches!(parse_sqpk(&body).unwrap(), SqpkCommand::File(_)));
}
}