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}