zipatch_rs/chunk/afsp.rs
1use binrw::BinRead;
2use std::io::Cursor;
3
4/// `APFS` chunk: free-space book-keeping; ignored at apply time.
5///
6/// This chunk was emitted by older versions of SE's patcher to signal how much
7/// free disk space the patch required. Modern patcher tooling (and all observed
8/// XIVARR+ patch files) never emit it. The `XIVLauncher` reference implementation
9/// reads the two fields and does nothing with them:
10///
11/// > "This is a NOP on recent patcher versions, so I don't think we'll be
12/// > seeing it."
13/// >
14/// > — `lib/FFXIVQuickLauncher/.../Chunk/ApplyFreeSpaceChunk.cs`
15///
16/// The fields are preserved in the parsed struct for completeness, but neither
17/// this crate nor the reference implementation acts on them.
18///
19/// # Wire format
20///
21/// ```text
22/// [unknown_a: i64 BE] [unknown_b: i64 BE]
23/// ```
24///
25/// Both fields are stored as signed 64-bit big-endian integers. No samples of
26/// this chunk in the wild have been found, so the exact semantics of the two
27/// values remain undocumented. The reference calls them `UnknownFieldA` and
28/// `UnknownFieldB`.
29///
30/// # Errors
31///
32/// Parsing fails with [`crate::ZiPatchError::BinrwError`] if the body is
33/// shorter than 16 bytes (the two `i64` fields).
34#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
35#[br(big)]
36pub struct ApplyFreeSpace {
37 /// First 8-byte signed integer field; purpose unknown.
38 ///
39 /// Read as a `u64` big-endian on the wire and reinterpreted as `i64` via
40 /// a bitcast (`as i64`), matching the C# `ReadInt64BE` call in the
41 /// reference.
42 #[br(map = |x: u64| x as i64)]
43 pub unknown_a: i64,
44 /// Second 8-byte signed integer field; purpose unknown.
45 ///
46 /// Same encoding as [`ApplyFreeSpace::unknown_a`].
47 #[br(map = |x: u64| x as i64)]
48 pub unknown_b: i64,
49}
50
51pub(crate) fn parse(body: &[u8]) -> crate::Result<ApplyFreeSpace> {
52 Ok(ApplyFreeSpace::read_be(&mut Cursor::new(body))?)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::parse;
58
59 #[test]
60 fn parses_apply_free_space() {
61 let mut body = Vec::new();
62 body.extend_from_slice(&0x8000_0000_0000_0000_u64.to_be_bytes()); // high bit → i64::MIN
63 body.extend_from_slice(&1u64.to_be_bytes());
64 let cmd = parse(&body).unwrap();
65 assert_eq!(cmd.unknown_a, i64::MIN);
66 assert_eq!(cmd.unknown_b, 1);
67 }
68
69 #[test]
70 fn rejects_truncated_body() {
71 // 16-byte body needed (two u64); supply only 8 to trigger the `?` error arm.
72 assert!(parse(&[0u8; 8]).is_err());
73 }
74}