Repository navigation
Produce byte-identical indexes to casync - #393
Merged
Merged
Conversation
Two differences kept desync's blob indexes from matching casync's for the same input, despite the README claiming identical output. The chunker never tested the rolling hash window that ends exactly at the min chunk size. Both tools apply the same optimization of leaving the first min-48 bytes unhashed, but casync's ca_chunker_scan() seeds the window and immediately tests it, so a boundary there yields a chunk of exactly min bytes. desync seeded the identical hash and rolled a byte in before its first test, making min+1 the smallest chunk it could produce. Whenever a boundary fell on min the two disagreed: on a 123MB tar that happened once in 1699 chunks, casync emitting 16384+56320 where desync emitted a single 72704. Test the seeded window before entering the loop. The blob index header also omitted CaFormatExcludeFile, which casync includes in CA_FORMAT_DEFAULT and stamps into every index, so the feature flags read 0xa000000000000000 rather than 0xb000000000000000 and the files differed in one byte even when the chunking agreed. The flag means the encoder honors .caexclude files while walking a directory tree, so it is inert for a blob and only set on that path. Archive indexes keep their current flags: desync doesn't implement .caexclude (#114) and shouldn't claim to, and their flags differ from casync's regardless because desync stores a smaller catar feature set. With both fixed, casync and desync produce byte-identical caibx files for the same input, verified for default and custom chunk sizes and for both digest algorithms. Note for existing stores: the chunker change alters boundaries in the rare case above, so re-chunking data already in a store will not dedup against those few chunks. Existing indexes and chunks stay valid and readable.
folbricht
force-pushed
the
casync-identical-output
branch
from
August 29, 2026 16:49
b96d0f9 to
c546776
Compare
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The README claims "identical output to casync". Two things stopped that from being true for blob indexes.
1. The chunker never tested the boundary at the min chunk size
Both tools apply the same optimization — leave the first
min - 48bytes unhashed, since no boundary can occur before the minimum chunk size. They differ in one step of the seeding.casync's
ca_chunker_scan()fills the 48-byte window sochunk_sizereaches exactlymin, computesca_chunker_start(), and then immediately tests it:desync seeded the identical hash over
c.buf[c.min-ChunkerWindowSize : c.min]but never evaluated it — the loop started atbase := int(c.min)and rolledbuf[min]in before the first test, somin+1was the smallest chunk it could emit.Whenever a boundary fell exactly on
minthe two disagreed. On a 123MB tar of/usrthat happened once in 1699 chunks: casync emitted 16384 + 56320 where desync emitted a single 72704. At that offset the window ending at 16384 hashes to0xad145682, and0xad145682 % 49535 == 49534 == d-1, so it is a genuine boundary; desync's first test one byte later is not. The two resynced at the next boundary, so it never cascaded.Fixed by testing the seeded window before entering the roll loop.
2. The blob index header omitted
CaFormatExcludeFilecasync includes it in
CA_FORMAT_DEFAULTand stamps it into every index, so its flags read0xb000000000000000where desync's read0xa000000000000000— a one-byte difference even when the chunking agreed.The flag means the encoder honors
.caexcludefiles while walking a directory tree (caencoder.c:538), so it is inert for a blob index, where there is no tree to walk. It is therefore set only on that path, inmake.go.Archive indexes deliberately keep their current flags.
ChunkStream(used only bytar -i) would be claiming.caexcludesupport that desync doesn't implement — that's still open as #114 — and caidx flags differ from casync's regardless, because desync stores a smaller catar feature set (0xb0000001ffeffe26vs0xb00000000100 1f22on a sample tree). Setting the bit there would buy no parity and assert something untrue.Verification
casync 2 (Debian
2+20201210-2+b2) and desync run in the same container over three data sets — a 123MB tar of/usr, 400MB of zeros, 400MB of random..caibxfiles are now byte-identical in every case, including the tar that previously diverged (both now 1699 chunks, 68064 bytes).Also identical with
--digest=sha256and with non-default chunk sizes (8:32:128K), and cross-tool extraction works both directions — casync extracting from a desync-built store and vice versa, output compared byte-for-byte, with both stores holding the same 1678 chunks.chunker_minsize_test.gocovers the min-size boundary directly by searching for a window whose hash lands on a boundary and asserting the resulting chunk is exactlyminbytes. It fails without the fix (produces 122 bytes for a 64-byte min).Compatibility
Nothing about the format, chunk IDs or stores changes, and existing indexes and chunks stay valid and readable by both tools.
The chunker change alters boundaries in the rare case above, so re-chunking data that is already in a store won't dedup against those few chunks. On the tar that was 1 chunk in 1699; the expected rate is roughly 1 in
discriminator(~49535 at the default average).