binary_patch 1.0.0
binary_patch: ^1.0.0 copied to clipboard
binary_patch is a Dart/Flutter library for creating and applying efficient binary diffs using the bsdiff algorithm, reducing update sizes with support for zstd, bzip2, and lzma compression.
binary_patch π§ #
Update your app with 1 MiB instead of 100 MiB. A high-performance, Pure Dart binary diff/patch library based on the bsdiff algorithm β the same algorithm used by macOS, Chrome, and major app stores for delta updates.
The Problem #
Every time you ship a new version of your app β even a one-line bug fix β your users must download the entire binary again. For a 100 MiB desktop app, that wastes bandwidth, time, and hosting cost on every update.
The Solution #
import 'package:binary_patch/binary_patch.dart';
// ββ Server side (CI/CD) ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
final result = await BinaryPatch.create(
oldFile: 'app_v1.exe', // 100 MiB
newFile: 'app_v2.exe', // 100 MiB
outputPatch: 'patch.bin', // ~1 MiB β users download only this!
options: PatchOptions.balanced(),
);
print('Savings: ${result.savingsPercent.toStringAsFixed(0)}%'); // "Savings: 99%"
// ββ Client side (user's device) ββββββββββββββββββββββββββββββββββββββββββββββ
await BinaryPatch.apply(
oldFile: 'app_v1.exe', // already on device
patchFile: 'patch.bin', // downloaded
outputFile: 'app_v2.exe', // reconstructed β bit-perfect match!
);
Features #
| Feature | Description |
|---|---|
| β‘ bsdiff algorithm | Best-in-class binary differencing, used by macOS & Chrome |
| ποΈ 3 compression modes | zstd (default), bzip2 (classic compat), lzma (max ratio) |
| π SHA-256 verification | Checksums embedded for old file, new file, and patch |
| π§΅ Non-blocking | All CPU work runs in Dart Isolates β UI thread never blocked |
| π± Flutter-ready | In-memory API (createBytes / applyBytes) for app storage |
| π₯οΈ CLI included | dart run binary_patch create/apply/verify/analyze/info |
| π― Pure Dart | No native dependencies, no FFI required |
| β Typed errors | ChecksumException, PatchFormatException, PatchException |
Installation #
# pubspec.yaml
dependencies:
binary_patch: ^1.0.0
dart pub get
Quick Start #
Create a patch (server/CI side) #
final result = await BinaryPatch.create(
oldFile: 'dist/app_v1.exe',
newFile: 'dist/app_v2.exe',
outputPatch: 'dist/patch_v1_to_v2.bin',
options: PatchOptions.balanced(), // zstd level 19
onProgress: (p) => print('${(p * 100).toInt()}%'),
);
print(result.summary);
Apply a patch (client device) #
await BinaryPatch.apply(
oldFile: 'app_v1.exe',
patchFile: 'patch_v1_to_v2.bin',
outputFile: 'app_v2.exe',
// verifyChecksum: true (default) β verifies SHA-256 before & after
);
Estimate savings before creating #
final analysis = await BinaryPatch.analyze(
oldFile: 'app_v1.exe',
newFile: 'app_v2.exe',
);
print('Patch will be ~${analysis.estimatedPatchMB.toStringAsFixed(2)} MiB '
'(saves ${analysis.estimatedSavingsPercent.toStringAsFixed(1)}%)');
In-memory API (Flutter) #
// Read files however you like (app documents, asset bundlesβ¦)
final oldBytes = await File(oldPath).readAsBytes();
final newBytes = await File(newPath).readAsBytes();
// Create patch in memory β no disk I/O
final patchBytes = await BinaryPatch.createBytes(
oldData: oldBytes,
newData: newBytes,
);
// Apply patch in memory
final restoredBytes = await BinaryPatch.applyBytes(
oldData: oldBytes,
patchData: patchBytes,
);
assert(restoredBytes.length == newBytes.length);
CLI Usage #
# Generate patch (typically run on CI)
dart run binary_patch create app_v1.exe app_v2.exe patch.bin
# Apply patch on client
dart run binary_patch apply app_v1.exe patch.bin app_v2.exe
# Fast header validation (no decompression)
dart run binary_patch verify patch.bin
# Dry-run estimate without writing a file
dart run binary_patch analyze app_v1.exe app_v2.exe --compression=lzma
# Read embedded metadata
dart run binary_patch info patch.bin
Compression Guide #
| Option | Algorithm | Speed | Ratio | Use when |
|---|---|---|---|---|
PatchOptions.fast() |
zstd L3 | β β β β β | β β β β | CI speed matters |
PatchOptions.balanced() |
zstd L19 | β β β β β | β β β β β | Default β recommended |
PatchOptions.maximum() |
lzma L9 | β β βββ | β β β β β | Minimise download size |
PatchOptions.uncompressed() |
none | β β β β β | β ββββ | Debug / inspection |
Custom:
PatchOptions(
compression: CompressionType.bzip2,
compressionLevel: 6,
label: '1.0.0 β 2.0.0',
)
Error Handling #
try {
await BinaryPatch.apply(/*β¦*/);
} on ChecksumException catch (e) {
// Wrong source file (different version than the patch expects)
print('Expected: ${e.expected}');
print('Actual: ${e.actual}');
} on PatchFormatException catch (e) {
// Corrupt or incompatible patch file
print('Bad patch at byte ${e.byteOffset}: ${e.message}');
} on PatchException catch (e) {
// Any other binary_patch error
print(e.message);
}
Patch File Format #
Offset Size Field
ββββββ ββββ βββββββββββββββββββββββββββββββββββββββββββ
0 6 Magic: ASCII "BPATCH"
6 1 Format version: 0x01
7 1 Compression type (0=zstd, 1=bzip2, 2=lzma, 3=none)
8 4 Metadata JSON length (uint32 LE)
12 N Metadata JSON (UTF-8, contains SHA-256, sizes, timestampβ¦)
12+N 8 Control section length (int64 LE)
20+N 8 Diff section length (int64 LE)
28+N 8 Extra section length (int64 LE)
36+N * Control data β compressed ControlBlock triples (24 B each)
* * Diff data β compressed byte-difference stream
* * Extra data β compressed verbatim-insert stream
Algorithm Overview #
The bsdiff algorithm (Percival, 2003) achieves its remarkable compression
ratio because recompiling even a trivial change causes many address relocations
throughout the binary. A naΓ―ve differ treats each relocated address as unique
new data β bsdiff instead subtracts old bytes from new bytes, turning
+4 address offsets into a stream of 0x04 values that compress to nothing.
Three phases:
1. Suffix Array β O(n logΒ² n) build from old file
O(log n) best-match search per position in new file
2. Match & Delta β diff[i] = (new[pos+i] β old[match+i]) mod 256
Bytes with no match β extra stream
3. Compress β zstd / bzip2 / lzma applied independently to
control stream, diff stream, extra stream
Apply (bspatch) is O(m):
for each ControlBlock(copyLen, extraLen, seekOffset):
newData[newPos..+copyLen] = oldData[oldPos..+copyLen] + diff[..+copyLen]
newData[newPos+copyLen..+extraLen] = extra[extraPos..+extraLen]
oldPos += seekOffset
Performance #
| File size | ~2% change | zstd L19 patch | Create time | Apply time |
|---|---|---|---|---|
| 16 KiB | 320 B | ~2 KiB | <50 ms | <5 ms |
| 1 MiB | ~20 KiB | ~10β30 KiB | ~200 ms | ~20 ms |
| 10 MiB | ~200 KiB | ~50β200 KiB | ~2 s | ~150 ms |
| 100 MiB | ~2 MiB | ~0.5β2 MiB | ~20β60 s | ~1.5 s |
Apply is always much faster than create β safe for low-power devices.
Comparison with Alternatives #
| Feature | binary_patch | diff_match_patch | bsdiff (C) |
|---|---|---|---|
| Target | Binary files | Text only | Binary |
| Language | Pure Dart | Dart | C |
| Flutter | β | β | β |
| Isolates | β | β | β |
| Multi-algo compression | β | β | bzip2 only |
| Built-in CLI | β | β | β |
| SHA-256 checksums | β | β | β |
| Savings (typical) | ~95% | text only | ~95% |
π License #
MIT β see LICENSE
Contributing #
Issues and pull requests are welcome at github.com/Brah-Timo/binary_patch.
Please run dart test and dart analyze before submitting.