Skip to content

Commit 4e72e10

Browse files
committed
Initial commit: cowdiff, reflink-aware incremental binary diff
0 parents  commit 4e72e10

28 files changed

Lines changed: 4814 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
jobs:
9+
check:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-go@v5
14+
with:
15+
go-version-file: go.mod
16+
- name: gofmt
17+
run: |
18+
out=$(gofmt -l .)
19+
if [ -n "$out" ]; then echo "gofmt needed on:"; echo "$out"; exit 1; fi
20+
- name: vet
21+
run: go vet ./...
22+
- name: build
23+
run: go build ./...
24+
- name: cross-build (darwin)
25+
run: GOOS=darwin GOARCH=arm64 go build ./...

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Built binaries
2+
/cowdiff
3+
*.exe
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binaries (go test -c), coverage & profiling output
9+
*.test
10+
*.out
11+
*.prof
12+
*.cover
13+
14+
# Go workspace (local; not committed for a library)
15+
go.work
16+
go.work.sum
17+
18+
# Editors / IDEs
19+
.idea/
20+
.vscode/
21+
*.swp
22+
*.swo
23+
*~
24+
25+
# OS junk
26+
.DS_Store
27+
Thumbs.db

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
3+
## [0.1.0] - 2026-07-17
4+
5+
Initial release.
6+
7+
- reflink-aware incremental binary diff of large files on CoW filesystems.
8+
- CLI: `checkpoint`, `diff`, `apply`, `merge`, `info`, `verify`.
9+
- Two diff modes -- `reflink` (FIEMAP) and `content` -- sharing one object format.
10+
- Go API: `Diff`, `Apply`, `ApplyTo`, `Reconstruct`, `Merge`, `Checkpoint`, `ReadHeader`, `Verify`.
11+
- Atomic writes, input/output alias guards, bounded-memory `Verify`.
12+
- Single external dependency (`golang.org/x/sys`); Linux-only paths behind build tags.
13+
14+
[0.1.0]: https://github.com/lf4096/cowdiff/releases/tag/v0.1.0

CONTRIBUTING.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Contributing
2+
3+
Issues and pull requests are welcome.
4+
5+
## Build and test
6+
7+
```sh
8+
go build ./...
9+
go vet ./...
10+
gofmt -l . # should print nothing
11+
go test ./...
12+
```
13+
14+
- The `checkpoint`, `diff --mode reflink`, and FIEMAP tests need **Linux + a CoW filesystem** (XFS reflink=1, Btrfs, bcachefs); they skip automatically elsewhere. To run them, point `TMPDIR` at a CoW filesystem and set `COWDIFF_REQUIRE_REFLINK=1` (fail instead of skip when reflink is unavailable).
15+
- Random-test breadth is tunable: `COWDIFF_FUZZ_CHAINS=N` (content chains) and `COWDIFF_FUZZ_REFLINK=N` (reflink chains).
16+
17+
## Code style
18+
19+
- `gofmt` and `go vet` must be clean before submitting.
20+
- Code and comments are in English; exported identifiers carry doc comments.
21+
- Keep the dependency surface minimal (currently only `golang.org/x/sys`).
22+
23+
## Pull requests
24+
25+
- Keep a PR focused on one thing, with tests covering the change.
26+
- Explain the motivation; if you change the diff object format, update [DESIGN.md](./DESIGN.md).

