Wire compatibility with upstream zsync¶
The whole point of go-deltasync/zsync2 is that the .zsync it reads and
writes is byte-for-byte identical to what the upstream C/C++
implementations produce. This page is the receipt.
What we test against¶
Two reference implementations exist in the wild:
| Implementation | Repo | Language | Status |
|---|---|---|---|
zsync (Phipps) |
https://github.com/cph6/zsync | C | the original, ships in Debian/Ubuntu as zsync |
zsync2 (AppImageCommunity) |
https://github.com/AppImageCommunity/zsync2 | C++17 | the rewrite, ships standalone |
The two share the on-the-wire format byte-for-byte, so it's enough to test
against one of them. We test against the Debian zsync package because:
- it's the most widely deployed
- it's the upstream that defines the format
- it installs in CI with
apt-get install -y zsync— no five-minute AppImageCommunity build
If the compat test passes against zsync, it passes against zsync2.
The compat test¶
internal/zsync/compat_test.go runs with the compat build tag:
It does four round trips:
gozsyncmake→zsync— we produce a.zsync, the C client reconstructs the target using only the seed + HTTP server, output must be byte-identicalzsyncmake→gozsync— same in reverse: upstream produces the.zsync, we reconstruct- Parse stability — read a C-produced
.zsyncinto ourControlFile, re-serialize it, diff against the original bytes - Bit-flip detection — flip one byte in the seed, verify both implementations notice the mismatch and re-fetch the affected block
All four pass at every commit; CI gates merges on this. If zsync /
zsyncmake are not on $PATH (local development on macOS, say) the suite
skips cleanly rather than failing.
CI¶
The compat workflow
runs on every push and PR to main. It:
- Installs
zsyncfrom Ubuntu's apt repo - Runs
go test -tags=compat -race ./... - Fails the PR if any of the four round trips diverge by a byte
What's covered as of this release¶
- Multi-URL failover —
ResolveTargetURLreturns the full list ofURL:entries embedded in the.zsync, andFetchBlocksMultiwalks them. Network errors /5xx/404advance to the next URL; any other4xxfails fast so a misconfigured URL list doesn't burn through every backup on the same problem. Once a URL accepts a Range request we stick to it for the rest of the missing blocks. Z-Map2reader — the header parser recognisesZ-Map2:andZ-URL:and decodes the per-entry deltas wire format (with the0x8000 NOTBLOCKSTARTflag). The parsed restart-point table is exposed atControlFile.ZMap.ResolveCompressedURLsis theZ-Map2-side counterpart ofResolveTargetURL. Byte-exact round trip viaWriteis preserved.Z-Map2maker — producing a.zsyncindexed against a gzip-compressed target. A self-contained deflate-bitstream walker (internal/zsync/deflate_walker.go) finds every block-boundary / Huffman-reset point and records the(compressed-bit-offset, uncompressed-byte-offset)table, since Go'scompress/flateexposes no boundary hooks. Driven bygozsyncmake --z-map(auto-enabled for.gzinput) and theMakeWithZMap2/EncodeZMap2API.Z-Map2end-to-end tests —TestCompatZMap2OurMakeRoundTripexercises our maker + our reader, andTestCompatZMap2UpstreamMakeOurApplybuilds the index with the Czsyncmake -Zand applies it with our client. The latter skips cleanly when the apt-installedzsyncmakewas compiled without the gzip-aware-Zpath (it varies across Ubuntu LTS versions).Z-Map2+zsync2: 1.0— explicitly rejected at parse time. The BLAKE3 proposal hasn't pinned down random- access deflate semantics yet, so combining the two is left as a separate spec.
What's still not covered¶
- Client-side
Z-Map2fetch. The maker emitsZ-URL:+Z-Map2:and the client parses the restart-point table intocf.ZMap, but the download path — fetch a gz byte range, reset the inflater at a restart bit-offset (priming the 32 KiB back-reference window), decompress just enough to cover a missing block — is not yet wired intogozsync. A client pointed at aZ-Map2.zsyncwhoseURL:is a gz endpoint currently falls back to downloading the gz whole-file. Recompress. The header round-trips throughWrite, but the client does not act on it.- BLAKE3 +
Z-Map2. Intentionally unspecified: the parser rejects the combination loudly and the maker refuses--z-map --format=zsync2, pending the BLAKE3 proposal pinning down the random-access deflate semantics.