Master the fundamental concepts of file systems through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksCopy-on-write filesystems (ZFS, btrfs snapshots) never overwrite live blocks; they write new blocks and update pointers, leaving old trees reachable for snapshots.
On write to snapshotted file:
For example, volume at snapshot S0 shares 90% of blocks with live tree; after heavy writes, shared ratio drops as new branches diverge.
Btrfs, ZFS, and Apple's APFS all use exactly this copy-on-write-plus-reference-counting design to make snapshots instantaneous and clones nearly free, which is why zfs snapshot and Time Machine local snapshots return immediately even on multi-terabyte volumes. Forgetting to check the reference count before an in-place write is exactly the bug class that would corrupt a snapshot that's supposed to be immutable.
Before you call the implementation done, walk failure modes on purpose. Test empty structures, single-element edge cases, maximum concurrency, and errno paths that must not crash the program. OS code usually fails in production when happy-path tests pass but invariants break under contention or memory pressure.
Keep structures small and name fields after kernel counterparts when possible. That lets you read man pages and kernel source side by side while you work. Print observable events during development; remove noisy logs once tests pass reliably.
You will implement COW block pointers and create read-only snapshots referencing older roots. This exercise requires proving reads from an old snapshot see historical contents after live file changes.
Model a copy-on-write filesystem, the design behind Btrfs, ZFS and APFS snapshots. Data blocks are never overwritten: every write goes to a freshly allocated block and the file's block pointer is switched to it. Blocks carry reference counts, so a snapshot is just a copy of the metadata (the list of files and their block pointers) that bumps every refcount. A snapshot therefore costs no data copying, and blocks are shared until one side writes.
| Command | Effect | Output |
|---|---|---|
write FILE IDX TEXT... | write block IDX of FILE (creating the file if needed). IDX may be an existing block or exactly one past the end (append). Always allocate a new block holding TEXT (the rest of the line after one space); if IDX replaced an old block, decrement the old block's refcount. | write a.txt[1]: block 3 (old block 1 kept, 1 ref) / (old block 1 freed) / nothing extra for an append |
snapshot NAME | copy the live file table under NAME; +1 on every block it references | snapshot s1: 2 files, 3 blocks shared, 0 copied |
cat FILE / cat SNAP:FILE | concatenate the blocks' text | cat a.txt: "hello world" blocks 0 1 |
rm FILE | remove a live file; −1 on each of its blocks | rm b.txt: 0 blocks freed, 1 still referenced |
delsnap NAME | delete a snapshot; −1 on each block it referenced | delsnap s1: 2 blocks freed |
refs | every block with a non-zero count | refs: 0=2 1=1 ... or refs: none |
df | physical vs logical usage | df: 4 blocks used, 6 referenced by files and snapshots, 2 saved by sharing |
referenced counts block pointers in the live files and all snapshots.blocks freed counts blocks whose count dropped to 0.write FILE[IDX]: bad index (file has N blocks), write FILE[IDX]: no space, cat X: no such file, cat SNAP:FILE: no such snapshot, rm FILE: no such file, snapshot NAME: already exists, delsnap NAME: no such snapshot.blocks (1 blocks freed); only the write note says 1 ref / N refs.Input:
cLoading…
Output:
cLoading…
Hidden tests cover two snapshots sharing blocks with the live tree, writes that free old blocks once no snapshot holds them, deleting snapshots in a different order than they were taken, reading an old version from a snapshot after the live file changed, and every error.