Skip to content

Package Format

clem-gb edited this page Oct 8, 2026 · 1 revision

Package Format

This page describes every layer the tool reads, outermost first, with the source file that handles it. FIH fields are little-endian; CNT fields are big-endian.

game.pkg
├─ FIH header (\x7FFIH)                             src/fih.rs
├─ CNT container (\x7FCNT): sce_sys metadata         src/fih.rs
└─ Outer PFS segment                                  src/pfs.rs
   ├─ naps_pkg_layout.dat: logical → stored map       src/naps.rs
   └─ pfs_image.dat: 256 KiB blocks, raw or Kraken    src/inner.rs, src/kraken.rs
      └─ Inner PFS (flat mode 0x10)
         └─ uroot/: the game tree
            ├─ eboot.bin, sce_module/*.prx (SELF)     src/selfelf.rs
            └─ every other game file

FIH header

  • Magic \x7FFIH at offset 0. A file starting with \x7FCNT instead is a PS4-style package and is refused.
  • Byte 5 is the signed flag: 0x00 for a debug or fake-signed image, 0x80 for retail. Anything other than 0x00 is refused.
  • 0x06 format version (u16), 0x10 PFS offset, 0x18 PFS size, 0x20 superblock offset, 0x58 CNT offset (all u64).

CNT container

A big-endian entry table (32 bytes per entry: id, name offset, flags, offset, size). Entry 0x0200 holds the name table. Entries with the top flag bit set are encrypted (licenses, keys) and are skipped. Every other entry is written to app0/sce_sys/ under its name from the table, or under a well-known name for its id (for example 0x2000 → param.json, 0x1200 → icon0.png). Names that could leave sce_sys/ are refused.

Outer PFS

  • The superblock (version 1 or 2, magic 20130315) sits where the FIH points for data-first images, or at the start of the segment.
  • The only readable seed is PPRPLAIN-NOAUTH!. Any other seed means an encrypted filesystem.
  • Signed inodes are 0x2C8 bytes with a 32-byte hash before every block pointer. The reader also handles the unsigned and 64-bit inode layouts.
  • Files use 12 direct block pointers and up to triple-indirect blocks. Block b is at pfs_offset + b × block_size (0x10000 in every package seen so far).
  • The tree holds two files that matter: pfs_image.dat and naps_pkg_layout.dat.

NAPS layout (naps_pkg_layout.dat)

In order:

  1. A 16-byte packed header with the counts.
  2. Outer-block digests (8 bytes each) and shuffle patterns (8 bytes each).
  3. File-offset entries (6 bytes: a 5-byte logical offset and a kind byte). Kind 0x40 marks the mount size (the last one) and sparse regions.
  4. u2c entries (10 bytes each).
  5. 9-byte CblockInfo records, one per 256 KiB logical block, giving where its stored bytes are, whether they are Kraken-compressed, the codec flags and the first chunk's compressed length.

The u2c table does not always start at the same place: some packages pad the file-offset table to 16 bytes, others don't. The tool tries both placements and keeps the only one whose blocks cover the image exactly. If both would work, it refuses rather than guessing.

Inner image (pfs_image.dat)

  • 256 KiB logical blocks, each stored raw, Kraken-compressed, or sparse (reads as zeros).
  • A block's stored length is where the next record starts.
  • The inner superblock, inodes and directories come after the last file in logical order. The tool decodes that tail and looks for a superblock (version 2) whose block count × block size equals the NAPS mount size.
  • Only the flat layout, mode 0x10, is known: each 0xA8-byte inode stores the byte address of its data at 0x60.
  • Files under uroot/ are the game tree.

Kraken

Kraken is the Oodle "newLZ" codec. In these images, a 256 KiB block is one or two 128 KiB chunks without the usual Oodle block headers; the NAPS record supplies the flags and the split point. The decoder (src/kraken.rs) is a port of PS5PCEM's Zig decoder, which follows LibProsperoPKG. It supports memcpy and Huffman (2- and 4-way) entropy blocks. tANS (type 1), RLE (type 3) and recursive (type 5) blocks are reported as unsupported.

SELF → ELF

  • A SELF has a 32-byte header (PS4 magic 4F 15 3D 1D or PS5 magic 54 14 F5 EE), a table of 32-byte segment entries, then the ELF header and program headers.
  • Each blocked entry carries the bytes of one program header; the header's index is in bits 20 to 31 of the entry properties.
  • The tool copies each segment to its program header's file offset and clears the section header fields, since the section table is not in the SELF.
  • The PT_SCE_VERSION segment, which sits after the last segment, is copied too.
  • Encrypted or compressed segments are refused.

Safety rules

Every size and offset read from a header is checked before it is used to index or allocate. Paths that could leave the output directory are refused, and a file being written when an error occurs is removed. See Development.

Clone this wiki locally