zipatch_rs/chunk/sqpk/target_info.rs
1use binrw::BinRead;
2use std::io::Cursor;
3
4/// SQPK `T` command body: declares the target platform, region, and patch
5/// statistics for all subsequent operations in a patch file.
6///
7/// A real FFXIV patch file begins with a `T` command before any write
8/// operations, so the platform declared here is in effect for all subsequent
9/// path resolution. The apply layer updates [`crate::apply::ApplyContext::platform`]
10/// from [`platform_id`](SqpkTargetInfo::platform_id) and otherwise ignores the
11/// remaining fields.
12///
13/// ## Wire format
14///
15/// **Important:** Most fields are big-endian (the struct-level `#[br(big)]`
16/// default), but [`deleted_data_size`](SqpkTargetInfo::deleted_data_size) and
17/// [`seek_count`](SqpkTargetInfo::seek_count) are **little-endian**. This
18/// endian anomaly is present in the original C# implementation
19/// (`SqpkTargetInfo.cs`) and must be preserved exactly.
20///
21/// ```text
22/// ┌───────────────────────────────────────────────────────────────────┐
23/// │ <reserved> : [u8; 3] (always zero) │ bytes 0–2
24/// │ platform_id : u16 BE 0=Win32, 1=PS3, 2=PS4 │ bytes 3–4
25/// │ region : i16 BE -1=Global │ bytes 5–6
26/// │ is_debug : i16 BE 0=release, nonzero=debug │ bytes 7–8
27/// │ version : u16 BE client version │ bytes 9–10
28/// │ deleted_data_size : u64 LE ← little-endian anomaly │ bytes 11–18
29/// │ seek_count : u64 LE ← little-endian anomaly │ bytes 19–26
30/// │ <trailing padding> bounded by the body slice │
31/// └───────────────────────────────────────────────────────────────────┘
32/// ```
33///
34/// ## Reference
35///
36/// See `SqpkTargetInfo.cs` in the `XIVLauncher` reference implementation.
37///
38/// # Errors
39///
40/// Parsing returns [`crate::ZiPatchError::BinrwError`] if the body is too
41/// short to contain all required fields.
42#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
43#[br(big)]
44pub struct SqpkTargetInfo {
45 /// Target platform identifier.
46 ///
47 /// | Value | Platform | Path suffix |
48 /// |-------|----------|-------------|
49 /// | `0` | Windows / PC | `win32` |
50 /// | `1` | `PlayStation` 3 | `ps3` |
51 /// | `2` | `PlayStation` 4 | `ps4` |
52 /// | other | Unknown | falls back to `win32` |
53 ///
54 /// Preceded by 3 bytes of reserved/padding in the wire format. Encoded as
55 /// a big-endian `u16`.
56 #[br(pad_before = 3)]
57 pub platform_id: u16,
58 /// Region code for the patch target.
59 ///
60 /// `-1` means Global (the standard international release). Other values
61 /// are region-specific builds (e.g. Chinese or Korean clients). Encoded as
62 /// a big-endian `i16`.
63 pub region: i16,
64 /// `true` when the patch targets a debug build of the game client.
65 ///
66 /// Parsed from a big-endian `i16`: zero → `false`, any nonzero → `true`.
67 /// Debug patches are not distributed through normal update channels.
68 #[br(map = |x: i16| x != 0)]
69 pub is_debug: bool,
70 /// Target client version number. Informational; not used for path resolution.
71 ///
72 /// Encoded as a big-endian `u16`.
73 pub version: u16,
74 /// Total bytes freed (deleted) across the entire patch.
75 ///
76 /// Used by patch manager UIs for progress estimation. **Little-endian**
77 /// despite the struct-level `#[br(big)]` default — see the wire-format
78 /// note above.
79 #[br(little)]
80 pub deleted_data_size: u64,
81 /// Total number of seek operations the patcher is expected to perform.
82 ///
83 /// Used by patch manager UIs for progress estimation. **Little-endian**
84 /// despite the struct-level `#[br(big)]` default — see the wire-format
85 /// note above.
86 #[br(little)]
87 pub seek_count: u64,
88 // The wire body contains 32 + 64 bytes of trailing zeros after seek_count.
89 // These are bounded by the body slice passed to the parser so no pad_after is needed.
90}
91
92pub(crate) fn parse(body: &[u8]) -> crate::Result<SqpkTargetInfo> {
93 Ok(SqpkTargetInfo::read_be(&mut Cursor::new(body))?)
94}