Skip to main content

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}