Skip to main content

orbis_pkg/
header.rs

1use std::fmt;
2
3use zerocopy::{
4    FromBytes, Immutable, IntoBytes, KnownLayout, TryFromBytes, Unaligned,
5    byteorder::big_endian::{U16, U32, U64},
6};
7
8/// Errors when reading a PKG header.
9#[derive(Debug, snafu::Snafu)]
10#[non_exhaustive]
11pub enum ReadError {
12    #[snafu(display("PKG file is too small"))]
13    TooSmall,
14    #[snafu(display("invalid PKG magic"))]
15    InvalidMagic,
16
17    #[snafu(display("invalid source bytes"))]
18    InvalidSourceBytes,
19}
20
21type Result<T, E = ReadError> = std::result::Result<T, E>;
22
23const PKG_MAGIC: u32 = 0x7F434E54;
24
25#[derive(Debug, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
26#[repr(C)]
27pub struct PkgHeaderRaw {
28    // Main header fields
29    pub pkg_magic: U32,            // 0x000 - 0x7F434E54
30    pub pkg_type: U32,             // 0x004
31    pub pkg_0x008: U32,            // 0x008 - unknown field
32    pub pkg_file_count: U32,       // 0x00C
33    pub pkg_entry_count: U32,      // 0x010
34    pub pkg_sc_entry_count: U16,   // 0x014
35    pub pkg_entry_count_2: U16,    // 0x016 - same as pkg_entry_count
36    pub pkg_table_offset: U32,     // 0x018 - file table offset
37    pub pkg_entry_data_size: U32,  // 0x01C
38    pub pkg_body_offset: U64,      // 0x020 - offset of PKG entries
39    pub pkg_body_size: U64,        // 0x028 - length of all PKG entries
40    pub pkg_content_offset: U64,   // 0x030
41    pub pkg_content_size: U64,     // 0x038
42    pub pkg_content_id: ContentId, // 0x040 - packages' content ID (36 bytes)
43    pub pkg_padding: [u8; 0xC],    // 0x064 - padding
44    pub pkg_drm_type: U32,         // 0x070 - DRM type
45    pub pkg_content_type: U32,     // 0x074 - Content type
46    pub pkg_content_flags: U32,    // 0x078 - Content flags
47    pub pkg_promote_size: U32,     // 0x07C
48    pub pkg_version_date: U32,     // 0x080
49    pub pkg_version_hash: U32,     // 0x084
50    pub pkg_0x088: U32,            // 0x088
51    pub pkg_0x08c: U32,            // 0x08C
52    pub pkg_0x090: U32,            // 0x090
53    pub pkg_0x094: U32,            // 0x094
54    pub pkg_iro_tag: U32,          // 0x098
55    pub pkg_drm_type_version: U32, // 0x09C
56
57    // Padding between header and digest table (0x0A0 - 0x100)
58    pub padding_0x0a0: [u8; 0x60],
59
60    // Digest table (0x100 - 0x180)
61    pub digest_table: DigestTable,
62
63    // Padding between digest table and PFS info (0x180 - 0x404)
64    pub padding_0x180: [u8; 0x284],
65
66    // PFS image info
67    pub pfs_image_count: U32,          // 0x404 - count of PFS images
68    pub pfs_image_flags: U64,          // 0x408 - PFS flags
69    pub pfs_image_offset: U64,         // 0x410 - offset to start of external PFS image
70    pub pfs_image_size: U64,           // 0x418 - size of external PFS image
71    pub mount_image_offset: U64,       // 0x420
72    pub mount_image_size: U64,         // 0x428
73    pub pkg_size: U64,                 // 0x430
74    pub pfs_signed_size: U32,          // 0x438
75    pub pfs_cache_size: U32,           // 0x43C
76    pub pfs_image_digest: [u8; 0x20],  // 0x440
77    pub pfs_signed_digest: [u8; 0x20], // 0x460
78    pub pfs_split_size_nth_0: U64,     // 0x480
79    pub pfs_split_size_nth_1: U64,     // 0x488
80
81    // Padding between PFS info and final digest (0x490 - 0xFE0)
82    pub padding_0x490: [u8; 0xB50],
83
84    // Final digest
85    pub pkg_digest: [u8; 0x20], // 0xFE0
86                                // 0x1000 - end of header
87}
88
89/// Content ID structure (36 bytes).
90///
91/// Format: `<service_id><region>-<title_id>_<version>-<label>`
92/// Example: `UP0102-CUSA03173_00-PSYCHONAUTS1PS40`
93#[derive(
94    Clone,
95    Copy,
96    Default,
97    PartialEq,
98    Eq,
99    PartialOrd,
100    Ord,
101    Hash,
102    FromBytes,
103    IntoBytes,
104    KnownLayout,
105    Immutable,
106    Unaligned,
107)]
108#[repr(C)]
109pub struct ContentId {
110    /// Service ID (2 bytes): "UP", "EP", "JP", "HP", "IP", etc.
111    service_id: [u8; 2],
112    /// Publisher/region code (4 bytes): e.g., "0102"
113    publisher_code: [u8; 4],
114    /// Separator (1 byte): "-"
115    _sep1: u8,
116    /// Title ID (9 bytes): e.g., "CUSA03173", "PPSA01234"
117    title_id: [u8; 9],
118    /// Separator (1 byte): "_"
119    _sep2: u8,
120    /// Content version (2 bytes): e.g., "00"
121    version: [u8; 2],
122    /// Separator (1 byte): "-"
123    _sep3: u8,
124    /// Content label (16 bytes): e.g., "PSYCHONAUTS1PS40"
125    label: [u8; 16],
126}
127
128impl ContentId {
129    /// Returns the service ID (e.g., "UP", "EP", "JP").
130    #[must_use]
131    pub fn service_id(&self) -> &str {
132        std::str::from_utf8(&self.service_id).unwrap_or("")
133    }
134
135    /// Returns the publisher/region code (e.g., "0102").
136    #[must_use]
137    pub fn publisher_code(&self) -> &str {
138        std::str::from_utf8(&self.publisher_code).unwrap_or("")
139    }
140
141    /// Returns the title ID (e.g., "CUSA03173").
142    #[must_use]
143    pub fn title_id(&self) -> &str {
144        let bytes = &self.title_id;
145        let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
146        std::str::from_utf8(&bytes[..len]).unwrap_or("")
147    }
148
149    /// Returns the content version (e.g., "00").
150    #[must_use]
151    pub fn version(&self) -> &str {
152        std::str::from_utf8(&self.version).unwrap_or("")
153    }
154
155    /// Returns the content label (e.g., "PSYCHONAUTS1PS40").
156    #[must_use]
157    pub fn label(&self) -> &str {
158        let bytes = &self.label;
159        let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
160        std::str::from_utf8(&bytes[..len]).unwrap_or("")
161    }
162
163    /// Returns the full content ID as a string slice.
164    #[must_use]
165    pub fn as_str(&self) -> &str {
166        let bytes = self.as_bytes();
167        let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
168        std::str::from_utf8(&bytes[..len]).unwrap_or("<invalid>")
169    }
170}
171
172impl fmt::Display for ContentId {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        write!(f, "{}", self.as_str())
175    }
176}
177
178impl fmt::Debug for ContentId {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.debug_struct("ContentId")
181            .field("service_id", &self.service_id())
182            .field("publisher_code", &self.publisher_code())
183            .field("title_id", &self.title_id())
184            .field("version", &self.version())
185            .field("label", &self.label())
186            .finish()
187    }
188}
189
190#[derive(
191    Clone,
192    Copy,
193    Debug,
194    Default,
195    PartialEq,
196    Eq,
197    PartialOrd,
198    Ord,
199    Hash,
200    FromBytes,
201    IntoBytes,
202    KnownLayout,
203    Immutable,
204)]
205#[repr(C)]
206pub struct ContentFlags(u32);
207
208bitflags::bitflags! {
209    impl ContentFlags: u32 {
210        const FIRST_PATCH = 0x00100000;
211        const PATCHGO = 0x00200000;
212        const REMASTER = 0x00400000;
213        const PS_CLOUD = 0x00800000;
214        const DELTA_PATCH_X = 0x01000000;
215        const GD_AC = 0x02000000;
216        const NON_GAME = 0x04000000;
217        const UNKNOWN_1 = 0x08000000;
218        const UNKNOWN_2 = 0x10000000;
219        const CUMULATIVE_PATCH_X = 0x20000000;
220        const SUBSEQUENT_PATCH = 0x40000000;
221        const DELTA_PATCH = 0x41000000;
222        const CUMULATIVE_PATCH = 0x60000000;
223    }
224}
225
226impl fmt::Display for ContentFlags {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        if self.is_empty() {
229            return write!(f, "(none)");
230        }
231
232        let mut first = true;
233        let mut write_flag = |name: &str| -> fmt::Result {
234            if !first {
235                write!(f, ", ")?;
236            }
237            first = false;
238            write!(f, "{}", name)
239        };
240
241        // Check compound flags first (they include multiple bits)
242        if self.contains(Self::CUMULATIVE_PATCH) {
243            write_flag("Cumulative Patch")?;
244        } else if self.contains(Self::DELTA_PATCH) {
245            write_flag("Delta Patch")?;
246        } else {
247            // Check individual flags
248            if self.contains(Self::FIRST_PATCH) {
249                write_flag("First Patch")?;
250            }
251            if self.contains(Self::PATCHGO) {
252                write_flag("PatchGo")?;
253            }
254            if self.contains(Self::REMASTER) {
255                write_flag("Remaster")?;
256            }
257            if self.contains(Self::PS_CLOUD) {
258                write_flag("PS Cloud")?;
259            }
260            if self.contains(Self::DELTA_PATCH_X) {
261                write_flag("Delta Patch X")?;
262            }
263            if self.contains(Self::GD_AC) {
264                write_flag("GD/AC")?;
265            }
266            if self.contains(Self::NON_GAME) {
267                write_flag("Non-Game")?;
268            }
269            if self.contains(Self::UNKNOWN_1) {
270                write_flag("Unknown (0x08000000)")?;
271            }
272            if self.contains(Self::UNKNOWN_2) {
273                write_flag("Unknown (0x10000000)")?;
274            }
275            if self.contains(Self::CUMULATIVE_PATCH_X) {
276                write_flag("Cumulative Patch X")?;
277            }
278            if self.contains(Self::SUBSEQUENT_PATCH) {
279                write_flag("Subsequent Patch")?;
280            }
281        }
282
283        Ok(())
284    }
285}
286
287#[derive(Debug, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
288#[repr(C)]
289pub struct DigestTable {
290    pub digest_entries1: [u8; 0x20],
291    pub digest_entries2: [u8; 0x20],
292    pub digest_table_digest: [u8; 0x20],
293    pub digest_body_digest: [u8; 0x20],
294}
295
296/// Returns a human-readable name for a content type value.
297#[must_use]
298pub const fn content_type_name(content_type: u32) -> &'static str {
299    match content_type {
300        0x01 => "GD (Game Data)",
301        0x02 => "AC (Additional Content)",
302        0x03 => "AL (App License)",
303        0x04 => "DP (Delta Patch)",
304        0x05 => "DP (Cumulative Patch)", // sometimes same as 0x04
305        0x06 => "Remaster",
306        0x1A => "GD (Game Data)",
307        0x1B => "AC (Additional Content)",
308        _ => "Unknown",
309    }
310}
311
312/// Returns a human-readable name for a DRM type value.
313#[must_use]
314pub const fn drm_type_name(drm_type: u32) -> &'static str {
315    match drm_type {
316        0x0 => "None",
317        0x1 => "PS4",
318        0xD => "PS4 (Free)",
319        0xF => "PS4",
320        _ => "Unknown",
321    }
322}
323
324/// Parsed PKG header information.
325#[derive(Debug)]
326#[must_use]
327pub struct PkgHeader {
328    raw_header: PkgHeaderRaw,
329}
330
331impl PkgHeader {
332    /// Parses a PKG header from raw bytes.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if the data is too small or has an invalid magic number.
337    pub fn read(pkg: &[u8]) -> Result<Self, ReadError> {
338        // Check size first so we can read without checking bound.
339        snafu::ensure!(pkg.len() >= 0x1000, TooSmallSnafu);
340
341        let (raw_header, _) =
342            PkgHeaderRaw::try_read_from_prefix(pkg).map_err(|_| InvalidSourceBytesSnafu.build())?;
343
344        // Check magic.
345        snafu::ensure!(raw_header.pkg_magic.get() == PKG_MAGIC, InvalidMagicSnafu);
346
347        Ok(Self { raw_header })
348    }
349
350    /// Returns the number of entries in the PKG.
351    #[must_use]
352    pub const fn entry_count(&self) -> usize {
353        self.raw_header.pkg_entry_count.get() as _
354    }
355
356    /// Returns the offset to the entry table.
357    #[must_use]
358    pub const fn table_offset(&self) -> usize {
359        self.raw_header.pkg_table_offset.get() as _
360    }
361
362    /// Returns the offset to the PFS image.
363    #[must_use]
364    pub const fn pfs_offset(&self) -> usize {
365        self.raw_header.pfs_image_offset.get() as _
366    }
367
368    /// Returns the size of the PFS image.
369    #[must_use]
370    pub const fn pfs_size(&self) -> usize {
371        self.raw_header.pfs_image_size.get() as _
372    }
373
374    /// Returns the content ID.
375    #[must_use]
376    pub fn content_id(&self) -> &ContentId {
377        &self.raw_header.pkg_content_id
378    }
379
380    /// Returns the PKG type.
381    #[must_use]
382    pub const fn pkg_type(&self) -> u32 {
383        self.raw_header.pkg_type.get()
384    }
385
386    /// Returns the DRM type.
387    #[must_use]
388    pub const fn drm_type(&self) -> u32 {
389        self.raw_header.pkg_drm_type.get()
390    }
391
392    /// Returns the human-readable name for the DRM type.
393    #[must_use]
394    pub const fn drm_type_name(&self) -> &'static str {
395        drm_type_name(self.drm_type())
396    }
397
398    /// Returns the content type.
399    #[must_use]
400    pub const fn content_type(&self) -> u32 {
401        self.raw_header.pkg_content_type.get()
402    }
403
404    /// Returns the human-readable name for the content type.
405    #[must_use]
406    pub const fn content_type_name(&self) -> &'static str {
407        content_type_name(self.content_type())
408    }
409
410    /// Returns the content flags.
411    #[must_use]
412    pub const fn content_flags(&self) -> ContentFlags {
413        ContentFlags::from_bits_truncate(self.raw_header.pkg_content_flags.get())
414    }
415
416    /// Returns the total PKG file size.
417    #[must_use]
418    pub const fn pkg_size(&self) -> u64 {
419        self.raw_header.pkg_size.get()
420    }
421
422    /// Returns the file count.
423    #[must_use]
424    pub const fn file_count(&self) -> u32 {
425        self.raw_header.pkg_file_count.get()
426    }
427
428    /// Returns the raw header.
429    #[must_use]
430    pub const fn raw_header(&self) -> &PkgHeaderRaw {
431        &self.raw_header
432    }
433}