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.
// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:binary_patch/binary_patch.dart';
/// Comprehensive example demonstrating all binary_patch features.
///
/// Run with:
/// dart run example/main.dart
Future<void> main() async {
print('');
print('╔══════════════════════════════════════════════════════════════╗');
print('║ binary_patch — Comprehensive Example ║');
print('╚══════════════════════════════════════════════════════════════╝');
print('');
// Create temporary directory for example files
final tmpDir = Directory.systemTemp.createTempSync('binary_patch_example_');
final oldPath = '${tmpDir.path}/app_v1.bin';
final newPath = '${tmpDir.path}/app_v2.bin';
final patchPath = '${tmpDir.path}/patch_v1_to_v2.bin';
final outPath = '${tmpDir.path}/app_v2_restored.bin';
try {
// ── 1. Generate example binary files ──────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 1: Generating test binary files ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final rng = Random(42);
final oldData = _generateFakeExecutable(rng, sizeKB: 512);
final newData = _simulateMinorUpdate(oldData, rng);
await File(oldPath).writeAsBytes(oldData);
await File(newPath).writeAsBytes(newData);
print(' ✓ Old file : ${(oldData.length / 1024).toStringAsFixed(1)} KiB → $oldPath');
print(' ✓ New file : ${(newData.length / 1024).toStringAsFixed(1)} KiB → $newPath');
print('');
// ── 2. Pre-flight analysis ─────────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 2: Pre-flight Analysis (dry run) ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final analysis = await BinaryPatch.analyze(
oldFile: oldPath,
newFile: newPath,
options: PatchOptions.balanced(),
);
print(' Estimated patch size : ${analysis.estimatedPatchMB.toStringAsFixed(3)} MiB');
print(' Estimated savings : ${analysis.estimatedSavingsPercent.toStringAsFixed(1)}%');
print('');
// ── 3. Create patch (balanced options) ────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 3: Creating the Patch ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final createResult = await BinaryPatch.create(
oldFile: oldPath,
newFile: newPath,
outputPatch: patchPath,
options: PatchOptions.balanced(),
onProgress: (p) {
final filled = (p * 30).round().clamp(0, 30);
final bar = '█' * filled + '░' * (30 - filled);
stdout.write('\r Progress: [$bar] ${(p * 100).toInt()}%');
},
);
print('\n');
print(createResult.summary);
print('');
// ── 4. Inspect metadata ────────────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 4: Inspecting Patch Metadata ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final meta = await BinaryPatch.readMeta(patchPath);
print(' Format version : ${meta.patchVersion}');
print(' Compression : ${meta.compressionType}');
print(' Created at : ${meta.createdAt}');
print(' Old size : ${meta.oldFileSize} bytes');
print(' New size : ${meta.newFileSize} bytes');
print(' Old SHA-256 : ${meta.oldFileChecksum.substring(0, 24)}…');
print(' New SHA-256 : ${meta.newFileChecksum.substring(0, 24)}…');
print('');
// ── 5. Verify patch ────────────────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 5: Verifying Patch Integrity ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final isValid = await BinaryPatch.verify(patchPath);
print(isValid
? ' ✓ Patch header is valid'
: ' ✗ Patch is INVALID!');
print('');
// ── 6. Apply patch ─────────────────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 6: Applying the Patch ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final applyResult = await BinaryPatch.apply(
oldFile: oldPath,
patchFile: patchPath,
outputFile: outPath,
verifyChecksum: true,
onProgress: (p) {
final filled = (p * 30).round().clamp(0, 30);
final bar = '█' * filled + '░' * (30 - filled);
stdout.write('\r Progress: [$bar] ${(p * 100).toInt()}%');
},
);
print('\n');
print(' ✓ Patch applied in ${applyResult.durationMs} ms');
print(' ✓ Output: ${applyResult.outputPath}');
print('');
// ── 7. Verify round-trip ───────────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 7: Round-trip Verification ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final restored = await File(outPath).readAsBytes();
final match = _bytesEqual(restored, newData);
if (match) {
print(' ✓ PERFECT MATCH — Restored file is bit-for-bit identical to original!');
} else {
print(' ✗ MISMATCH — Something went wrong!');
exit(1);
}
print('');
// ── 8. In-memory API demo ──────────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 8: In-Memory API (Flutter ready) ');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
final patchBytes = await BinaryPatch.createBytes(
oldData: oldData,
newData: newData,
options: PatchOptions.fast(),
);
print(' ✓ In-memory patch created: ${patchBytes.length} bytes');
final restoredBytes = await BinaryPatch.applyBytes(
oldData: oldData,
patchData: patchBytes,
verifyChecksum: true,
);
print(' ✓ In-memory patch applied : ${restoredBytes.length} bytes');
print(' ✓ Match: ${_bytesEqual(restoredBytes, newData)}');
print('');
// ── 9. Compression comparison ──────────────────────────────────────────
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' STEP 9: Compression Algorithm Comparison');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
for (final type in [
CompressionType.none,
CompressionType.bzip2,
CompressionType.zstd,
CompressionType.lzma,
]) {
final sw = Stopwatch()..start();
final bytes = await BinaryPatch.createBytes(
oldData: oldData,
newData: newData,
options: PatchOptions(compression: type, compressionLevel: 6),
);
sw.stop();
final savings = (1.0 - bytes.length / newData.length) * 100;
print(' ${type.name.padRight(6)}: '
'${(bytes.length / 1024).toStringAsFixed(1).padLeft(8)} KiB '
'savings=${savings.toStringAsFixed(1).padLeft(5)}% '
'${sw.elapsedMilliseconds}ms');
}
print('');
// ── Summary ────────────────────────────────────────────────────────────
print('╔══════════════════════════════════════════════════════════════╗');
print('║ All steps completed successfully! 🎉 ║');
print('╚══════════════════════════════════════════════════════════════╝');
print('');
} finally {
await tmpDir.delete(recursive: true);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Test data generators
// ─────────────────────────────────────────────────────────────────────────────
/// Generates a fake binary "executable" with realistic byte distribution.
Uint8List _generateFakeExecutable(Random rng, {required int sizeKB}) {
final size = sizeKB * 1024;
final data = Uint8List(size);
// Header
data[0] = 0x4D; data[1] = 0x5A; // MZ
data[2] = 0x50; data[3] = 0x45; // PE
// Fill with pseudo-code patterns (lots of zero bytes + structured data)
for (var i = 4; i < size; i++) {
// Simulate compiled code — mostly zeros and small values
final r = rng.nextInt(100);
data[i] = r < 40 ? 0 : (r < 70 ? rng.nextInt(16) : rng.nextInt(256));
}
return data;
}
/// Simulates a minor code update: changes ~2% of bytes.
Uint8List _simulateMinorUpdate(Uint8List original, Random rng) {
final data = Uint8List.fromList(original);
final changes = (original.length * 0.02).toInt();
// Simulate function body change — consecutive block
final start = rng.nextInt(original.length ~/ 2);
for (var i = 0; i < changes; i++) {
data[start + i] = rng.nextInt(256);
}
// Simulate address relocation — scattered +4 offsets
for (var i = 0; i < 50; i++) {
final pos = rng.nextInt(original.length - 4);
final val = (data[pos] + 4) & 0xFF;
data[pos] = val;
}
return data;
}
bool _bytesEqual(Uint8List a, Uint8List b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}