moon-deltasync

    Deterministic incremental transfer kernel: block signatures, verified deltas, content-defined chunking and deduplication

    delta
    rsync
    incremental
    content-defined-chunking
    deduplication
    Download zip
    Version
    0.1.2
    License
    MIT
    Last updated
    1 hour ago
    Downloads
    3

    #moon-deltasync

    MoonBit 增量传输内核。在两个版本的数据之间只传差异,并在接收端可验证地重建出与源逐字节相同的结果。

    三个能力在同一套字节格式上闭合:rsync 风格块签名、可逐块校验的 delta、内容定义分块与去重。库的根包只 import moonbitlang/core;模块另有一个依赖 moonbitlang/x@0.5.6,仅 cmd/main 读文件时用到(库本身不碰文件系统)。算法全部来自公开定义,本项目是独立实现,不宣称发明 rsync、Rabin/gear CDC 或 SHA-256。与 MoonBit 相邻包的关系和有限查重结论见 SOURCES.md。

    #定位与边界

    它解决的是和 rsync、增量 OTA、备份去重、CI 构建缓存同一类的问题:数据有历史版本,下一次只需要传「变了的部分」,而正确性必须能被验证,不是被相信。

    本库不做的事,逐条写清楚,避免过度承诺:

    • 不做网络:没有 socket、没有协议握手、没有断点续传,不做 rsync 的 ssh 通道与并发调度。传输层由调用者决定。
    • 不做文件系统:库的输入输出统一是内存字节(BytesView / Bytes);只有 cmd/main 碰文件。不读目录树、不做多文件同步、不做内存映射。
    • 不做压缩:可与任意压缩层叠加,本库不感知也不内嵌 deflate/zstd/brotli。
    • 不做加密,也不做签名认证:强校验是完整性校验,不是数字签名。它能发现传输或存储造成的损坏,不能阻止有能力的篡改者伪造数据并同步伪造指纹。
    • 不追求比成熟 C 实现更快。追求的是确定性、可验证、不做隐藏的第三方依赖、四后端一致。

    #三个完整工作流

    使用者与问题输入 → 库内计算 → 输出可运行入口
    收到增量包的客户端:这个 delta 能不能安全落到本地?旧数据 + 服务端给的 delta → 反序列化、重放 COPY/COPY_RUN/LITERAL、逐 op 校验、末尾再验整体摘要 → 新数据 + 结论(成功,或第几块失败)verified_update;examples/corruption_check
    发布方 / 服务端:这次更新实际要传多少字节?客户端签名 + 新版本 → 弱校验分桶、滑窗匹配、规划、规范化 → DDLT 字节流 + 字节数与节省比例plan_transfer / plan_from_signature;examples/version_upgrade
    备份 / 构建缓存:两个版本重复了多少?两个版本的对象 → 内容定义分块、指纹索引、引用计数 → DCHK manifest + 去重率 + 可重建校验ChunkStore::ingest / assemble;examples/chunk_dedup

    第一条是唯一会拒绝数据的路径,也是一层的叠加:delta 里每个 COPY/COPY_RUN 自带所复制内容的强校验(接收端不需要签名文件就能逐 op 验证),最后一层是 DDLT 头部携带的目标数据 SHA-256。

    #环境与快速开始

    要求 MoonBit CLI,moonc >= 0.10.16。本机使用的工具链在 D:\moonbit\_tools\moon-0.10.17\bin,moon version --all 实测输出:

    moon 0.1.20261023 (3778465 2026-10-23) moonc v0.10.17+d971670cd (2026-10-23) moonrun 0.1.20261023 (3778465 2026-10-23)

    在该目录外使用时,把工具链 bin 放进 PATH:

    $env:PATH = 'D:\moonbit\_tools\moon-0.10.17\bin;' + $env:PATH

    在项目根目录执行:

    moon check --deny-warn --target all moon build --target js moon test --deny-warn --target js moon run examples/version_upgrade --target js moon run examples/chunk_dedup --target js moon run examples/corruption_check --target js

    JS 后端需要 Node.js;wasm、wasm-gc 由 moonrun 直接执行;native 后端需要 MSVC(cl.exe/clang-cl),本机未安装,见下文「验证状态」。四个后端必须产生逐字节相同的签名、delta 与 manifest(不变量 7);本机已在 js / wasm / wasm-gc 上跑通,native 由 CI 覆盖。

    已发布到 Mooncakes:niuniu513-ask/moon-deltasync@0.1.1(MIT)。在另一个包里引用:

    moon add niuniu513-ask/moon-deltasync@0.1.1

    import {
    "niuniu513-ask/moon-deltasync" @ds,
    }

    也可以把本目录作为工作区模块直接引用,跳过下载。完整公开接口见 pkg.generated.mbti。

    #三个示例的实际输出

    以下是本机实测(--target js,工具链同上),不是设计意图的转述:

    examples/version_upgrade —— 2000 段文本,中间插入一行:

    old bytes: 120890 new bytes: 120935 | delta 字节 | 8346 | 复制操作数 2,复制字节 114688,字面量字节 8237,节省比例 0.9310 delta bytes on the wire: 8346 rebuilt bytes: 120935 (verified 14 blocks) identical to the new version: true

    examples/chunk_dedup —— 两个版本各约 139 KiB,只有一个 generation 标记不同:

    {"kind":"chunk","path":"snapshot-1","length":142471,"chunk_count":13,"new_chunks":13,"reused_chunks":0,"stored_bytes":142471,"saved_bytes":0,"root_hex":"e23b35b5…"} {"kind":"chunk","path":"snapshot-2","length":142471,"chunk_count":13,"new_chunks":1,"reused_chunks":12,"stored_bytes":148786,"saved_bytes":136156,"root_hex":"80b61929…"} distinct chunks in the store: 14 bytes actually kept: 148786 bytes deduplication avoided: 136156 matches the source: true

    examples/corruption_check —— 同一个 delta 在三种情况下的结局:

    intact delta rebuilds 25089 bytes, all checksums agree damaged delta: rejected: the rebuilt bytes do not match the digest the delta carries same delta against a different old version: rejected: block 1 does not match the signature

    最后两行是本文档最有说服力的部分:篡改被拒绝,而且拒绝原因是可区分的——整体摘要不符(DigestMismatch)与块级不符(BlockMismatch(1))走的是不同构造子。

    #字节格式

    四类整数默认大端,magic 是 4 字节 ASCII,version 固定为 1。以下是实现的实际布局(与 docs/design.md §5 一致,逐字节由测试与外部校验器复核):

    DSIG 签名 v1 — 定长头 22 字节,签名总长 = 22 + block_count * (4 + strong_len):

    "DSIG"(4) version:u8 block_size:u32 strong_len:u8 total_length:u64 block_count:u32 repeat block_count 次: weak:u32 strong:u8[strong_len]

    DDLT delta v1 — 头部带目标数据摘要,每个复制 op 自带强校验:

    "DDLT"(4) version:u8 block_size:u32 strong_len:u8 new_length:u64 content_digest:u8[32] op_count:u32 repeat op_count 次: tag:u8=0 COPY: block_index:u32 strong:u8[strong_len] tag:u8=1 LITERAL: length:u32 bytes[length] tag:u8=2 COPY_RUN: start:u32 count:u32 strong:u8[strong_len]

    DCHK manifest v1 — 头部记录分块参数,块长用 varint:

    "DCHK"(4) version:u8 min_size:u32 max_size:u32 mask_bits:u8 chunk_count:u32 repeat chunk_count 次: sha256:u8[32] length:varint root:u8[32] // = SHA-256(concat(每个块的 32 字节 sha256))

    三个解析器都会拒绝尾部多余字节,而不是忽略它们。strong_len 只接受 8 或 16;block_count 必须等于 ceil(total_length / block_size)。root 由块指纹确定性导出,不掺时间戳或随机量。

    #公开 API

    按层分组,全部来自 pkg.generated.mbti(moon info 生成,请以它为准):

    层入口
    签名sign_bytes(input, block_size?, strong_len?) -> FileSignature、decode_signature、FileSignature::{encode, block_count, block_length, digest_hex}、signature_matches
    deltacompute_delta(new, signature) -> Delta、Delta::encode、decode_delta、apply_delta(old, delta) -> ApplyResult、Delta::{digest_hex, op_count, copy_ops, copied_blocks, copied_bytes, literal_bytes}
    工作流plan_transfer(old, new, block_size?, strong_len?)、plan_from_signature(sig, new)、verified_update(old, delta_bytes)、TransferPlan::{delta, signature, delta_bytes, new_length, copy_ops, copied_bytes, literal_bytes, savings_ppm}
    分块chunk_boundaries(input, params?)、chunk_manifest(input, params?)、ChunkManifest::{encode, verify, chunk_count, total_length, unique_chunks, root_hex}、decode_manifest
    块存储ChunkStore::{new, ingest, assemble, unique_chunks, stored_bytes, saved_bytes}、ingest_snapshot(store, input, params?)
    报告render_signature_json(signature, path, content_sha256_hex, content_xxh64_hex)、render_delta_json、render_apply_json、render_chunk_json、render_error_json、render_plan_text、render_plan_markdown、ppm_text
    基础sha256、sha256_hex、sha256_prefix、weak_checksum、WeakRoll、xxh64、hex_encode、hex_decode、constant_time_equal、ByteSink

    两个值得注意的类型细节:BlockSignature.index / weak 是 Int32 / UInt32;Delta 含 content_digest : Bytes,DeltaOp::Copy 是 Copy(Int32, Bytes)(块号 + 该块强校验),CopyRun(Int32, Int32, Bytes),Literal(Bytes)。

    #命令行

    moon-deltasync sign <path> # 签名摘要与统计 moon-deltasync delta <old> <new> --out <d> # 计算 delta,可写出 DDLT 原始字节 moon-deltasync apply <old> <delta> --out <f> # 应用并逐块校验,可写出重建结果 moon-deltasync chunk <path> # 分块与去重率报告 moon-deltasync verify <path> <sha256-hex> # 全流校验

    四个子命令的 --out 都可省略;省略时只打印 JSON。带 --out 时 sign / chunk / delta / apply 分别写出 DSIG / DCHK / DDLT / 重建结果。

    本仓库里通过 moon run 调用:

    moon run cmd/main --target js -- sign old.txt --out old.dsig moon run cmd/main --target js -- delta old.txt new.txt --out patch.ddlt moon run cmd/main --target js -- apply old.txt patch.ddlt --out rebuilt.txt moon run cmd/main --target js -- chunk new.txt --out new.dchk

    每条命令向 stdout 打印一行 JSON。以下是同一对文件(120890 → 120935 字节文本)的实跑输出:

    {"kind":"sign","path":"old.txt","total_length":120890,"block_size":8192,"strong_len":16,"block_count":15,"signature_bytes":322,"signature_sha256_hex":"add8904b…","sha256_hex":"d242f2a7…","xxh64_hex":"8d32c2ca93e03df3"} {"kind":"delta","old":"old.txt","new":"new.txt","new_length":120935,"op_count":3,"copy_ops":2,"copy_blocks":14,"copied_bytes":114688,"literal_bytes":8237,"delta_bytes":8346,"delta_sha256_hex":"7830b34f…","savings_ratio":0.9310} {"kind":"apply","ok":true,"bytes_written":120935,"blocks_verified":14,"ops_applied":3,"sha256_hex":"970caa0e…"} {"kind":"chunk","path":"new.txt","length":120935,"chunk_count":4,"unique_chunks":4,"new_chunks":4,"reused_chunks":0,"stored_bytes":120935,"saved_bytes":0,"dedup_ratio":0.0000,"manifest_bytes":190,"root_hex":"ba92558c…"} {"kind":"verify","ok":true,"sha256_hex":"970caa0e…"} {"kind":"error","code":"usage","message":"the expected digest is not hexadecimal"}

    字段含义有几处容易混:

    • signature_bytes 是 DSIG 序列化长度(322 = 22 + 15 × 20)。sha256_hex 是被签名内容的全流 SHA-256,xxh64_hex 是同内容的 XXH64,二者让外部脚本可以独立复核内容;signature_sha256_hex 则是 DSIG 字节本身的哈希,用于比较两份签名是否相同。
    • delta 行同时给出 op_count(op 总数)、copy_ops(复制 op 数)、copy_blocks(复制的块数)、copied_bytes(复制字节)。op 数与块数不是一回事:本例 3 个 op 里 2 个是复制,覆盖 14 个块。
    • chunk 行的 manifest_bytes 是 DCHK 序列化长度,dedup_ratio 是 4 位小数文本;unique_chunks / stored_bytes / saved_bytes 是存储累计视角(CLI 每次新建 store,所以等于本文件的数)。
    • savings_ratio 由整数 ppm 渲染成 4 位小数,不受浮点后端差异影响。

    四条实测行为,写清楚以免踩坑:

    • 四个子命令的 --out 都生效:sign 写 DSIG、chunk 写 DCHK、delta 写 DDLT、apply 写重建结果,写的都是原始字节而不是 JSON。实测 sign --out 得到 322 字节、chunk --out 得到 190 字节,与对应 JSON 里的 signature_bytes / manifest_bytes 一致。
    • verify 有两档失败,语义不同:① 参数个数不是 2、摘要不是合法十六进制或不是 32 字节 → {"kind":"error","code":"usage",…} 且退出码 1;② 摘要合法但与文件不符 → {"kind":"verify","ok":false,…} 且退出码 0——这是正常结论(「这份文件不是这个摘要」),不是错误。
    • 未知命令与未知参数都返回 usage 错误 + 退出码 1,不会静默丢弃参数(实测 sign f --bogus → unexpected argument: --bogus)。
    • 文件不可读、格式损坏分别走 read_failed / format_error,退出码 1。

    #不变量

    以下 8 条是工程契约,每条都有对应测试(moon test 38 项,见「验证状态」):

    1. 重建一致:任意 old、new,apply_delta(old, compute_delta(new, sign_bytes(old))) 的输出等于 new。
    2. 规范化:compute_delta 的输出满足「相邻同类 op 已合并」,重新序列化后逐字节相同。
    3. 局部性:new 相对 old 只改 1% 的块时,delta_bytes <= 块数*8 + 改动字节 + 16。
    4. 检测损坏(三层):① 翻转 delta 中任意一个 LITERAL 字节 → 逐 op 校验或整体摘要拒绝;② 把 COPY/COPY_RUN 指向内容不同的旧数据 → BlockMismatch(index);③ 翻转或截断 DDLT 头部的 content_digest → DigestMismatch。第三层挡住了字面量与长度字段被改坏这类逐块校验看不见的损坏。
    5. 分块稳定:数据前缀插入 1 字节后,≥ 90% 的块指纹仍复用(默认参数、≥ 1 MiB 输入)。
    6. 去重正确:manifest 重建出的字节与输入相同,root 由块指纹确定性导出。
    7. 跨后端一致:相同输入在 wasm / wasm-gc / js / native 上的签名、delta、manifest 逐字节相同。
    8. 无 panic:截断、错 magic、错长度、超大 count、错块号等畸形输入全部返回 FormatError 诊断。

    FormatError 有 8 个构造子:BadMagic / UnsupportedVersion / Truncated / BadField / TooLarge / BlockMismatch / UnsupportedOp / DigestMismatch,并提供 message() 取人读文本。

    #复杂度与上界

    操作时间额外空间
    sign_bytesO(n) 单遍签名 = 22 + block_count * (4 + strong_len) 字节
    compute_deltaO(新数据长度),未命中时最多额外 block_size 次字节比较弱校验分桶索引 + 输出 delta
    apply_deltaO(新数据长度)输出缓冲
    chunk_boundaries / chunk_manifestO(n) 单遍,窗口恒定每块 32 字节指纹 + varint 长度

    规模上界(越界即 TooLarge/BadField,不做静默降级):block_size ∈ [256, 1 MiB],strong_len ∈ {8, 16},block_count ≤ 1_000_000,total_length ≤ 2^40。未指定 block_size 时按长度自适应:length < 4096 用 512,否则 8192。

    签名与数据成比例,这是 rsync 类方法的固有成本:strong_len=16、block_size=8192 时签名约为原文的 0.24%。块越小定位越精确、签名越大,这是可配置的取舍。

    #与现有 MoonBit 生态的关系

    在 mooncakes 全量索引快照(3,337 个包,mooncakes-index-20261026.json)中按名称与描述检索:rsync、delta sync、delta encoding、rolling hash 均为 0 命中;content-defined 命中 2 个(cn-ybm/MoonHashKit、xgzh1111/moonchunk),deduplicat 4 个,content-address 3 个,merkle 3 个。

    这一组数字在 2026-11-09 的在线注册表上已经变了:在线共 3,665 个包,rsync 在名称+描述口径下命中 1 个——YKDshaxing/moonlibsync(LGPL-2.1-or-later,2026-11-09 发布,自述「Librsync-compatible byte-buffer library and Native single-file CLI」,keywords 含 librsync/delta/sync)。这是目前最接近的相邻项目,因为 librsync 本身就是 rsync 算法的库化实现。delta sync、delta encoding、rolling hash 在在线索引中仍为 0 命中;content-defined、deduplicat 的在线计数比快照各多 1 个,因为本项目自身已发布进索引。

    扩大口径到 keywords 字段后,相邻能力更清楚地暴露出来:yangxixi00/moon_vcdiff(RFC 3284 VCDIFF 二进制 delta)、YKDshaxing/moonlibsync(librsync 兼容)、bzhangui/moonrestore(可验证增量备份 + 内容寻址)、yuzhiblue/moon-diff(文本 LCS/Myers diff、git patch)、gaato/*(流式解压缩 codec)、zploc/loci、ggbond44439/moonloom、Yingqingxue/mooncas、any07557-wq/moonvcs。

    因此本项目不宣称「MoonBit 生态没有增量传输 / 去重 / 内容寻址相关代码」——那是不成立的,librsync 兼容的包与 VCDIFF 编解码器都在。它主张的是:把块签名、可逐块校验的 delta、内容定义分块放在同一个库、同一套字节格式里,delta 自带逐 op 强校验与整体 content_digest,并在四个后端上逐字节一致;本项目用的是自定义的 DSIG/DDLT/DCHK 格式,与 librsync、VCDIFF 都不互通。逐包比对、检索命令与命中计数见 SOURCES.md;本库与上述包无代码依赖、无移植关系、无作者背书。

    #验证状态

    本机实测(2026-11-09,moonc v0.10.17+d971670cd):

    项目结果
    moon check --deny-warn --target all通过,0 warning
    moon build --target js / --target wasm-gc通过
    moon test --deny-warn --target js38 tests, 38 passed, 0 failed
    moon test --deny-warn --target wasm / wasm-gc各 38/38 通过
    moon test --target native未在本机运行:moon build --target native 报缺少 MSVC(cl.exe/clang-cl),由 CI 覆盖
    三个示例全部实跑,输出见上文
    CLI 端到端sign→delta→apply 重建结果与新文件 SHA-256 一致;verify 与 PowerShell Get-FileHash 一致
    CLI --out 四个子命令全部写出原始字节:sign 322 B(= signature_bytes)、chunk 190 B(= manifest_bytes)、delta 与 apply 同样落地,均与 JSON 内的字节数一致

    CI(.github/workflows/ci.yml)在 ubuntu-latest 上跑 check/build/test --target all,并用 moon fmt + git diff --exit-code 与 moon info + git diff --exit-code 检查格式化与接口文件是否稳定。main 上最近若干次运行均为 success(gh run list 复核,最近一次耗时 36s)。

    外部独立校验器:scripts/ 下的 Python 工具按 design.md 语义独立重写了弱校验、XXH64、三个格式的解析与 delta 重放,用来交叉验证本实现;它不读本仓库的 .mbt 源码。最近一次运行(scripts/verify-report.txt,生成于 2026-11-09 22:37:47,耗时 24.3s)的结论是:

    指标结果
    硬断言775 / 775 通过,0 失败
    advisory(不计入失败)26 / 26
    阻塞项0
    退出码0(0 = 硬断言全过;1 = 有硬断言失败;2 = 自测通过但 CLI 联调被阻塞)

    分类覆盖 selftest(hash 4、weak 3、format 36、replay 9、planner 5、cdc 4)与 CLI(sign 190、delta 200、apply 80、interop 30、chunk 154、verify 21、malformed 29、chunk-stability 2、cross-backend 7)。其中四条证据特别值得单独看:

    • delta 字节数与校验器自己的独立 rsync 规划器逐字节相等(10/10 语料,ratio 1.000,最大 96755 B / 2 MiB 语料)——规划质量不是自评出来的。
    • 双向互操作 30/30:它用自己生成的 DDLT 喂 CLI apply,重建逐字节相同;CLI 产出的 delta 用它的独立重放器也能重建。
    • 跨后端 js vs wasm-gc 7/7 一致;弱校验与 zlib.adler32 逐位一致(含空窗口),滑动更新 20034 次滑窗与直接计算一致。
    • 畸形输入 29/29 只报错、不崩溃。§7.5 分块稳定性与 DSIG/DCHK 导出两项阻塞均已解除。

    复现:在仓库根目录执行 D:\anaconda3\python.exe scripts\verify_deltasync.py(或在 scripts/ 下执行 --mode auto)。该报告是外部佐证,不是官方验收结论。契约与实现的对齐状态逐条列在 docs/usage.md §11。

    #发布状态与残余不确定

    已核实的发布事实(2026-11-09 用 gh 与 mooncakes 注册表 API 直接查询):

    • GitHub:niuniu513-ask/moon-deltasync 是 PUBLIC 仓库,默认分支 main,本机 main 与 origin/main 指向同一提交(仓库仍在活跃提交,SHA 会变)。
    • CI:gh run list 显示 main 上最近若干次运行均为 success,最近一次是「docs: make the type declarations and the --out note match the code」,耗时 36s。
    • Mooncakes:niuniu513-ask/moon-deltasync@0.1.1 已发布(MIT,is_new: true,创建于 2026-11-09),因此上面的 moon add 可用。

    仍然只是「未被本文核实」、而不是「已知有问题」的部分:

    1. CI 只看了最近几次运行:没有逐条回溯全部历史运行结果,也没有核对分支保护、必需检查等仓库设置。
    2. 相邻包的实现深度:SOURCES.md 的逐包比对基于索引元数据(名称、描述、keywords、许可证、仓库地址),没有阅读任何相邻包的源码或 README 正文,因此不对其能力细节下定论。
    3. 检索快照时效:逐关键词统计基于 2026-10-26 的快照;2026-11-09 的在线注册表为 3,665 个包,中间新增的包(包括 moonlibsync)已在上文单独说明,但未对全部新增包重做逐项比对。

    #许可证

    MIT,见 LICENSE。来源、相邻项目逐项比对与有限查重边界见 SOURCES.md;使用示例与 API 细节见 docs/usage.md。

    FormatError

    pub suberror FormatError {
    BadMagic(String)
    UnsupportedVersion(Int)
    Truncated(Int)
    BadField(String)
    TooLarge(String)
    BlockMismatch(Int)
    UnsupportedOp(Int)
    DigestMismatch
    }

    Everything that can be wrong with a serialized signature or delta.

    FormatError::message

    fn FormatError::message(self : FormatError) -> String

    ApplyResult

    pub struct ApplyResult {
    bytes_written : Int
    blocks_verified : Int
    ops_applied : Int
    digest_hex : String
    output : Bytes
    }

    The outcome of applying a delta: what was written and what was verified.

    BlockSignature

    pub struct BlockSignature {
    index : Int
    weak : UInt
    strong : Bytes
    }

    One block of the old data: its position, its weak checksum and the truncated SHA-256 of its bytes.

    ByteSink

    pub struct ByteSink {
    bytes : Array[Byte]
    }

    A growable byte sink that the format writers append to.

    ByteSink exists instead of Bytes because Bytes is immutable and the writers do not know the final size in advance. It keeps the "append then freeze" pattern in one place.

    ByteSink::length

    fn ByteSink::length(self : ByteSink) -> Int

    ByteSink::new

    fn ByteSink::new() -> ByteSink

    ByteSink::to_bytes

    fn ByteSink::to_bytes(self : ByteSink) -> Bytes

    Freeze the sink into immutable bytes.

    ByteSink::write_bytes

    fn ByteSink::write_bytes(self : ByteSink, view : BytesView) -> Unit

    Append every byte of view.

    ByteSink::write_u32

    fn ByteSink::write_u32(self : ByteSink, value : UInt) -> Unit

    Append four bytes, most significant first.

    ByteSink::write_u64

    fn ByteSink::write_u64(self : ByteSink, value : UInt64) -> Unit

    Append eight bytes, most significant first.

    ByteSink::write_u8

    fn ByteSink::write_u8(self : ByteSink, value : Int) -> Unit

    Append one byte, keeping the low eight bits of value.

    ByteSink::write_varint

    fn ByteSink::write_varint(self : ByteSink, value : UInt64) -> Unit

    An unsigned LEB128 variable length integer. Used by the manifest so that a chunk length below 128 bytes costs one byte rather than four.

    ChunkManifest

    pub struct ChunkManifest {
    params : ChunkParams
    chunks : Array[ChunkRef]
    root : Bytes
    }

    The description of one version of a stream as a sequence of chunks.

    ChunkManifest::chunk_count

    fn ChunkManifest::chunk_count(self : ChunkManifest) -> Int

    ChunkManifest::encode

    fn ChunkManifest::encode(self : ChunkManifest) -> Bytes

    The DCHK encoding of this manifest.

    ChunkManifest::root_hex

    fn ChunkManifest::root_hex(self : ChunkManifest) -> String

    The root digest as hexadecimal text.

    ChunkManifest::total_length

    fn ChunkManifest::total_length(self : ChunkManifest) -> Int

    The total length of the described stream.

    ChunkManifest::unique_chunks

    fn ChunkManifest::unique_chunks(self : ChunkManifest) -> Int

    How many distinct chunk digests the manifest references. A version with many repeated chunks reports a number far below chunk_count.

    ChunkManifest::verify

    fn ChunkManifest::verify(self : ChunkManifest, input : BytesView) -> Bool

    Recompute the manifest of input with the same parameters and compare roots. A mismatch means the bytes are not the version this manifest describes.

    ChunkParams

    pub struct ChunkParams {
    min_size : Int
    max_size : Int
    mask_bits : Int
    }

    The parameters that decide how a stream is cut.

    mask_bits controls the average chunk length: a boundary is taken when the low mask_bits bits of the rolling hash are zero, so the expected length is 2^mask_bits before the minimum and maximum corrections apply.

    ChunkParams::default

    fn ChunkParams::default() -> ChunkParams

    Min 2 KiB, average 8 KiB, max 32 KiB: small enough that an edit only re-cuts a couple of neighbours, large enough that the manifest stays small.

    ChunkParams::new

    fn ChunkParams::new(min_size? : Int, max_size? : Int, mask_bits? : Int) -> ChunkParams raise FormatError

    Build parameters, rejecting combinations that cannot produce sane chunks.

    ChunkRef

    pub struct ChunkRef {
    digest : Bytes
    length : Int
    }

    One chunk of a manifest: its SHA-256 digest and its length.

    ChunkStore

    pub struct ChunkStore {
    chunks : Map[String, Bytes]
    references : Map[String, Int]
    total_bytes : Int
    unique_bytes : Int
    }

    A content addressed store: chunks keyed by digest, which is what makes a second, similar version cost only the chunks that actually changed.

    ChunkStore::assemble

    fn ChunkStore::assemble(self : ChunkStore, manifest : ChunkManifest) -> Bytes raise FormatError

    Rebuild the bytes described by manifest from the chunks in the store.

    ChunkStore::ingest

    fn ChunkStore::ingest(self : ChunkStore, input : BytesView, params? : ChunkParams) -> IngestReport

    Split input, add every chunk that is not already stored, and return the manifest plus what changed.

    ChunkStore::new

    fn ChunkStore::new() -> ChunkStore

    ChunkStore::saved_bytes

    fn ChunkStore::saved_bytes(self : ChunkStore) -> Int

    The bytes that deduplication avoided: what the versions contained minus what the store actually keeps.

    ChunkStore::stored_bytes

    fn ChunkStore::stored_bytes(self : ChunkStore) -> Int

    The sum of the lengths of the distinct chunks.'

    ChunkStore::total_bytes_ingested

    fn ChunkStore::total_bytes_ingested(self : ChunkStore) -> Int

    The sum of the lengths of every version ingested so far.

    ChunkStore::unique_chunks

    fn ChunkStore::unique_chunks(self : ChunkStore) -> Int

    How many distinct chunks the store holds.

    Delta

    pub struct Delta {
    block_size : Int
    strong_len : Int
    new_length : Int
    content_digest : Bytes
    ops : Array[DeltaOp]
    }

    A sequence of operations that turns the old data into the new data.

    Delta::copied_blocks

    fn Delta::copied_blocks(self : Delta) -> Int

    The number of blocks the delta copies from the old data, counting a run as its length rather than as one operation.

    Delta::copied_bytes

    fn Delta::copied_bytes(self : Delta) -> Int

    How many bytes the delta carries as copy operations rather than literals.

    Delta::copy_ops

    fn Delta::copy_ops(self : Delta) -> Int

    The number of copy operations (single or run) in this delta.

    Delta::digest_hex

    fn Delta::digest_hex(self : Delta) -> String

    The SHA-256 of the DDLT encoding.

    Delta::encode

    fn Delta::encode(self : Delta) -> Bytes

    The DDLT encoding of this delta.

    Delta::literal_bytes

    fn Delta::literal_bytes(self : Delta) -> Int

    The number of bytes carried as literals.'

    Delta::op_count

    fn Delta::op_count(self : Delta) -> Int

    The number of operations in the delta.

    DeltaOp

    pub enum DeltaOp {
    Copy(Int, Bytes)
    CopyRun(Int, Int, Bytes)
    Literal(Bytes)
    }

    One operation of a delta.

    FileSignature

    pub struct FileSignature {
    block_size : Int
    strong_len : Int
    total_length : Int
    blocks : Array[BlockSignature]
    }

    The signature of a whole byte string.

    FileSignature::block_count

    fn FileSignature::block_count(self : FileSignature) -> Int

    FileSignature::block_length

    fn FileSignature::block_length(self : FileSignature, index : Int) -> Int?

    The length of the block at index, which is block_size for every block but the last one. Returns None when the index is outside the signature.

    FileSignature::digest_hex

    fn FileSignature::digest_hex(self : FileSignature) -> String

    The SHA-256 of the DSIG encoding, which is what the reports quote so two signatures can be compared without comparing every block.

    FileSignature::encode

    fn FileSignature::encode(self : FileSignature) -> Bytes

    The DSIG encoding of this signature.

    IngestReport

    pub struct IngestReport {
    total_bytes : Int
    chunk_count : Int
    new_chunks : Int
    reused_chunks : Int
    unique_bytes : Int
    manifest : ChunkManifest
    }

    What one ingest call changed in the store.

    SnapshotReport

    pub struct SnapshotReport {
    total_bytes : Int
    chunk_count : Int
    unique_chunks : Int
    new_chunks : Int
    reused_chunks : Int
    stored_bytes : Int
    saved_bytes : Int
    manifest_bytes : Int
    dedup_ppm : Int
    root_hex : String
    }

    What one snapshot added to a store, in the shape a report wants it.

    TransferPlan

    pub struct TransferPlan {
    signature : FileSignature
    delta : Delta
    new_length : Int
    delta_bytes : Int
    }

    The result of planning one transfer: the delta plus the numbers that make the saving visible.

    TransferPlan::copied_bytes

    fn TransferPlan::copied_bytes(self : TransferPlan) -> Int

    TransferPlan::copy_ops

    fn TransferPlan::copy_ops(self : TransferPlan) -> Int

    TransferPlan::delta

    fn TransferPlan::delta(self : TransferPlan) -> Delta

    TransferPlan::delta_bytes

    fn TransferPlan::delta_bytes(self : TransferPlan) -> Bytes

    The encoded delta, ready to be stored or sent.

    TransferPlan::literal_bytes

    fn TransferPlan::literal_bytes(self : TransferPlan) -> Int

    TransferPlan::new_length

    fn TransferPlan::new_length(self : TransferPlan) -> Int

    TransferPlan::savings_ppm

    fn TransferPlan::savings_ppm(self : TransferPlan) -> Int

    The saving as parts per million: (new_length - delta_bytes) / new_length.

    Returned as an integer on purpose. A floating point ratio would print differently on different backends in the last digit, and a report that cannot be reproduced byte for byte is not much use as evidence.

    TransferPlan::signature

    fn TransferPlan::signature(self : TransferPlan) -> FileSignature

    WeakRoll

    pub struct WeakRoll {
    a : UInt
    b : UInt
    window : UInt
    }

    A sliding window over a byte stream whose weak checksum can be updated in constant time when one byte leaves and one enters.

    The window length never changes, so value stays comparable between positions of the same stream and with the checksums stored in a signature.

    WeakRoll::new

    fn WeakRoll::new(view : BytesView) -> WeakRoll

    Build a rolling window whose initial position is the whole of view.

    WeakRoll::reset

    fn WeakRoll::reset(self : WeakRoll, view : BytesView) -> Unit

    Re-point the window at a completely new position, recomputing from scratch. Used after a copy, where the next window is not one byte away from the last.

    WeakRoll::roll

    fn WeakRoll::roll(self : WeakRoll, leaving : Byte, entering : Byte) -> Unit

    Slide the window one byte to the right: leaving drops out of the front, entering arrives at the back. Both arguments must be the actual bytes at the two edges, otherwise the checksum stops matching weak_checksum.

    WeakRoll::value

    fn WeakRoll::value(self : WeakRoll) -> UInt

    The checksum of the current window, laid out as (b << 16) | a.

    apply_delta

    fn apply_delta(old : BytesView, delta : Delta) -> ApplyResult raise FormatError

    Apply delta to old, verifying the strong checksum of every copy.

    The result is byte identical to the data the encoder started from when the old data is the same version the signature described. Any other combination raises BlockMismatch or BadField rather than returning plausible bytes.

    be_u32

    fn be_u32(value : UInt) -> Bytes

    The four big endian bytes of a u32.

    be_u64

    fn be_u64(value : UInt64) -> Bytes

    The eight big endian bytes of a u64.

    block_count

    fn block_count(total : Int, block_size : Int) -> Int

    The number of blocks of block_size bytes needed to hold total bytes, rounded up. A total length of zero needs zero blocks.

    chunk_boundaries

    fn chunk_boundaries(input : BytesView, params? : ChunkParams) -> Array[(Int, Int)]

    The cut points of input as half open (start, end) pairs.

    The boundaries depend only on the bytes since the previous boundary, capped by min_size and max_size, so an edit only affects the chunks around it.

    chunk_manifest

    fn chunk_manifest(input : BytesView, params? : ChunkParams) -> ChunkManifest

    Split input and describe the result.

    compute_delta

    fn compute_delta(new_bytes : BytesView, signature : FileSignature) -> Delta raise FormatError

    Build the delta that turns the signed old data into new_bytes.

    The result is normalised: consecutive copies are folded into one CopyRun, consecutive literals into one Literal, and the bytes covered by the operations equal new_bytes.length().

    constant_time_equal

    fn constant_time_equal(left : BytesView, right : BytesView) -> Bool

    Byte wise equality that does not stop at the first difference.

    Used when comparing strong checksums: a mismatch should not tell a peer where in the digest the difference starts.

    decode_delta

    fn decode_delta(bytes : BytesView) -> Delta raise FormatError

    Parse a DDLT byte string.

    decode_manifest

    fn decode_manifest(bytes : BytesView) -> ChunkManifest raise FormatError

    Parse a DCHK byte string.

    decode_signature

    fn decode_signature(bytes : BytesView) -> FileSignature raise FormatError

    Parse a DSIG byte string.

    Everything a sender could get wrong is checked here: the magic, the version, the strong length, and the arithmetic that ties block_count to total_length and block_size. A signature that parses is a signature whose block boundaries the delta encoder can trust.

    default_block_size

    fn default_block_size(length : Int) -> Int

    The block size used when the caller does not choose one.

    Small inputs get small blocks so a one byte edit on a small file still produces a small delta; large inputs get bigger blocks so that the signature and the delta op list stay proportional to length / 8192 rather than to length. The result is always a power of two inside [min_block_size, max_block_size], which keeps the format predictable.

    hex_decode

    fn hex_decode(text : String) -> Bytes?

    Parse lower or upper case hexadecimal text. An odd length, or a character that is not a hexadecimal digit, is a malformed input rather than a best effort: silently dropping a nibble would make checksums meaningless.

    hex_encode

    fn hex_encode(view : BytesView) -> String

    Lower case hexadecimal text for view, two characters per byte.

    ingest_snapshot

    fn ingest_snapshot(store : ChunkStore, input : BytesView, params? : ChunkParams) -> SnapshotReport

    Ingest one version into store and summarise what it cost.

    json_string

    fn json_string(text : String) -> String

    Quote a JSON string, escaping the characters that must be escaped.

    plan_from_signature

    fn plan_from_signature(signature : FileSignature, new_bytes : BytesView) -> TransferPlan raise FormatError

    Compute a delta against a signature that was produced earlier, which is what a server does when it keeps the client's signature between requests.

    plan_transfer

    fn plan_transfer(old : BytesView, new_bytes : BytesView, block_size? : Int, strong_len? : Int) -> TransferPlan raise FormatError

    Sign old and compute the delta from it to new_bytes.

    ppm_text

    fn ppm_text(ppm : Int) -> String

    A ratio expressed in parts per million as fixed four decimal text, e.g. 0.9992. Integer arithmetic only, so the same input prints the same text on every backend.

    read_u32

    fn read_u32(view : BytesView, offset : Int) -> UInt?

    Read four big endian bytes at offset, or None when they do not fit.

    read_u64

    fn read_u64(view : BytesView, offset : Int) -> UInt64?

    Read eight big endian bytes at offset, or None when they do not fit.

    read_u8

    fn read_u8(view : BytesView, offset : Int) -> Int?

    Read one unsigned byte at offset, or None when it is outside the view.

    read_varint

    fn read_varint(view : BytesView, offset : Int) -> (UInt64, Int)?

    Read an unsigned LEB128 integer at offset, returning the value and the offset just after it. At most ten bytes are accepted; a longer encoding is a malformed input, not a number.

    render_apply_json

    fn render_apply_json(result : ApplyResult) -> String

    One JSON line describing an applied delta.

    render_chunk_json

    fn render_chunk_json(report : SnapshotReport, path : String) -> String

    One JSON line describing one snapshot ingested into a chunk store.

    render_delta_json

    fn render_delta_json(plan : TransferPlan, old_path : String, new_path : String) -> String

    One JSON line describing the plan for one transfer.

    render_error_json

    fn render_error_json(code : String, message : String) -> String

    One JSON line describing a failure. The command line prints this instead of a stack trace, and exits non zero.

    render_plan_markdown

    fn render_plan_markdown(plan : TransferPlan) -> String

    The same summary as a Markdown table, for a pull request comment or a report.

    render_plan_text

    fn render_plan_text(plan : TransferPlan) -> String

    A human readable summary of one transfer, for a terminal or a log line.

    render_signature_json

    fn render_signature_json(signature : FileSignature, path : String, content_sha256_hex : String, content_xxh64_hex : String) -> String

    One JSON line describing a signature.

    sha256

    fn sha256(view : BytesView) -> Bytes

    The SHA-256 digest of view, as 32 bytes.

    sha256_hex

    fn sha256_hex(view : BytesView) -> String

    The SHA-256 digest of view as lower case hexadecimal text.

    sha256_prefix

    fn sha256_prefix(view : BytesView, length : Int) -> Bytes

    The first length bytes of the SHA-256 digest of view.

    A signature stores a truncated digest: 8 or 16 bytes is enough to make an accidental match impossible while keeping signatures small, and the weak checksum already filtered the candidates. Truncation is a format decision, so it lives here rather than in the caller.

    sign_bytes

    fn sign_bytes(input : BytesView, block_size? : Int, strong_len? : Int) -> FileSignature raise FormatError

    Sign input block by block. The last block may be shorter than block_size.

    signature_matches

    fn signature_matches(signature : FileSignature, old : BytesView) -> Bool

    Check that old really is the data the signature describes, block by block. Useful when a signature is cached between requests.

    sub_view

    fn sub_view(view : BytesView, offset : Int, length : Int) -> BytesView?

    A bounds checked sub view, or None when the range is not fully inside.

    verified_update

    fn verified_update(old : BytesView, delta_bytes : BytesView) -> ApplyResult raise FormatError

    Decode a delta, apply it and verify every copy. This is the only way a receiver is expected to use a delta.

    weak_checksum

    fn weak_checksum(view : BytesView) -> UInt

    The weak checksum of an entire slice, taken as one window.

    xxh64

    fn xxh64(view : BytesView) -> UInt64

    The XXH64 fingerprint of view with seed zero.

    This is a fast non cryptographic hash: it detects accidental change and it makes reports readable. It must never be the reason two byte strings are considered equal, which is what sha256 and constant_time_equal are for.

    xxh64_hex

    fn xxh64_hex(view : BytesView) -> String

    The XXH64 fingerprint as sixteen lower case hexadecimal characters, which is how the reports spell it.