Skip to content

Output Formats

asdfmonster261 edited this page May 3, 2026 · 3 revisions

Output Formats

Update Patch exe

[ stub EXE bytes                ]
[ patch data                    ]  ← see "patch data" below
[ extra file 0 bytes            ]  \
[ extra file 1 bytes            ]   > zero or more bundled files
[ ...                           ]  /
[ backdrop image bytes          ]  (zero bytes if none)
[ JSON metadata (UTF-8)         ]
[ metadata length   4 bytes LE  ]
[ magic "XPATCH01"  8 bytes     ]  ← end of file

Patch data

The shape of the patch data block depends on the engine:

  • HDiffPatch uses its native upstream container directly — the bytes are whatever hdiffz produced (single-file diff or TDirPatcher directory diff). The applier in hdiffpatch_stub.c hands the buffer straight to upstream HDiffPatch APIs.
  • JojoDiff wraps its per-file engine patches in a PFMD container for directory-mode patches (the dominant case). Single-file mode passes the raw engine output through unchanged.

PFMD container (JojoDiff directory mode)

[ magic "PFMD"        4 bytes ]
[ version = 2         1 byte  ]
[ num_entries         4 bytes LE uint32 ]
  Per entry:
    [ op              1 byte  ]   0=delete, 1=patch, 2=new-file
    [ path_len        2 bytes LE uint16 ]
    [ path            path_len bytes UTF-8, forward slashes, no leading slash ]
    [ data_len        8 bytes LE uint64 ]   0 for op=delete
    [ data            data_len bytes ]
                                        op=patch: engine patch bytes for this file
                                        op=new-file: raw target file contents
                                        op=delete: omitted (data_len = 0)

The C parser is in stub/dir_patch_format.h; it refuses anything other than version == 2. The Python writer is in src/core/engines/dir_format.py. Path components are validated against .. / drive letters / UNC prefixes by every callback site that consumes a PFMD record (see [security review 2026-04-25, item S-C1]).

Version 2 widened data_len from uint32 to uint64 so OP_NEW entries for files larger than 4 GB don't overflow. There is no version 1 in the wild.


Repack (XPACK01) exe

[ installer_stub EXE bytes      ]
[ XPACK01 blob                  ]  ← see format below (absent if split-bin)
[ backdrop image bytes          ]  (zero bytes if none)
[ JSON metadata (UTF-8)         ]
[ metadata length   4 bytes LE  ]
[ magic "XPACK01\0" 8 bytes     ]  ← end of file

When the base game archive exceeds 3.5 GB (or --split-bin is set), the XPACK01 blob is written to <name>_base_game.bin instead of being embedded in the exe. External component sidecars are always separate files named after the component group or label (e.g. Crack.bin). All sidecar files must be distributed alongside the exe.

XPACK01 blob layout

[ 4B LE: num_files              ]
  Per file:
    [ 2B LE: path_len           ]
    [ path_len bytes: UTF-8 path (forward slashes) ]
    [ 8B LE: offset within stream ]
    [ 8B LE: uncompressed size  ]
    [ 4B LE: component_index    ]  0 = base game; 1..N = optional components
    [ 4B LE: CRC32              ]  IEEE 802.3 checksum of uncompressed file data
[ 4B LE: num_streams            ]
  Per stream:
    [ 4B LE: component_index    ]
    [ 8B LE: compressed size    ]  0 = stream is in an external sidecar .bin file
    [ N bytes: XZ/LZMA2 or Zstandard stream  ]  (omitted when csize = 0)

Each optional component has its own compressed stream. The installer decompresses only the streams corresponding to the user's selections. File offsets are relative to the start of their own stream's decompressed data. The codec (lzma or zstd) is recorded in the JSON metadata, along with external_components, external_offsets, and external_csizes maps for any sidecar streams.

The stub reads backwards from the end of its own file to locate and parse the embedded data. End-users just double-click the .exe — no installer or runtime required.

Clone this wiki locally