feat: add delta transfer for incremental sync #22
Reference in New Issue
Block a user
Delete Branch "feature/delta-transfer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implement rsync-style delta transfer using rolling checksums (Adler-32 + xxHash32). When a file exists on both sides but has changed, only the changed blocks are transmitted instead of the entire file.
Changes
New files
src/shared/delta.h/delta.c— Core delta engine (Adler-32, xxHash32, signature generation, rolling checksum delta computation, serialization, application)src/shared/xxhash.h— Vendored xxHash single-header librarytests/test_delta.h/test_delta.c— 13 unit testsModified files
src/shared/protocol.h/.c— New status codes: STATUS_DELTA_SIGNATURE (7), STATUS_DELTA_DATA (8)src/shared/config.h/.c— Added use_delta, delta_block_size fields; protocol version bumped to 1.2.0src/shared/file.c— Extended receive_incremental_check() with full delta negotiationsrc/client/client_send.c— Extended incremental_check() for delta; added send_delta()src/client/client_cli.c— Added --delta and --delta-block flags with validationtests/runner.c— Registered delta test suiteProtocol flow
Usage
Testing
CHANGES REQUESTED — 1 critical performance issue
CRITICAL:
delta_computeis O(n*m) — not a rolling checksumdelta.c:407-461— The rolling checksum window advances 1 byte on miss and recomputes both adler32 and xxhash32 from scratch at every position:Adler-32 is specifically designed as a rolling hash — you can update it in O(1) by subtracting the leaving byte and adding the entering byte. The current implementation defeats this entirely.
Worst case: 100MB file with a 1-byte insertion at position 50MB. After the change, all blocks shift by 1 byte, so every position from 50MB to 100MB fails to match, each recomputing adler32+xxhash32 on 8KB. That's ~50M * 8KB = ~400TB of hash computation.
Fix: Implement a proper rolling adler32 that updates in O(1), and only compute xxhash32 (the strong hash) when adler32 matches. This is the standard rsync technique and reduces the worst case from O(n*m) to O(n + m).
Other findings (non-blocking)
Duplicated delta logic in
send_chunk(client_send.c:102-162vs125-185): The incremental_check + send_delta pattern is copy-pasted for the sendfile path and the normal path. Should be extracted into a helper.Deep nesting in
receive_incremental_check(file.c:149-1034): The delta handling path is 6+ levels of if/else with 30+ local variable cleanup paths. Functionally correct but hard to maintain. Consider extracting the delta receive logic into a separate function.delta_should_attemptuses hardcodedDELTA_MAX_FILE_SIZE(256MB). For files larger than 256MB, delta is skipped entirely. May want to make this configurable or raise the limit.Tests use only 2-4KB files — the O(n*m) bug isn't exercised. Add a test with a 100KB+ file with a small edit to catch the performance regression.
What's good
delta_is_worthwhileat 70% ratio)PASS — All review findings addressed. Rolling adler32 is correctly implemented in O(1). Code is clean with extracted helpers. Tests improved with 200KB file case. Ready to merge.
❌ Request: Replace vendored xxhash.h with CMake dependency
src/shared/xxhash.his 7490 lines vendored, but onlyXXH32()is used. The project already uses CMake and links external libs (libzstd-dev,libssl-dev), so this should be a proper dependency.Recommended:
FetchContent— pull xxHash at CMake configure time, no system install needed, no 7500-line file in the repo:In
delta.c, replace the vendored include with:This keeps the repo clean and makes version management straightforward.