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.
$ cargo build --release
$ ./target/release/patapply --helpRequires Rust 1.88 or newer.
$ 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 fileA .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 UTCThat 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.
| 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 |
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.
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.
Work on a copy, since patching replaces the file in place:
$ cp game.iso game-en.iso
$ patapply translation.pat game-en.isoOr leave the original alone and name a separate output:
$ patapply translation.pat game.iso -o game-en.isoA few things specific to this workflow:
- Check the dump before anything else.
patapply listprints 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
.patat 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.
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 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.
.patis also the extension for IDA FLIRT signatures and several unrelated game-patching formats. Detect theVPATmagic rather than trusting the name.
$ cargo testThe 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.
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.