zipatch_rs/chunk/sqpk/expand_data.rs
1use binrw::BinRead;
2use std::io::Cursor;
3
4use super::SqpackFileId;
5
6/// SQPK `E` command body: grow a `.dat` file by writing empty-block markers
7/// into a previously unallocated region.
8///
9/// `ExpandData` and [`SqpkDeleteData`](super::SqpkDeleteData) produce the same
10/// on-disk result — both write a `SqPack` empty-block header at `block_offset`
11/// followed by zeroed bytes for the full block range. The semantic difference is
12/// in the patch's intent:
13///
14/// - `E` (`ExpandData`) extends the file into space that did not previously exist,
15/// growing the archive. It typically precedes a series of `A` (`AddData`) writes
16/// into that newly allocated space.
17/// - `D` (`DeleteData`) clears existing live blocks, logically freeing them.
18///
19/// The apply implementation (`src/apply/sqpk.rs`) handles both commands with
20/// the same `write_empty_block` helper.
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/// │ block_count : u32 BE number of 128-byte blocks to allocate │ bytes 15–18
32/// │ <reserved> : u32 (always zero) │ bytes 19–22
33/// └────────────────────────────────────────────────────────────────────┘
34/// ```
35///
36/// `block_offset_raw` is in **128-byte `SqPack` block units** and is multiplied
37/// by 128 (`<< 7`) during parsing. `block_count` is a direct block count (not
38/// a byte count) and is stored as-is.
39///
40/// The total byte range affected by this command is `block_count * 128` bytes
41/// starting at `block_offset`.
42///
43/// ## Reference
44///
45/// # Errors
46///
47/// Parsing returns [`crate::ParseError::Decode`] if the body is too
48/// short to contain all required fields.
49#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
50#[br(big)]
51pub struct SqpkExpandData {
52 /// `SqPack` file to expand.
53 ///
54 /// Preceded by 3 bytes of alignment padding in the wire format.
55 #[br(pad_before = 3)]
56 pub target_file: SqpackFileId,
57 /// Byte offset within the target `.dat` file at which the new block range
58 /// begins.
59 ///
60 /// Decoded from a raw big-endian `u32` by multiplying by 128 (`raw << 7`).
61 /// The raw wire value is in 128-byte `SqPack` block units.
62 #[br(map = |raw: u32| (raw as u64) << 7)]
63 pub block_offset: u64,
64 /// Number of 128-byte `SqPack` blocks to allocate.
65 ///
66 /// Stored directly as a big-endian `u32` without any unit shift. The
67 /// total byte length of the affected region is `block_count * 128`.
68 /// Must be non-zero; the apply layer's `write_empty_block` helper returns
69 /// an error for `block_count == 0`.
70 ///
71 /// Followed by 4 bytes of reserved padding (`pad_after = 4`) in the wire format.
72 #[br(pad_after = 4)]
73 pub block_count: u32,
74}
75
76pub(crate) fn parse(body: &[u8]) -> crate::ParseResult<SqpkExpandData> {
77 Ok(SqpkExpandData::read_be(&mut Cursor::new(body))?)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn parses_expand_data() {
86 let mut body = Vec::new();
87 body.extend_from_slice(&[0u8; 3]); // alignment
88 body.extend_from_slice(&0u16.to_be_bytes()); // main_id
89 body.extend_from_slice(&0u16.to_be_bytes()); // sub_id
90 body.extend_from_slice(&1u32.to_be_bytes()); // file_id
91 body.extend_from_slice(&4u32.to_be_bytes()); // block_offset raw → 4 << 7 = 512
92 body.extend_from_slice(&10u32.to_be_bytes()); // block_count (no shift)
93 body.extend_from_slice(&[0u8; 4]); // reserved
94
95 let cmd = parse(&body).unwrap();
96 assert_eq!(cmd.block_offset, 512);
97 assert_eq!(cmd.block_count, 10);
98 }
99}