DESIGN.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# cowdiff - Design
2+
3+
This document describes cowdiff's object format, operation semantics, and design rationale. For installation, commands, and typical usage see the [README](./README.md); for the full API see [godoc](https://pkg.go.dev/github.com/lf4096/cowdiff).
4+
5+
## 1. What it solves
6+
7+
Given a large file `B` (base) and a later version `N`, produce a compact **diff object** `D` such that `apply(B, D)` reconstructs `N` byte for byte. When `N` is a reflink clone of `B`, only the changed regions are read (via `FIEMAP`) -- no full scan -- and the result is **always correct**.
8+
9+
This is the primitive behind incremental backup, snapshot chains, and cross-machine restore -- without relying on a filesystem's native `send`/`receive`, and without a full scan per snapshot.
10+
11+
## 2. Goals / non-goals
12+
13+
**Goals**
14+
15+
- Cheap incremental diff of reflink descendants (read only the changed extents via `FIEMAP`, no full scan).
16+
- A correct, portable fallback (content comparison) when reflink lineage is unavailable.
17+
- Self-contained diff objects: `apply(base, diff)` is deterministic and filesystem-independent.
18+
- Chain operations: apply a chain, merge/compact a chain.
19+
- Reflink-accelerated apply (clone base, patch only the changed regions).
20+
- A minimal dependency surface; usable both as a CLI (Unix filter) and as a Go library.
21+
22+
**Non-goals**
23+
24+
- **Storage and transport.** The tool reads and writes files and diff streams; wiring them to any backend is the caller's job.
25+
- **Chain bookkeeping.** Parent relationships, retention policy, garbage collection, fork/branch reachability -- that is a manifest the caller maintains. A diff object carries only an optional `from_hash` for **verification**.
26+
- **Lazy read-through.** Restore materializes the full file. If you need to run directly on a backing chain without materializing, use a format built for that (qcow2 backing chain, or a FUSE overlay).
27+
- **Online locking / consistency.** The tool assumes its input files are quiescent (see section 7); it does not freeze or lock.
28+
29+
## 3. Concepts
30+
31+
- **base** -- the level-0 baseline of a chain: the full content of the working file at the chain's start (a plain file), the root of restore. It has no parent.
32+
- **diff** -- an incremental object relative to a single predecessor (the base, or the result of another diff).
33+
- **chain** -- `base -> D1 -> D2 -> ... -> Dk`. `apply(base, D1..Dk)` yields the state after `Dk`.
34+
- **checkpoint** -- a reflink clone (see section 6), O(1) and almost free in space. It is used both to build a base (kept as the chain root, stored) and as the moving baseline for each increment (local, only the latest kept).
35+
- **mode** -- `reflink` (the FIEMAP fast path) and `content` (the portable fallback); both produce the same format (see section 5).
36+
37+
## 4. Diff object format
38+
39+
A diff object is a single self-contained binary blob laid out so that **the directory precedes the data**. This lets a consumer cheaply read all the headers of a chain, work out which layer each byte range comes from, and then fetch only the data it actually needs.
40+
41+
```
42+
+-----------------------------------------------------------+
43+
| Header |
44+
| magic 8 "COWDIFF\x01" |
45+
| version u32 |
46+
| flags u32 bit0 has_from_hash |
47+
| target_size u64 file size after apply (grow/shrink) |
48+
| seg_count u64 |
49+
| from_hash 32? full-file hash the diff applies onto|
50+
+-----------------------------------------------------------+
51+
| Segment directory (seg_count entries, 25 bytes each) |
52+
| offset u64 logical offset in the target |
53+
| length u64 |
54+
| type u8 0 = DATA, 1 = ZERO (hole/discard) |
55+
| data_off u64 byte offset into the Data section |
56+
| (unused for ZERO segments) |
57+
+-----------------------------------------------------------+
58+
| Data section |
59+
| concatenated bytes of all DATA segments, in directory |
60+
| order |
61+
+-----------------------------------------------------------+
62+
| Trailer |
63+
| checksum 32 SHA-256 of header + directory + data |
64+
+-----------------------------------------------------------+
65+
```
66+
67+
All integers are little-endian. Design points and their reasons:
68+
69+
- **Directory before data** -- headers stream cheaply, restore can be optimized (skip lower layers that are fully overwritten), and only the needed data ranges are fetched.
70+
- **Segment `type` DATA / ZERO** -- trim/discard and sparse ranges are represented compactly (a zeroed 1 GiB range is one ZERO segment, not a gibibyte of zeros) and applied correctly (punch hole / write zeros).
71+
- **`target_size`** -- the file may grow or shrink between versions; apply truncates/extends to `target_size`.
72+
- **`from_hash`** -- optional: the hash of the **full file content** this diff applies onto (the whole-file state before applying). It lets the **caller** verify a diff is applied to the intended baseline -- by comparing it to the baseline's known hash; the tool merely **carries** the field (exposed via `ReadHeader`) and **does not enforce it at apply time**, because the tool never computes hashes (doing so would require a full read of base, defeating reflink's read-only-changed-extents property). It stores the **file state**, not the hash of some diff object, so it is **invariant under compaction** (merge / roll-forward do not change the reconstructed state). Caller-supplied (content-addressed storage usually already has it), omitted by default, and not required for reconstruction.
73+
- **`checksum`** -- the object's own integrity (detects corruption in transit or storage).
74+
75+
A diff object is a **single** self-contained blob: the trailer must be followed by end of stream (EOF), which lets a consumer reject trailing junk or a second concatenated object; each reader carries exactly one object. `Verify` reads in bounded memory; `Apply` (writing a file) patches per diff in a streaming fashion, while `ApplyTo`/`Reconstruct`/`Merge` buffer the participating diffs' data sections in memory (a diff is usually far smaller than base, but a full-change diff equals the whole file size).
76+
77+
The format deliberately stays close to a minimal, documented block-level diff (offset/length/data records with metadata) rather than a general delta codec.
78+
79+
## 5. The two modes and their compatibility
80+
81+
`reflink` compares the physical extents of `N` and `B` (via FIEMAP); diverging extents are candidates, and only the changed parts are read. `content` compares the full contents of the two files. The former requires a CoW filesystem and reflink lineage between `N` and `B`; the latter has no prerequisites.
82+
83+
The key point is that both **produce the same object format** and both guarantee `apply(B, D) == N`, so they are fully interchangeable:
84+
85+
- A `reflink`-mode diff covers a **superset** of the truly changed ranges (an extent moved physically but identical in content -- e.g. after defrag -- is also included; its bytes equal `B`'s, so applying it is a no-op). In the normal case (no movement between snapshots) it is in fact minimal.
86+
- Because neither mode ever **misses** a change and both store the new bytes for every range they include, a chain may freely mix diffs from both modes; `apply` and `merge` treat them identically.
87+
88+
`reflink` is the fast path when its prerequisites hold; `content` is the always-correct fallback (lineage lost, non-CoW filesystem, a baseline materialized on another machine) and the ground truth for verification.
89+
90+
FIEMAP is only a **hint**: the implementation compares physical offsets to get candidate ranges, but for each candidate range it still reads the actual bytes from `N` into the diff, so correctness does not depend on physical-offset semantics that may vary across filesystems. An extent flagged `UNKNOWN`/`DELALLOC`/`ENCODED` (or inline/tail-packed) is treated as "changed" and read from `N` (conservative but correct).
91+
92+
## 6. reflink checkpoint -- why a related baseline is needed
93+
94+
The `reflink` fast path requires base and new to **share physical extents** -- so that "physically shared means unchanged" holds. A plain `cp` cannot give this: it produces an independent copy with its own blocks, so `FIEMAP` reports every extent as changed. A reflink clone (FICLONE / `cp --reflink=always`) is O(1) metadata sharing all extents; only ranges written afterward copy-on-write split and consume space. This is exactly what the `checkpoint` command does: perform FICLONE and verify the result really shares extents.
95+
96+
**Gotcha:** modern `cp` defaults to `--reflink=auto`, which on a non-reflink filesystem **silently** degrades to a full copy -- leaving you an independent file and a next diff quietly downgraded to a full scan. Always use `--reflink=always` or `cowdiff checkpoint` (the latter verifies sharing and errors if not shared).
97+
98+
Diffing between two **frozen** checkpoints (rather than against a live file) means the working file can keep changing while the diff is computed. A checkpoint is local; losing one (eviction, crash) is recoverable: materialize that state from the durable chain, do one `content`-mode diff, and re-establish a checkpoint.
99+
100+
## 7. Consistency
101+
102+
cowdiff operates on whatever bytes an input file currently holds; it does not freeze or lock. Presenting a consistent point is the caller's responsibility:
103+
104+
- **Crash-consistent**: `FICLONE` captures the file's extents at an instant -- equivalent to a power cut at that moment for whatever is writing it.
105+
- **Clean-consistent** (no recovery needed afterward): briefly quiesce the writer (pause or flush) before `FICLONE`, then resume -- usually a sub-second stall.
106+
107+
Because diffs are taken between **frozen** checkpoints, the live file can keep changing throughout.
108+
109+
## 8. Correctness guarantees
110+
111+
- `apply(base, diff) == new`, byte for byte, in both modes.
112+
- `apply(base, D1..Dk) == apply(base, merge(D1..Dk))`.
113+
- apply is filesystem- and machine-independent (a diff is logical offset/length/bytes); the restoring side needs neither reflink nor XFS.
114+
- Objects carry a content checksum; the optional `from_hash` lets the **caller** verify apply lands on the right baseline (the tool carries it, does not enforce it; invariant under compaction).
115+
- fork/rollback is just several diffs sharing a parent; supported at the object layer (the caller's manifest tracks reachability for GC).
116+
- Sparse ranges, discard/trim, and file grow/shrink are represented and applied correctly (ZERO segments + `target_size`).
117+
118+
## 9. Related work and standards
119+
120+
cowdiff deliberately reuses well-understood building blocks rather than inventing new mechanisms:
121+
122+
- **FIEMAP / filefrag** (`e2fsprogs`) -- the authoritative way to read a file's physical extents; the basis of reflink mode.
123+
- **FICLONE / reflink** -- checkpoint and reflink-accelerated apply.
124+
125+
## 10. Dependencies and portability
126+
127+
The only external module is `golang.org/x/sys/unix` (FICLONE, FICLONERANGE, punch-hole, pread/pwrite). A few deliberate choices:
128+
129+
- **FIEMAP hand-rolled.** x/sys has no FIEMAP, so this project wraps the ioctl directly (~50 lines) rather than pulling in an unmaintained third-party library.
130+
- **Checksum uses standard-library SHA-256.** `from_hash` is caller-supplied and never computed by the tool, so no extra hash library (such as BLAKE3) is needed.
131+
- **Linux-only paths isolated by build tags.** reflink / FIEMAP / punch-hole carry `//go:build linux`; other platforms substitute stubs that return a "requires Linux" error. `content`-mode diff, apply, merge, and format encode/decode are portable and usable/testable on any platform.

0 commit comments

Comments
 (0)