zipatch-rs 1.5.0

Parser for FFXIV ZiPatch patch files
Documentation
use binrw::{BinRead, BinResult, Endian};

/// Which apply-time flag an `APLY` chunk toggles.
///
/// The discriminant values (1 and 2) are fixed by the wire format and match the
/// C# `ApplyOptionKind` enum in
/// `lib/FFXIVQuickLauncher/.../Chunk/ApplyOptionChunk.cs`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApplyOptionKind {
    /// Wire value `1`. When set to `true`, missing files and directories
    /// encountered during apply are silently skipped rather than causing an
    /// error. Corresponds to [`crate::ApplyContext::ignore_missing`].
    IgnoreMissing,
    /// Wire value `2`. When set to `true`, mismatches between the patch's
    /// expected "old" file content and the actual on-disk content are
    /// silently ignored. Corresponds to
    /// [`crate::ApplyContext::ignore_old_mismatch`].
    IgnoreOldMismatch,
}

/// `binrw` parse helper: read a `u32 BE` and map it to an [`ApplyOptionKind`].
///
/// Returns a `binrw::Error::Custom` for any value other than `1` or `2`.
fn read_apply_option_kind<R: std::io::Read + std::io::Seek>(
    reader: &mut R,
    endian: Endian,
    (): (),
) -> BinResult<ApplyOptionKind> {
    let raw = <u32 as BinRead>::read_options(reader, endian, ())?;
    match raw {
        1 => Ok(ApplyOptionKind::IgnoreMissing),
        2 => Ok(ApplyOptionKind::IgnoreOldMismatch),
        _ => Err(binrw::Error::Custom {
            pos: 0,
            err: Box::new(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "unknown ApplyOption kind",
            )),
        }),
    }
}

/// `APLY` chunk: sets or clears a boolean flag on the [`crate::ApplyContext`].
///
/// Each `APLY` chunk carries one flag selector ([`ApplyOptionKind`]) and one
/// boolean value. When applied, the corresponding field on [`crate::ApplyContext`]
/// is updated. In all FFXIV patch files observed by the `XIVLauncher` project,
/// both flags are set to `false` — the chunk exists to allow SE's tooling to
/// override the defaults without hard-coding them.
///
/// See `lib/FFXIVQuickLauncher/.../Chunk/ApplyOptionChunk.cs`.
///
/// # Wire format
///
/// ```text
/// [kind: u32 BE] [padding: 4 bytes] [value: u32 BE, non-zero = true]
/// ```
///
/// The 4-byte padding between `kind` and `value` is always `0x00000004` in
/// observed files, but the reference implementation discards it without
/// checking.
///
/// # Errors
///
/// Parsing fails with [`crate::ZiPatchError::BinrwError`] if:
/// - the `kind` field is not `1` or `2` (unrecognised option), or
/// - the body is too short to contain all three 4-byte fields.
#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
#[br(big)]
pub struct ApplyOption {
    /// Which [`crate::ApplyContext`] flag this chunk targets.
    ///
    /// Encoded as a `u32` big-endian on the wire; mapped to [`ApplyOptionKind`]
    /// during parsing.
    #[br(parse_with = read_apply_option_kind)]
    pub kind: ApplyOptionKind,
    /// New value for the targeted flag.
    ///
    /// On the wire: a `u32 BE` preceded by 4 bytes of padding (`pad_before = 4`).
    /// Any non-zero value is treated as `true`.
    #[br(pad_before = 4, map = |x: u32| x != 0)]
    pub value: bool,
}

pub(crate) fn parse(body: &[u8]) -> crate::Result<ApplyOption> {
    super::util::parse_be(body)
}

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

    fn make_body(kind: u32, value: u32) -> Vec<u8> {
        let mut body = Vec::new();
        body.extend_from_slice(&kind.to_be_bytes());
        body.extend_from_slice(&[0u8; 4]); // padding
        body.extend_from_slice(&value.to_be_bytes());
        body
    }

    #[test]
    fn parses_apply_option_ignore_missing() {
        let cmd = parse(&make_body(1, 0)).unwrap();
        assert_eq!(cmd.kind, ApplyOptionKind::IgnoreMissing);
        assert!(!cmd.value);
    }

    #[test]
    fn parses_apply_option_ignore_old_mismatch() {
        let cmd = parse(&make_body(2, 1)).unwrap();
        assert_eq!(cmd.kind, ApplyOptionKind::IgnoreOldMismatch);
        assert!(cmd.value);
    }

    #[test]
    fn rejects_invalid_apply_option_kind() {
        assert!(parse(&make_body(0, 0)).is_err());
        assert!(parse(&make_body(3, 0)).is_err());
    }
}