Skip to content

test: validate binary patch codec contract - #149

Open
floyd-soomgo wants to merge 3 commits into
codex-try-turbomodule-3from
feature/binary-patch-codec-contract
Open

test: validate binary patch codec contract#149
floyd-soomgo wants to merge 3 commits into
codex-try-turbomodule-3from
feature/binary-patch-codec-contract

Conversation

@floyd-soomgo

Copy link
Copy Markdown
Member

Summary

First PR of a series adding binary differential OTA updates: instead of downloading the full JS bundle on every release, clients will apply a small binary patch against the Hermes bytecode bundle embedded in the app binary, falling back to the full archive when anything mismatches.

This PR establishes the codec contract only — the diff/patch format, the vendored native applier sources, and automated tests that pin the format's behavior. Nothing is wired into the runtime yet: no gradle/CMake target, no podspec entry, and no change to the update flow. Follow-up PRs build on this contract (CLI artifact generation → release metadata → Android/iOS appliers → E2E).

Codec: HDiffPatch v5.1.3, patches generated with hdiffz -f -m-6 -c-zstd-21-24 (identifier hdiffpatch-m-zstd, format version 1). Chosen for patch size (~3x smaller than bsdiff on Hermes bundles in our measurements), generation speed, and a lightweight apply-side: the applier needs only the HPatch decoder sources plus a zstd decompressor.

Changes

  • cpp/binarypatch/ — platform-shared native sources (iOS will consume them via the podspec at the repo root, Android via externalNativeBuild; both land in follow-up PRs):
    • vendor/ — minimal vendored subset: HDiffPatch v5.1.3 patch-apply sources + zstd decompress-only sources (ZSTD_DISABLE_ASM=1, _IS_USED_MULTITHREAD=0; upstream layout preserved for traceability)
    • binarypatch_zstd_decompressor.{h,c} — our own hpatch_TDecompress adapter over zstd streaming decompression; window log capped at 24 to match the pinned generation options, so an over-wide (corrupt) frame header is rejected before allocation
    • host/ — a small host-compilable harness (apply <old> <patch> <out>, exit codes distinguish failure modes) used by the test suite to prove the vendored applier restores byte-identical targets; excluded from the npm package
  • cli/utils/binaryPatch.tsgeneratePatch/applyPatch wrappers around hdiffz/hpatchz with the options pinned
  • scripts/binary-patch/build-hdiffpatch.sh — builds the pinned tools from source into .hdiffpatch-tools/ (gitignored); minimal build needs only sisong/zstd alongside
  • cli/fixtures/binary-patch/ — deterministic base/target fixtures (generator script checked in) plus the committed update.patch
  • cli/utils/binaryPatch.test.ts — the contract tests (see below)
  • THIRD-PARTY-NOTICES — HDiffPatch (MIT) and zstd (BSD-3-Clause) attribution

The contract the tests pin

  • A generated patch applied to the exact base restores the target byte-identically (verified via SHA-256, through both the CLI applier and a host build of the vendored native sources).
  • Header corruption and truncation fail cleanly (no crash, non-zero exit).
  • Body corruption is NOT reliably detected by the codec: flipping bytes inside the compressed diff data can decode "successfully" into wrong output (the format carries no content checksum — we swept 41 offsets to confirm). The same holds for applying against a same-size-but-different base. The tests therefore assert the honest property — the output never hash-matches the target — and the runtime appliers in follow-up PRs treat base/target hash verification as mandatory, with full-archive fallback on any mismatch.

Test plan

npm run --workspace cli test   # 16/16 — builds hdiffz/hpatchz on first run (network required once)
npm run typecheck
npm run jest                   # full suite

Notes for reviewers

  • ~55k lines of this diff are verbatim vendored upstream sources under cpp/binarypatch/vendor/. The first-party surface is: the adapter, the host harness, cli/utils/binaryPatch.*, the fixture generator, the build script, and the notices.
  • npm pack ships cpp/ (42 files) but excludes the dev-only host/ harness.

Binary patch updates need HDiffPatch's patch applier and zstd's decompressor
compiled into the native library, so the minimal set of sources for applying a patch
is vendored here.

Only the apply side is vendored: libHDiffPatch/HPatch plus zstd's common and
decompress directories. zstd's amd64 assembly implementation is left out because the
applier builds with ZSTD_DISABLE_ASM=1. The upstream directory layout is kept because
the HDiffPatch headers include each other by repository path.

The tree lives at the repository root rather than under android/ or ios/ because both
platforms compile the same sources: iOS through the podspec at the root, Android
through externalNativeBuild. One shared copy leaves nothing to drift.
…lier

HDiffPatch takes the decompressor as a plugin. Upstream ships a demo header
implementing a dozen codecs, which would pull in headers for codecs the applier never
sees, so this is a standalone hpatch_TDecompress implementation for zstd - the only
codec CodePush patches are compressed with. The window it accepts is capped at the
2^24 the generation options use, so a corrupted frame header asking for a wider
window is rejected before anything is allocated.

apply_patch_host is the reference applier built from those sources for the development
machine. It loads the base bundle and the patch into memory and writes the restored
bundle sequentially, which is the memory contract the Android and iOS appliers follow,
and it maps each failure to its own exit code so a caller can tell a corrupt patch
from a mismatched base.
Adds the CLI side of the codec - generatePatch and applyPatch spawn hdiffz and hpatchz
with the fixed options that define the patch format - together with a test suite that
pins the format down against real bytes and real binaries.

The fixtures are committed and can be regenerated deterministically by
scripts/binary-patch/generate-fixtures.mjs, so the tests verify the patch bytes that
ship rather than bytes produced on the fly. hdiffz and hpatchz are built from upstream
sources by scripts/binary-patch/build-hdiffpatch.sh, which the suite runs on demand
when the tools are missing.

Two properties are pinned down because callers have to handle them: a patch carries no
checksum of the base data, and its zstd streams carry no content checksums. Applying a
patch to a different base of the same size, or applying a patch whose body is
corrupted, can therefore report success and still produce the wrong bytes. Verifying
the base and target hashes stays the caller's responsibility.

The applier sources ship in the npm package so both platforms can build them, minus
the host harness, which only exists to run this suite.
An error occurred while trying to automatically change base from codex-try-turbomodule-3 to master August 6, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant