zipatch-rs 1.0.2

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

/// `APFS` chunk: free-space book-keeping; ignored at apply time.
///
/// This chunk was emitted by older versions of SE's patcher to signal how much
/// free disk space the patch required. Modern patcher tooling (and all observed
/// XIVARR+ patch files) never emit it. The `XIVLauncher` reference implementation
/// reads the two fields and does nothing with them:
///
/// > "This is a NOP on recent patcher versions, so I don't think we'll be
/// > seeing it."
/// >
/// > — `lib/FFXIVQuickLauncher/.../Chunk/ApplyFreeSpaceChunk.cs`
///
/// The fields are preserved in the parsed struct for completeness, but neither
/// this crate nor the reference implementation acts on them.
///
/// # Wire format
///
/// ```text
/// [unknown_a: i64 BE] [unknown_b: i64 BE]
/// ```
///
/// Both fields are stored as signed 64-bit big-endian integers. No samples of
/// this chunk in the wild have been found, so the exact semantics of the two
/// values remain undocumented. The reference calls them `UnknownFieldA` and
/// `UnknownFieldB`.
///
/// # Errors
///
/// Parsing fails with [`crate::ZiPatchError::BinrwError`] if the body is
/// shorter than 16 bytes (the two `i64` fields).
#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
#[br(big)]
pub struct ApplyFreeSpace {
    /// First 8-byte signed integer field; purpose unknown.
    ///
    /// Read as a `u64` big-endian on the wire and reinterpreted as `i64` via
    /// a bitcast (`as i64`), matching the C# `ReadInt64BE` call in the
    /// reference.
    #[br(map = |x: u64| x as i64)]
    pub unknown_a: i64,
    /// Second 8-byte signed integer field; purpose unknown.
    ///
    /// Same encoding as [`ApplyFreeSpace::unknown_a`].
    #[br(map = |x: u64| x as i64)]
    pub unknown_b: i64,
}

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

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

    #[test]
    fn parses_apply_free_space() {
        let mut body = Vec::new();
        body.extend_from_slice(&0x8000000000000000u64.to_be_bytes()); // high bit → i64::MIN
        body.extend_from_slice(&1u64.to_be_bytes());
        let cmd = parse(&body).unwrap();
        assert_eq!(cmd.unknown_a, i64::MIN);
        assert_eq!(cmd.unknown_b, 1);
    }

    #[test]
    fn rejects_truncated_body() {
        // 16-byte body needed (two u64); supply only 8 to trigger the `?` error arm.
        assert!(parse(&[0u8; 8]).is_err());
    }
}