Skip to main content

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}