File content
#9.1 Layout shapes
The number of chunks the chunker produces (contract in §9.2) determines the file layout. There are exactly three shapes:
Writer algorithm (normative — it is what makes output independent of input slicing):
Run the chunker over the content. Skip zero-length chunks.
Hold the first non-empty chunk until a second arrives or input ends.
If input ends with exactly one non-empty chunk held:
- length ≤
inlineThreshold(default 1024): emit an inline entry (d= the chunk,sz= its length). No block is written. - otherwise: store the chunk as one raw block; entry
r= its raw-codec ref.
- length ≤
If input ends with zero non-empty chunks: emit the empty-file entry (
sz0, neitherdnorr).If a second chunk arrives: store all chunks as raw blocks, collect
[length, ref]pairs in chunk order, and build a balanced tree of"f"nodes bottom-up:layer = the chunk pairs while length(layer) > maxChunkRefs: partition layer sequentially into groups of maxChunkRefs (the final group may be smaller) layer = [ putFileNode(group) for each group ] entry r = dag-cbor ref of putFileNode(layer) # the single root "f" nodewhere
putFileNode(pairs)stores{v:1, t:"f", sz: Σ sizes, c: pairs}and yields the pair[Σ sizes, dag-cbor ref]. Nested pairs thus carry the subtree's logical size.maxChunkRefsdefaults to 2048 (§14).A lone chunk never becomes a one-child tree (step 3 handles it), but interior one-pair
"f"nodes MAY occur (e.g. 2049 chunks → groups of 2048 + 1).A chunker that yields no chunks at all (not even an empty one) is a contract violation and MUST be treated as an error, not as an empty file.
Reader validation (§13.2): for every fetched leaf, block length MUST equal
the declared pair size (or entry sz for a single-chunk file); for every
fetched "f" node, its sz MUST equal both the referencing pair's size (or
the entry's sz at the top) and the sum of its own pairs; inline d length
MUST equal sz. "f" node nesting deeper than 64 (MAX_DEPTH) MUST be
rejected.
#9.2 Chunker contract
A chunker consumes a stream of input pieces and produces a stream of chunks such that:
- Concatenated chunks equal the concatenated input.
- At least one chunk is produced (a single empty chunk for empty input).
- Boundaries depend only on the content bytes, never on how the input was sliced into pieces (piece-slicing invariance).
- Produced chunks are stable, unaliased buffers: a producer reusing one input buffer between pieces MUST NOT corrupt already-produced chunks.
#9.3 Fixed-size chunker
fixedSize(size) (size ≥ 1): emit consecutive size-byte chunks; the final
chunk carries the remainder (possibly the whole input when shorter than
size; a single empty chunk for empty input).
#9.4 FastCDC chunker (default)
Content-defined chunking: cut points stick to content, so an edit re-chunks only its neighborhood and unchanged regions keep their chunk refs (and therefore dedupe across versions):
Bounds min, avg, max (defaults 8192 / 32768 / 98304): integers,
1 ≤ min ≤ avg ≤ max, avg a power of two in [16, 2^29] (the cut
arithmetic is 32-bit). The bounds are parameters, recorded in the store
metadata as chunkMin, chunkAvg, chunkMax (§14, §10.3).
The GEAR table is a fixed array of 256 unsigned 32-bit values, generated
by splitmix32 from seed 0. All arithmetic below is modulo 2^32; ^ is XOR,
>> is a logical (zero-fill) right shift:
s = 0
for i in 0 .. 255:
s = (s + 0x9e3779b9) mod 2^32
z = s
z = (z ^ (z >> 16)) * 0x21f0aaad mod 2^32
z = (z ^ (z >> 15)) * 0x735a2d97 mod 2^32
GEAR[i] = z ^ (z >> 15)The GEAR table and the cut rule below are format constants: every chunk boundary — and therefore every ref of CDC-chunked content — depends on them. The bounds are not: they are parameters (§14). The first eight GEAR values are pinned in Appendix A.4.
Cut widths: with bits = log2(avg):
kS = bits + 2 # strict, before the normal point
kL = bits − 2 # relaxed, after itThe avg constraint (a power of two in [16, 2^29]) is exactly what
guarantees 2 ≤ kL and kS ≤ 31.
Cut rule. Given a window buf of pending bytes (and knowing whether the
input has ended), the next cut length is:
cut(buf):
n = min(length(buf), max)
if n ≤ min: return n # only reachable on final flush
normal = min(avg, n)
h = 0
for i in min .. normal − 1:
h = ((h << 1) + GEAR[buf[i]]) mod 2^32
if (h >>> (32 − kS)) == 0: return i + 1
for i in normal .. n − 1:
h = ((h << 1) + GEAR[buf[i]]) mod 2^32
if (h >>> (32 − kL)) == 0: return i + 1
return n # hit max, or end of dataHere >>> is a logical (zero-fill) right shift on the 32-bit value h, so
the boundary test is "the top kS (resp. kL) bits of h are all zero" —
equivalently h < 2^(32 − kS) (resp. h < 2^(32 − kL)).
The test is on the high bits, and this is load-bearing: under the
recurrence, the low bits barely depend on the window (bit 0 of h is
exactly bit 0 of GEAR[buf[i]]), so a low-bit test would collapse the
shift-resistance CDC exists to provide. See Appendix B
for the full argument and measurements.
Note the hash starts at zero at offset min; bytes before offset min
never influence the cut (sub-minimum skipping).
Streaming discipline. A cut decision is only definitive when the window
holds at least max bytes of lookahead, or the input has ended:
while pending ≥ max bytes: # during input
emit buf[0 .. cut(buf))
while pending > 0: # after input ends
emit buf[0 .. cut(buf))
if nothing was emitted: emit one empty chunkThis is what guarantees piece-slicing invariance: a boundary is never declared while more input could still have moved it.