zipatch-rs 1.1.0

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

/// SQPK `T` command body: declares the target platform, region, and patch
/// statistics for all subsequent operations in a patch file.
///
/// A real FFXIV patch file begins with a `T` command before any write
/// operations, so the platform declared here is in effect for all subsequent
/// path resolution. The apply layer updates [`crate::apply::ApplyContext::platform`]
/// from [`platform_id`](SqpkTargetInfo::platform_id) and otherwise ignores the
/// remaining fields.
///
/// ## Wire format
///
/// **Important:** Most fields are big-endian (the struct-level `#[br(big)]`
/// default), but [`deleted_data_size`](SqpkTargetInfo::deleted_data_size) and
/// [`seek_count`](SqpkTargetInfo::seek_count) are **little-endian**. This
/// endian anomaly is present in the original C# implementation
/// (`SqpkTargetInfo.cs`) and must be preserved exactly.
///
/// ```text
/// ┌───────────────────────────────────────────────────────────────────┐
/// │ <reserved>        : [u8; 3]  (always zero)                        │  bytes 0–2
/// │ platform_id       : u16 BE   0=Win32, 1=PS3, 2=PS4               │  bytes 3–4
/// │ region            : i16 BE   -1=Global                            │  bytes 5–6
/// │ is_debug          : i16 BE   0=release, nonzero=debug             │  bytes 7–8
/// │ version           : u16 BE   client version                        │  bytes 9–10
/// │ deleted_data_size : u64 LE   ← little-endian anomaly              │  bytes 11–18
/// │ seek_count        : u64 LE   ← little-endian anomaly              │  bytes 19–26
/// │ <trailing padding>            bounded by the body slice            │
/// └───────────────────────────────────────────────────────────────────┘
/// ```
///
/// ## Reference
///
/// See `SqpkTargetInfo.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 SqpkTargetInfo {
    /// Target platform identifier.
    ///
    /// | Value | Platform | Path suffix |
    /// |-------|----------|-------------|
    /// | `0` | Windows / PC | `win32` |
    /// | `1` | `PlayStation` 3 | `ps3` |
    /// | `2` | `PlayStation` 4 | `ps4` |
    /// | other | Unknown | falls back to `win32` |
    ///
    /// Preceded by 3 bytes of reserved/padding in the wire format. Encoded as
    /// a big-endian `u16`.
    #[br(pad_before = 3)]
    pub platform_id: u16,
    /// Region code for the patch target.
    ///
    /// `-1` means Global (the standard international release). Other values
    /// are region-specific builds (e.g. Chinese or Korean clients). Encoded as
    /// a big-endian `i16`.
    pub region: i16,
    /// `true` when the patch targets a debug build of the game client.
    ///
    /// Parsed from a big-endian `i16`: zero → `false`, any nonzero → `true`.
    /// Debug patches are not distributed through normal update channels.
    #[br(map = |x: i16| x != 0)]
    pub is_debug: bool,
    /// Target client version number. Informational; not used for path resolution.
    ///
    /// Encoded as a big-endian `u16`.
    pub version: u16,
    /// Total bytes freed (deleted) across the entire patch.
    ///
    /// Used by patch manager UIs for progress estimation. **Little-endian**
    /// despite the struct-level `#[br(big)]` default — see the wire-format
    /// note above.
    #[br(little)]
    pub deleted_data_size: u64,
    /// Total number of seek operations the patcher is expected to perform.
    ///
    /// Used by patch manager UIs for progress estimation. **Little-endian**
    /// despite the struct-level `#[br(big)]` default — see the wire-format
    /// note above.
    #[br(little)]
    pub seek_count: u64,
    // The wire body contains 32 + 64 bytes of trailing zeros after seek_count.
    // These are bounded by the body slice passed to the parser so no pad_after is needed.
}

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