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