Skip to main content

zipatch_rs/chunk/
aply.rs

1use binrw::{BinRead, BinResult, Endian};
2
3/// Which apply-time flag an `APLY` chunk toggles.
4///
5/// The discriminant values (1 and 2) are fixed by the wire format and match the
6/// C# `ApplyOptionKind` enum in
7/// `lib/FFXIVQuickLauncher/.../Chunk/ApplyOptionChunk.cs`.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ApplyOptionKind {
10    /// Wire value `1`. When set to `true`, missing files and directories
11    /// encountered during apply are silently skipped rather than causing an
12    /// error. Corresponds to [`crate::ApplyContext::ignore_missing`].
13    IgnoreMissing,
14    /// Wire value `2`. When set to `true`, mismatches between the patch's
15    /// expected "old" file content and the actual on-disk content are
16    /// silently ignored. Corresponds to
17    /// [`crate::ApplyContext::ignore_old_mismatch`].
18    IgnoreOldMismatch,
19}
20
21/// `binrw` parse helper: read a `u32 BE` and map it to an [`ApplyOptionKind`].
22///
23/// Returns a `binrw::Error::Custom` for any value other than `1` or `2`.
24fn read_apply_option_kind<R: std::io::Read + std::io::Seek>(
25    reader: &mut R,
26    endian: Endian,
27    (): (),
28) -> BinResult<ApplyOptionKind> {
29    let raw = <u32 as BinRead>::read_options(reader, endian, ())?;
30    match raw {
31        1 => Ok(ApplyOptionKind::IgnoreMissing),
32        2 => Ok(ApplyOptionKind::IgnoreOldMismatch),
33        _ => Err(binrw::Error::Custom {
34            pos: 0,
35            err: Box::new(std::io::Error::new(
36                std::io::ErrorKind::InvalidData,
37                "unknown ApplyOption kind",
38            )),
39        }),
40    }
41}
42
43/// `APLY` chunk: sets or clears a boolean flag on the [`crate::ApplyContext`].
44///
45/// Each `APLY` chunk carries one flag selector ([`ApplyOptionKind`]) and one
46/// boolean value. When applied, the corresponding field on [`crate::ApplyContext`]
47/// is updated. In all FFXIV patch files observed by the `XIVLauncher` project,
48/// both flags are set to `false` — the chunk exists to allow SE's tooling to
49/// override the defaults without hard-coding them.
50///
51/// See `lib/FFXIVQuickLauncher/.../Chunk/ApplyOptionChunk.cs`.
52///
53/// # Wire format
54///
55/// ```text
56/// [kind: u32 BE] [padding: 4 bytes] [value: u32 BE, non-zero = true]
57/// ```
58///
59/// The 4-byte padding between `kind` and `value` is always `0x00000004` in
60/// observed files, but the reference implementation discards it without
61/// checking.
62///
63/// # Errors
64///
65/// Parsing fails with [`crate::ZiPatchError::BinrwError`] if:
66/// - the `kind` field is not `1` or `2` (unrecognised option), or
67/// - the body is too short to contain all three 4-byte fields.
68#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
69#[br(big)]
70pub struct ApplyOption {
71    /// Which [`crate::ApplyContext`] flag this chunk targets.
72    ///
73    /// Encoded as a `u32` big-endian on the wire; mapped to [`ApplyOptionKind`]
74    /// during parsing.
75    #[br(parse_with = read_apply_option_kind)]
76    pub kind: ApplyOptionKind,
77    /// New value for the targeted flag.
78    ///
79    /// On the wire: a `u32 BE` preceded by 4 bytes of padding (`pad_before = 4`).
80    /// Any non-zero value is treated as `true`.
81    #[br(pad_before = 4, map = |x: u32| x != 0)]
82    pub value: bool,
83}
84
85pub(crate) fn parse(body: &[u8]) -> crate::Result<ApplyOption> {
86    super::util::parse_be(body)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn make_body(kind: u32, value: u32) -> Vec<u8> {
94        let mut body = Vec::new();
95        body.extend_from_slice(&kind.to_be_bytes());
96        body.extend_from_slice(&[0u8; 4]); // padding
97        body.extend_from_slice(&value.to_be_bytes());
98        body
99    }
100
101    #[test]
102    fn parses_apply_option_ignore_missing() {
103        let cmd = parse(&make_body(1, 0)).unwrap();
104        assert_eq!(cmd.kind, ApplyOptionKind::IgnoreMissing);
105        assert!(!cmd.value);
106    }
107
108    #[test]
109    fn parses_apply_option_ignore_old_mismatch() {
110        let cmd = parse(&make_body(2, 1)).unwrap();
111        assert_eq!(cmd.kind, ApplyOptionKind::IgnoreOldMismatch);
112        assert!(cmd.value);
113    }
114
115    #[test]
116    fn rejects_invalid_apply_option_kind() {
117        assert!(parse(&make_body(0, 0)).is_err());
118        assert!(parse(&make_body(3, 0)).is_err());
119    }
120}