Skip to content
SHADOWDANCHPublic

About

Apply VPatch .pat binary patches on macOS, Linux, and Windows — zero-dependency Rust CLI and library, including a written specification of the .pat format.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

patapply

Apply VPatch .pat binary patches, from the command line or as a Rust library.

VPatch is a delta-patching tool by Koen van de Sande, bundled with the NSIS installer system and widely used to ship small updates for Windows software. Its .pat files are usually applied by a Windows-only runtime. This is an independent implementation of the applying half, so those patches can be used anywhere Rust builds.

Generating patches is not implemented — that is GenPat's job.

The case this was built for is patching console game images: applying fan localization patches to PSP ISO dumps and the like. Those patches ship as .pat files because a patch holds only the difference between the original dump and the translated one — it carries no game data itself, so you supply your own dump. See Patching game images.

  • No dependencies. Pure std, including MD5 and CRC32.
  • Streams: only the patch file is held in memory, so multi-gigabyte targets are fine.
  • Atomic: a failed or interrupted patch never leaves a half-written binary in place.
  • Verifies the result against the checksum the patch promises, before committing it.

Install

$ cargo build --release
$ ./target/release/patapply --help

Requires Rust 1.88 or newer.

Usage

$ patapply patch.pat firmware.bin              # patch in place
$ patapply patch.pat firmware.bin -o out.bin   # write elsewhere, leave input alone
$ patapply patch.pat firmware.bin --dry-run    # apply and verify, write nothing
$ patapply list patch.pat                      # inspect a patch file

A .pat file holds patches for one or more source files, each keyed by a checksum of the file it upgrades. There are no filenames inside, so the right patch is selected automatically from the contents of the file you name:

$ patapply list tests/fixtures/patch.pat
tests/fixtures/patch.pat
  checksums: MD5
  patches:   1

  [0] dcfb2bd10556fc8dd280afa67d6a146a -> de333d4b27b86c4cdf8e7ffa0405c368
      3 blocks, 51 byte body
      result: 125 bytes (91 reused, 34 new)
      target mtime: 2003-05-21 11:24:14 UTC

That is the example patch shipped with VPatch itself. Records list what a patch reuses from the source against what it has to carry, which is the useful view when sizing an update.

Options

Option Effect
-o, --output <path> Write the result here instead of replacing the input
-n, --dry-run Apply and verify without writing anything
--no-mtime Do not restore the modification time carried in the patch
--progress / --no-progress Force the progress display on or off
-q, --quiet Only report errors

Progress

Long patches show progress on stderr, in two stages — identifying the file means reading all of it, which is a real share of the work:

