Implements the core design: a mount table published as an atomically- swapped snapshot; mem/dir/pack/overlay backends; copy-on-write overlay with copy-up and whiteout deletion; a checksummed append journal; compaction with exact-duplicate elimination; single-writer/wait-free- reader concurrency with a structural/content write split; openat2/ Landlock path containment for dir mounts; and load-time pack integrity validation. Zero required third-party dependencies. Sanitizer testing (ASan/UBSan) caught and led to fixing a genuine heap-use-after-free in the snapshot-reclamation path: the textbook "load pointer, then increment its refcount" pattern left a gap a concurrent writer could free through. Closed with a small reclaim_gate rwlock, documented in internal.h and CLAUDE.md since it's a pattern every refcounted structure in the codebase now follows. zip/tar import/export backends, recommended in concept.md Section 11, will not be built — a permanent project decision recorded in CLAUDE.md since concept.md itself is frozen and cannot be edited to reflect it. Includes a runnable demo (examples/demo.c, `make demo`) exercising the library end to end and proving cross-run persistence through the pack file, plus open-source scaffolding: MIT license, README, CONTRIBUTING, and a CI workflow running the test suite under ASan/UBSan/TSan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
125 lines
4.2 KiB
C
125 lines
4.2 KiB
C
/* test_pack_overlay.c — overlay backend: copy-up, whiteout deletion of a
|
|
* lower-layer entry (Section 4.2), compaction (Section 4.1/5.3),
|
|
* cross-restart durability via journal replay (Section 4.4), and pack
|
|
* integrity validation rejecting a corrupted file (Section 7). */
|
|
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include "packfs.h"
|
|
#include "test_harness.h"
|
|
|
|
static char *tmp_pack_path(void) {
|
|
static char path[512];
|
|
snprintf(path, sizeof(path), "/tmp/packfs_test_pack_%d.img", (int)getpid());
|
|
return path;
|
|
}
|
|
|
|
int main(void) {
|
|
char *pack_path = tmp_pack_path();
|
|
unlink(pack_path);
|
|
char jpath[600];
|
|
snprintf(jpath, sizeof(jpath), "%s.jnl", pack_path);
|
|
unlink(jpath);
|
|
|
|
/* --- phase 1: build an initial pack directly (as if shipped) --- */
|
|
Vfs *v = vfs_new();
|
|
Backend *mem = backend_mem_new();
|
|
int oerr = 0;
|
|
Backend *ov = backend_overlay_new(pack_path, mem, &oerr);
|
|
CHECK(ov != NULL);
|
|
CHECK_EQ_INT(vfs_mount(v, "/", ov), VFS_OK);
|
|
|
|
int err = 0;
|
|
VfsFile *f = vfs_open(v, "/a.txt", VFS_O_WRONLY | VFS_O_CREAT, &err);
|
|
CHECK(f != NULL);
|
|
CHECK_EQ_INT(vfs_write(f, "AAAA", 4), 4);
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
|
|
f = vfs_open(v, "/dup.txt", VFS_O_WRONLY | VFS_O_CREAT, &err);
|
|
CHECK(f != NULL);
|
|
CHECK_EQ_INT(vfs_write(f, "AAAA", 4), 4); /* same content as a.txt: exercises dedup (Section 9.2) */
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
|
|
f = vfs_open(v, "/b.txt", VFS_O_WRONLY | VFS_O_CREAT, &err);
|
|
CHECK(f != NULL);
|
|
CHECK_EQ_INT(vfs_write(f, "BBBBBB", 6), 6);
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
|
|
CHECK_EQ_INT(vfs_sync(v, "/"), VFS_OK); /* compaction */
|
|
|
|
/* read-only open straight from the freshly-compacted pack */
|
|
f = vfs_open(v, "/a.txt", VFS_O_RDONLY, &err);
|
|
CHECK(f != NULL);
|
|
char buf[16] = {0};
|
|
CHECK_EQ_INT(vfs_read(f, buf, sizeof(buf)), 4);
|
|
CHECK_STR_EQ(buf, "AAAA");
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
|
|
/* --- copy-up on write intent, whiteout on delete (Section 4.2) --- */
|
|
f = vfs_open(v, "/a.txt", VFS_O_RDWR, &err); /* triggers copy-up */
|
|
CHECK(f != NULL);
|
|
CHECK_EQ_INT(vfs_write(f, "ZZZZ", 4), 4);
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
f = vfs_open(v, "/a.txt", VFS_O_RDONLY, &err);
|
|
memset(buf, 0, sizeof(buf));
|
|
CHECK_EQ_INT(vfs_read(f, buf, sizeof(buf)), 4);
|
|
CHECK_STR_EQ(buf, "ZZZZ");
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
|
|
CHECK_EQ_INT(vfs_unlink(v, "/b.txt"), VFS_OK); /* whiteout: b.txt only existed in the pack */
|
|
VfsStat st;
|
|
CHECK_EQ_INT(vfs_stat(v, "/b.txt", &st), VFS_ERR_NOENT);
|
|
|
|
/* --- durability: a fresh Vfs replays the journal without compacting --- */
|
|
vfs_unmount(v, "/");
|
|
backend_free(ov);
|
|
backend_free(mem);
|
|
vfs_free(v);
|
|
|
|
Vfs *v2 = vfs_new();
|
|
Backend *mem2 = backend_mem_new();
|
|
Backend *ov2 = backend_overlay_new(pack_path, mem2, &oerr);
|
|
CHECK(ov2 != NULL);
|
|
CHECK_EQ_INT(vfs_mount(v2, "/", ov2), VFS_OK);
|
|
|
|
CHECK_EQ_INT(vfs_stat(v2, "/b.txt", &st), VFS_ERR_NOENT); /* whiteout replayed */
|
|
f = vfs_open(v2, "/a.txt", VFS_O_RDONLY, &err);
|
|
CHECK(f != NULL);
|
|
memset(buf, 0, sizeof(buf));
|
|
CHECK_EQ_INT(vfs_read(f, buf, sizeof(buf)), 4);
|
|
CHECK_STR_EQ(buf, "ZZZZ"); /* the post-compaction write survived via the journal */
|
|
CHECK_EQ_INT(vfs_close(f), VFS_OK);
|
|
|
|
CHECK_EQ_INT(vfs_sync(v2, "/"), VFS_OK); /* compact again to fold the journal in */
|
|
vfs_unmount(v2, "/");
|
|
backend_free(ov2);
|
|
backend_free(mem2);
|
|
vfs_free(v2);
|
|
|
|
/* --- pack integrity validation (Section 7): a corrupted pack is rejected --- */
|
|
int fd = open(pack_path, O_RDWR);
|
|
CHECK(fd >= 0);
|
|
unsigned char one;
|
|
CHECK_EQ_INT(pread(fd, &one, 1, 40), 1); /* somewhere inside the header/offsets region */
|
|
one = (unsigned char)(one ^ 0xFF);
|
|
CHECK_EQ_INT(pwrite(fd, &one, 1, 40), 1);
|
|
close(fd);
|
|
|
|
Vfs *v3 = vfs_new();
|
|
Backend *mem3 = backend_mem_new();
|
|
int oerr3 = 0;
|
|
Backend *ov3 = backend_overlay_new(pack_path, mem3, &oerr3);
|
|
CHECK(ov3 == NULL);
|
|
CHECK_EQ_INT(oerr3, VFS_ERR_CORRUPT);
|
|
backend_free(mem3);
|
|
vfs_free(v3);
|
|
|
|
unlink(pack_path);
|
|
unlink(jpath);
|
|
TEST_MAIN_END();
|
|
}
|