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
157 lines
5.6 KiB
C
157 lines
5.6 KiB
C
/*
|
|
* demo.c — a runnable, human-readable demonstration of PackFS, not another
|
|
* automated test. Run it and read the output; each step prints what it did
|
|
* and what it found, so the library's core behaviors (mem backend, a
|
|
* pack-backed overlay with copy-up and whiteout, compaction, persistence
|
|
* across a re-open, and a sandboxed dir mount) are visible directly,
|
|
* without reading test assertions.
|
|
*
|
|
* Usage: packfs_demo [pack-file-path]
|
|
* (defaults to /tmp/packfs_demo.pack; re-run it to see the previous run's
|
|
* data survive via the pack file written by the first run's vfs_sync.)
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "packfs.h"
|
|
|
|
static void die(const char *what, int err) {
|
|
fprintf(stderr, "FAILED: %s (error code %d)\n", what, err);
|
|
exit(1);
|
|
}
|
|
|
|
static void print_stat(Vfs *v, const char *path) {
|
|
VfsStat st;
|
|
int rc = vfs_stat(v, path, &st);
|
|
if (rc != VFS_OK) {
|
|
printf(" stat %-20s -> error %d\n", path, rc);
|
|
return;
|
|
}
|
|
printf(" stat %-20s -> %s, size=%llu\n", path,
|
|
st.kind == VFS_KIND_DIR ? "dir " : "file",
|
|
(unsigned long long)st.size);
|
|
}
|
|
|
|
static void list_dir(Vfs *v, const char *path) {
|
|
VfsDir dir;
|
|
int rc = vfs_readdir(v, path, &dir);
|
|
if (rc != VFS_OK) {
|
|
printf(" readdir %s -> error %d\n", path, rc);
|
|
return;
|
|
}
|
|
printf(" readdir %s -> %zu entr%s:\n", path, (size_t)dir.count, dir.count == 1 ? "y" : "ies");
|
|
for (pfs_usize i = 0; i < dir.count; i++) {
|
|
printf(" %s%s\n", dir.entries[i].name, dir.entries[i].kind == VFS_KIND_DIR ? "/" : "");
|
|
}
|
|
vfs_dir_free(&dir);
|
|
}
|
|
|
|
static void write_file(Vfs *v, const char *path, const char *content) {
|
|
int err = 0;
|
|
VfsFile *f = vfs_open(v, path, VFS_O_WRONLY | VFS_O_CREAT | VFS_O_TRUNC, &err);
|
|
if (!f) die(path, err);
|
|
pfs_usize len = (pfs_usize)strlen(content);
|
|
if (vfs_write(f, content, len) != (pfs_isize)len) die("short write", VFS_ERR_IO);
|
|
vfs_close(f);
|
|
printf(" wrote %-20s (%zu bytes)\n", path, (size_t)len);
|
|
}
|
|
|
|
static void read_file(Vfs *v, const char *path) {
|
|
int err = 0;
|
|
VfsFile *f = vfs_open(v, path, VFS_O_RDONLY, &err);
|
|
if (!f) { printf(" read %-20s -> error %d\n", path, err); return; }
|
|
char buf[256] = {0};
|
|
pfs_isize n = vfs_read(f, buf, sizeof(buf) - 1);
|
|
vfs_close(f);
|
|
printf(" read %-20s -> \"%.*s\"\n", path, (int)n, buf);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *pack_path = argc > 1 ? argv[1] : "/tmp/packfs_demo.pack";
|
|
|
|
printf("=== PackFS demo: pack-backed overlay at %s ===\n", pack_path);
|
|
printf("(if this file already exists from a previous run, its contents\n"
|
|
" should reappear below, proving compaction + reload persistence)\n\n");
|
|
|
|
Vfs *v = vfs_new();
|
|
Backend *mem = backend_mem_new();
|
|
int oerr = 0;
|
|
Backend *overlay = backend_overlay_new(pack_path, mem, &oerr);
|
|
if (!overlay) die("backend_overlay_new", oerr);
|
|
if (vfs_mount(v, "/", overlay) != VFS_OK) die("vfs_mount /", 0);
|
|
|
|
printf("-- initial state (from the pack file, if one existed) --\n");
|
|
list_dir(v, "/");
|
|
|
|
printf("\n-- writing files --\n");
|
|
write_file(v, "/hello.txt", "Hello from PackFS!");
|
|
write_file(v, "/config.json", "{\"greeting\":\"hi\"}");
|
|
if (vfs_mkdir(v, "/notes") != VFS_OK) printf(" (/notes already existed)\n");
|
|
else printf(" mkdir /notes\n");
|
|
write_file(v, "/notes/todo.txt", "buy milk");
|
|
|
|
printf("\n-- reading back --\n");
|
|
read_file(v, "/hello.txt");
|
|
read_file(v, "/notes/todo.txt");
|
|
|
|
printf("\n-- directory listing --\n");
|
|
list_dir(v, "/");
|
|
list_dir(v, "/notes");
|
|
|
|
printf("\n-- stat --\n");
|
|
print_stat(v, "/hello.txt");
|
|
print_stat(v, "/notes");
|
|
print_stat(v, "/does-not-exist.txt");
|
|
|
|
printf("\n-- copy-up + whiteout: deleting a file that lives in the pack --\n");
|
|
/* On the second run, /hello.txt (if it survived compaction) lives in
|
|
* the read-only pack layer; this unlink is served by a whiteout
|
|
* (Section 4.2), not a mutation of the pack. */
|
|
int rc = vfs_unlink(v, "/config.json");
|
|
printf(" unlink /config.json -> %s\n", rc == VFS_OK ? "ok (whiteout or plain removal)" : "error");
|
|
list_dir(v, "/");
|
|
|
|
printf("\n-- compacting the overlay into a fresh pack (vfs_sync) --\n");
|
|
if (vfs_sync(v, "/") != VFS_OK) die("vfs_sync", 0);
|
|
printf(" wrote %s\n", pack_path);
|
|
|
|
vfs_unmount(v, "/");
|
|
backend_free(overlay);
|
|
backend_free(mem);
|
|
vfs_free(v);
|
|
|
|
printf("\n=== sandboxed dir mount demo ===\n");
|
|
{
|
|
char tmpl[] = "/tmp/packfs_demo_sandbox_XXXXXX";
|
|
char *sandbox = mkdtemp(tmpl);
|
|
if (!sandbox) die("mkdtemp", 0);
|
|
printf("sandbox root: %s\n", sandbox);
|
|
|
|
Vfs *v2 = vfs_new();
|
|
int derr = 0;
|
|
Backend *dir = backend_dir_new(sandbox, &derr);
|
|
if (!dir) die("backend_dir_new", derr);
|
|
vfs_mount(v2, "/data", dir);
|
|
|
|
write_file(v2, "/data/inside.txt", "this file is contained to the sandbox root");
|
|
read_file(v2, "/data/inside.txt");
|
|
|
|
printf(" attempting /data/../../../etc/passwd (must be rejected) ...\n");
|
|
int err2 = 0;
|
|
VfsFile *escape = vfs_open(v2, "/data/../../../etc/passwd", VFS_O_RDONLY, &err2);
|
|
printf(" -> %s (error code %d)\n", escape ? "UNEXPECTEDLY OPENED (bug!)" : "rejected, as required", err2);
|
|
if (escape) vfs_close(escape);
|
|
|
|
vfs_unmount(v2, "/data");
|
|
backend_free(dir);
|
|
vfs_free(v2);
|
|
}
|
|
|
|
printf("\n=== done ===\n");
|
|
printf("Re-run this program (same pack path) to see /hello.txt and\n"
|
|
"/notes/todo.txt survive, and /config.json stay deleted.\n");
|
|
return 0;
|
|
}
|