checksumming 100% [############################]  60.0 MiB / 60.0 MiB
patching      52% [###############             ]  33.4 MiB / 63.0 MiB

The output size is computed from the patch before writing starts, so the percentage is exact rather than an estimate. It appears only on a terminal and only when there is at least 8 MiB to move; --progress forces it on. Since it goes to stderr, redirecting stdout still gives clean output.

Exit codes

These match the original VPatch runtime, so scripts written against it port over unchanged.

Code Meaning
0 Success
1 The patch applied but the result failed verification
2 The patch file is corrupt
3 No patch in the file matches this input
4 The file is already up to date
5 The patch file could not be read
6 The input file could not be read
7 The output could not be written

"Already up to date" is reported only when nothing matched: a patch file may legitimately contain both an upgrade from a version and one to it.

Patching game images

Work on a copy, since patching replaces the file in place:

$ cp game.iso game-en.iso
$ patapply translation.pat game-en.iso

Or leave the original alone and name a separate output:

$ patapply translation.pat game.iso -o game-en.iso

A few things specific to this workflow:

  • Check the dump before anything else. patapply list prints the source checksum each patch expects, so you can compare it against your own file. Exit code 3 (no matching patch) nearly always means your dump is a different region, revision, or rip — not that the patch is broken. Code 4 means the file has already been patched.
  • Decompress first. Patches are made against a plain ISO. Convert a CSO or ZSO back to ISO before patching, and recompress afterwards if you want to.
  • Nothing is silently half-applied. The result is checksummed against the value the patch promises and written to a temporary file first, so a wrong dump or an interrupted run leaves your image untouched rather than subtly corrupted.
  • Size. A UMD image tops out around 1.8 GB, comfortably in range. The format itself caps at 4 GB because its offsets are 32-bit, so images beyond that — dual-layer DVD, Wii — cannot be expressed as a .pat at all; that is a limit of the format, not of this tool. The original Windows runtime seeks with signed arithmetic and gets unreliable above 2 GB, while patapply seeks with unsigned 64-bit offsets and addresses the full 32-bit range.
  • Only the patch file is held in memory. The image itself is streamed, and a patch of a few hundred MB against a multi-gigabyte ISO is unremarkable.

Library

use std::fs::File;
use patapply::{apply, checksum_stream, select_record, PatchFile};

# fn main() -> Result<(), Box<dyn std::error::Error>> {
let patch = PatchFile::parse(std::fs::read("tests/fixtures/patch.pat")?)?;
let mut source = File::open("tests/fixtures/oldfile.txt")?;

// Patches are keyed by checksum, so the file identifies its own patch.
let checksum = checksum_stream(patch.md5_mode, &mut source)?;
let index = select_record(&patch, &checksum)?;

let mut patched = Vec::new();
let applied = apply(&patch, index, &mut source, &mut patched)?;

assert_eq!(applied.bytes_written, 125);
# Ok(())
# }

apply_with_progress and checksum_stream_with_progress take an Option<&mut dyn FnMut(u64, u64)> if you want to report progress yourself. PatchFile::summarize measures a record — output size, how much is reused versus carried, and the timestamp — without touching any file data.

The .pat format

The format has no public specification; this is what the reference implementation does. All integers are little-endian.

A .pat file is a container of independent patch records:

[ optional prefix, e.g. a host .exe ]
"VPAT"                4 bytes
u32 count_and_flags   bit 31 = MD5 mode (else CRC32); count = value & 0x00FFFFFF
patch record * count
[ optional u32 at EOF = absolute offset of the "VPAT" magic ]

The header is normally at offset 0. When a patch is appended to a host executable to make a self-contained patcher, the last four bytes of the file point at it instead; both placements are supported.

Each record is:

Field Size Notes
num_blocks u32 Blocks in this record's stream
source 16 or 4 bytes MD5 digest, or CRC32 — per the mode flag
target 16 or 4 bytes Checksum of the patched result
body_size u32 Length of the block stream, so non-matching records can be skipped
body body_size The blocks

Each block is a type byte followed by its payload:

Type Payload Meaning
1 / 2 / 3 len as u8 / u16 / u32, then u32 offset Copy len bytes from the source file at offset
5 / 6 / 7 len as u8 / u16 / u32, then len bytes Copy literal bytes carried in the patch
255 u32 low, u32 high Windows FILETIME to stamp on the result

Three width classes keep short lengths cheap; the generator always picks the narrowest that fits. Type 4 was never assigned. A zero length is invalid. Copies always reference the source file, never already-written output, so applying is a single forward pass with seeks into the source.

Notes for anyone else implementing this:

  • CRC32 is the standard zlib one (polynomial 0xEDB88320, pre- and post-inverted). MD5 has been the default since VPatch 3.0 (2005), but the mode is per-file, so both are needed to read patches in the wild.
  • Offsets and sizes are all u32, capping the format at 4 GB; the original runtime uses signed arithmetic, so 2 GB is its safe practical ceiling. This is what puts disc images larger than 4 GB out of reach of the format.
  • .pat is also the extension for IDA FLIRT signatures and several unrelated game-patching formats. Detect the VPAT magic rather than trusting the name.

Tests

$ cargo test

The suite applies the patch file that ships with VPatch itself and checks the result byte-for-byte, alongside hand-built patches covering CRC32 mode, appended headers, multi-patch selection, every block width, and the corruption cases. MD5 is checked against the RFC 1321 vectors.

License

Zlib, matching the original VPatch. This is an independent implementation written from the format's behavior; the original VPatch is copyright Koen van de Sande / Van de Sande Productions.

About

Apply VPatch .pat binary patches on macOS, Linux, and Windows — zero-dependency Rust CLI and library, including a written specification of the .pat format.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages