zipatch-rs 1.1.0

Parser for FFXIV ZiPatch patch files
Documentation
use binrw::BinRead;
use std::io::Cursor;

use super::SqpackFile;

/// SQPK `D` command body: overwrite a contiguous block range in a `.dat` file
/// with empty-block markers, logically freeing that space.
///
/// `DeleteData` is the inverse of [`SqpkAddData`](super::SqpkAddData): where
/// `A` writes live data into a block range, `D` marks that same range as free.
/// After a `D` command the game engine treats the blocks as available for
/// reuse.
///
/// On disk the operation writes a `SqPack` empty-block header at `block_offset`
/// and zeroes the full `block_count * 128` byte range. This is identical to
/// what [`SqpkExpandData`](super::SqpkExpandData) writes; the semantic
/// difference is that `D` clears existing live content while `E` allocates
/// previously non-existent space.
///
/// The apply implementation (`src/apply/sqpk.rs`) handles both `D` and `E`
/// with the same `write_empty_block` helper.
///
/// ## Wire format (all big-endian)
///
/// ```text
/// ┌────────────────────────────────────────────────────────────────────┐
/// │ <padding>        : [u8; 3]   (reserved, always zero)               │  bytes 0–2
/// │ main_id          : u16 BE                                          │  bytes 3–4
/// │ sub_id           : u16 BE                                          │  bytes 5–6
/// │ file_id          : u32 BE                                          │  bytes 7–10
/// │ block_offset_raw : u32 BE    multiply by 128 to get byte offset    │  bytes 11–14
/// │ block_count      : u32 BE    number of 128-byte blocks to clear    │  bytes 15–18
/// │ <reserved>       : u32       (always zero)                         │  bytes 19–22
/// └────────────────────────────────────────────────────────────────────┘
/// ```
///
/// `block_offset_raw` is in **128-byte `SqPack` block units** and is multiplied
/// by 128 (`<< 7`) during parsing; `block_count` is a direct block count
/// stored as-is.
///
/// The total byte range cleared by this command is `block_count * 128` bytes
/// starting at `block_offset`.
///
/// ## Reference
///
/// See `SqpkDeleteData.cs` in the `XIVLauncher` reference implementation.
///
/// # Errors
///
/// Parsing returns [`crate::ZiPatchError::BinrwError`] if the body is too
/// short to contain all required fields.
#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
#[br(big)]
pub struct SqpkDeleteData {
    /// `SqPack` file to modify.
    ///
    /// Preceded by 3 bytes of alignment padding in the wire format.
    #[br(pad_before = 3)]
    pub target_file: SqpackFile,
    /// Byte offset within the target `.dat` file at which to begin clearing.
    ///
    /// Decoded from a raw big-endian `u32` by multiplying by 128 (`raw << 7`).
    /// The raw wire value is in 128-byte `SqPack` block units.
    #[br(map = |raw: u32| (raw as u64) << 7)]
    pub block_offset: u64,
    /// Number of 128-byte `SqPack` blocks to clear.
    ///
    /// Stored directly as a big-endian `u32` without any unit shift. The
    /// total byte length of the cleared region is `block_count * 128`.
    /// Must be non-zero; the apply layer's `write_empty_block` helper returns
    /// an error for `block_count == 0`.
    ///
    /// Followed by 4 bytes of reserved padding (`pad_after = 4`) in the wire format.
    #[br(pad_after = 4)]
    pub block_count: u32,
}

pub(crate) fn parse(body: &[u8]) -> crate::Result<SqpkDeleteData> {
    Ok(SqpkDeleteData::read_be(&mut Cursor::new(body))?)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_delete_data() {
        let mut body = Vec::new();
        body.extend_from_slice(&[0u8; 3]); // alignment
        body.extend_from_slice(&4u16.to_be_bytes()); // main_id
        body.extend_from_slice(&5u16.to_be_bytes()); // sub_id
        body.extend_from_slice(&6u32.to_be_bytes()); // file_id
        body.extend_from_slice(&2u32.to_be_bytes()); // block_offset raw → 2 << 7 = 256
        body.extend_from_slice(&7u32.to_be_bytes()); // block_count (no shift)
        body.extend_from_slice(&[0u8; 4]); // reserved

        let cmd = parse(&body).unwrap();
        assert_eq!(cmd.target_file.main_id, 4);
        assert_eq!(cmd.block_offset, 256);
        assert_eq!(cmd.block_count, 7);
    }
}