Skip to main content

zipatch_rs/chunk/sqpk/
add_data.rs

1use binrw::BinRead;
2use std::io::Cursor;
3
4use super::SqpackFileId;
5
6// SQPK 'A' body layout (all big-endian):
7//   3 pad + 2 main_id + 2 sub_id + 4 file_id          (SqpackFileId, padded)
8//   + 4 block_offset_raw + 4 data_bytes_raw + 4 block_delete_number_raw
9//   = 23 bytes before the inline data payload
10const SQPK_ADDDATA_HEADER_SIZE: u64 = 23;
11
12/// SQPK `A` command body: write an inline data payload into a `.dat` file,
13/// then zero a trailing region.
14///
15/// The `A` command is the primary mechanism for patching game data. It carries
16/// its payload inline in the patch file: the bytes at `data[0..data_bytes]` are
17/// written to the resolved `.dat` file starting at `block_offset`, and then
18/// `block_delete_number` additional bytes are zeroed out immediately after the
19/// payload. This zeroing step logically marks the trailing range of blocks as
20/// deleted / available.
21///
22/// ## Wire format (all big-endian)
23///
24/// ```text
25/// ┌────────────────────────────────────────────────────────────────────────┐
26/// │ <padding>              : [u8; 3]  (reserved, always zero)              │  bytes 0–2
27/// │ main_id                : u16 BE                                        │  bytes 3–4
28/// │ sub_id                 : u16 BE                                        │  bytes 5–6
29/// │ file_id                : u32 BE                                        │  bytes 7–10
30/// │ block_offset_raw       : u32 BE  multiply by 128 to get byte offset    │  bytes 11–14
31/// │ data_bytes_raw         : u32 BE  multiply by 128 to get payload size   │  bytes 15–18
32/// │ block_delete_number_raw: u32 BE  multiply by 128 to get zero length    │  bytes 19–22
33/// │ data                   : [u8; data_bytes]  inline payload              │  bytes 23–…
34/// └────────────────────────────────────────────────────────────────────────┘
35/// ```
36///
37/// All three raw `u32` size/offset fields are in **128-byte `SqPack` block units**
38/// and are multiplied by 128 (`<< 7`) during parsing; the decoded fields
39/// (`block_offset`, `data_bytes`, `block_delete_number`) are already in bytes.
40///
41/// ## Reference
42///
43/// # Errors
44///
45/// Parsing returns [`crate::ParseError::Decode`] if the body is too
46/// short to contain the fixed header or the declared `data_bytes` payload.
47#[derive(BinRead, Debug)]
48#[br(big)]
49pub struct SqpkAddData {
50    /// `SqPack` file to write into.
51    ///
52    /// Preceded by 3 bytes of alignment padding in the wire format.
53    #[br(pad_before = 3)]
54    pub target_file: SqpackFileId,
55    /// Byte offset within the target `.dat` file at which to begin writing.
56    ///
57    /// Decoded from a raw big-endian `u32` by multiplying by 128 (`raw << 7`).
58    /// The raw value is in 128-byte `SqPack` block units.
59    #[br(map = |raw: u32| (raw as u64) << 7)]
60    pub block_offset: u64,
61    /// Length in bytes of the inline [`data`](SqpkAddData::data) payload.
62    ///
63    /// Decoded from a raw big-endian `u32` by multiplying by 128 (`raw << 7`).
64    /// Used to determine how many bytes to read into `data`.
65    #[br(map = |raw: u32| (raw as u64) << 7)]
66    pub data_bytes: u64,
67    /// Number of bytes to zero immediately after writing `data`.
68    ///
69    /// Decoded from a raw big-endian `u32` by multiplying by 128 (`raw << 7`).
70    /// If non-zero, the apply layer writes this many zero bytes starting at
71    /// `block_offset + data_bytes`, logically marking those blocks as freed.
72    #[br(map = |raw: u32| (raw as u64) << 7)]
73    pub block_delete_number: u64,
74    /// Inline data payload of exactly `data_bytes` bytes.
75    ///
76    /// Written verbatim to the target `.dat` file at `block_offset`. The
77    /// content is raw `SqPack` block data — compressed game assets, index
78    /// tables, etc. — as the game engine expects them.
79    #[br(count = data_bytes)]
80    pub data: Vec<u8>,
81}
82
83impl SqpkAddData {
84    /// Byte offset of the [`data`](SqpkAddData::data) field within the SQPK
85    /// command body slice (i.e. the byte slice starting after the SQPK
86    /// `inner_size` + sub-command tag).
87    ///
88    /// This constant (23) is the size of the fixed header preceding the inline
89    /// payload: 3 bytes padding + 8 bytes `SqpackFileId` + 4 + 4 + 4 bytes for
90    /// the three raw size/offset `u32`s.
91    ///
92    /// Adding this constant to the chunk's absolute position in the patch file
93    /// gives the patch-file offset where `data` begins — the value needed for
94    /// `IndexedZiPatch` random-access reads that skip decompressing the full
95    /// patch stream.
96    pub const DATA_SOURCE_OFFSET: u64 = SQPK_ADDDATA_HEADER_SIZE;
97}
98
99pub(crate) fn parse(body: &[u8]) -> crate::ParseResult<SqpkAddData> {
100    Ok(SqpkAddData::read_be(&mut Cursor::new(body))?)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn parses_add_data() {
109        let mut body = Vec::new();
110        body.extend_from_slice(&[0u8; 3]); // alignment
111        body.extend_from_slice(&1u16.to_be_bytes()); // main_id
112        body.extend_from_slice(&2u16.to_be_bytes()); // sub_id
113        body.extend_from_slice(&3u32.to_be_bytes()); // file_id
114        body.extend_from_slice(&1u32.to_be_bytes()); // block_offset raw → 1 << 7 = 128
115        body.extend_from_slice(&1u32.to_be_bytes()); // data_bytes raw → 1 << 7 = 128
116        body.extend_from_slice(&0u32.to_be_bytes()); // block_delete_number raw → 0
117        body.extend_from_slice(&[0xABu8; 128]); // data blob
118
119        let cmd = parse(&body).unwrap();
120        assert_eq!(cmd.target_file.main_id, 1);
121        assert_eq!(cmd.target_file.sub_id, 2);
122        assert_eq!(cmd.target_file.file_id, 3);
123        assert_eq!(cmd.block_offset, 128);
124        assert_eq!(cmd.data_bytes, 128);
125        assert_eq!(cmd.block_delete_number, 0);
126        assert_eq!(cmd.data.len(), 128);
127        assert!(cmd.data.iter().all(|&b| b == 0xAB));
128        assert_eq!(SqpkAddData::DATA_SOURCE_OFFSET, 23);
129    }
130}