Document every public function; fix a real gap the audit found
Cross-checked every function declared in include/packfs.h against nm -D libpackfs.so.0 as the starting point for a full documentation pass, per the request to document literally everything rather than just the parts already covered. That check found a genuine bug, not just a documentation gap: backend_pack_new was declared in the public header and named in CLAUDE.md's architecture map, but never implemented in src/pack.c — any caller would fail at link time. Implemented it as a standalone, read-only `pack` Backend (every mutating call returns VFS_ERR_PERM, consistent with concept.md Section 2.1 listing `pack` as its own backend kind distinct from the overlay), covered it with a new test case, and verified it under -fsanitize=undefined per CLAUDE.md's sanitizer rule. Added a doc comment to every previously-undocumented function and struct field in packfs.h and internal.h (vfs_open/read/write/close/ stat/readdir/mkdir/unlink/rename, every upper_* structural/content function, pfs_dir_*, pfs_fnv1a64, PackIndexEntry/Pack fields). Updated README and CLAUDE.md to mention backend_pack_new and to stop gesturing at zip/tar as though import/export exists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
This commit is contained in:
+78
-5
@@ -35,6 +35,9 @@ int pfs_path_under(const char *prefix, const char *path);
|
||||
|
||||
/* ---- hash.c ---- */
|
||||
|
||||
/* FNV-1a, 64-bit. Not cryptographic — used only for pack integrity
|
||||
* checksums (Section 7) and compaction's exact-duplicate detection
|
||||
* (Section 9.2), never where adversarial collision resistance matters. */
|
||||
uint64_t pfs_fnv1a64(const void *data, size_t len);
|
||||
|
||||
/* ---- containment.c: capability-scoped dir mounts (Section 6.2) ---- */
|
||||
@@ -53,9 +56,15 @@ int pfs_dir_capability_open(const char *path, int *err);
|
||||
* is unavailable (Section 6.3). `flags`/`mode` are plain open(2) flags.
|
||||
*/
|
||||
int pfs_dir_openat(int root_fd, const char *rel, int flags, unsigned mode, int *err);
|
||||
|
||||
/* Same containment (openat2/O_NOFOLLOW-fallback) as pfs_dir_openat, applied
|
||||
* to the parent directory of `rel`, then mkdirat/unlinkat/renameat'd
|
||||
* against that safely-resolved parent — never a raw path-string syscall. */
|
||||
int pfs_dir_mkdirat(int root_fd, const char *rel, int *err);
|
||||
int pfs_dir_unlinkat(int root_fd, const char *rel, int is_dir, int *err);
|
||||
int pfs_dir_unlinkat(int root_fd, const char *rel, int is_dir, int *err); /* is_dir selects AT_REMOVEDIR */
|
||||
int pfs_dir_renameat(int root_fd, const char *from, const char *to, int *err);
|
||||
|
||||
/* Contained fstat of `rel` beneath `root_fd`, filling the public VfsStat. */
|
||||
int pfs_dir_statat(int root_fd, const char *rel, VfsStat *out, int *err);
|
||||
|
||||
/*
|
||||
@@ -72,15 +81,23 @@ int pfs_landlock_restrict_to(const int *roots, size_t count);
|
||||
|
||||
#define PFS_MODE_DIR 0x1u /* directory-marker bit, Section 9.3 */
|
||||
|
||||
/* One on-disk index row (Section 9). Offsets are absolute byte offsets
|
||||
* into the pack file, already bounds-checked by pack_load (Section 7)
|
||||
* before any entry is trusted; `mode`'s only currently-interpreted bit
|
||||
* is PFS_MODE_DIR. */
|
||||
typedef struct PackIndexEntry {
|
||||
uint64_t name_off;
|
||||
uint32_t name_len;
|
||||
uint64_t data_off;
|
||||
uint64_t size;
|
||||
uint64_t name_off; /* offset of the name within the strings region */
|
||||
uint32_t name_len; /* name length, excluding the NUL terminator */
|
||||
uint64_t data_off; /* absolute offset of the blob within the pack file */
|
||||
uint64_t size; /* blob length in bytes */
|
||||
uint32_t mode;
|
||||
int64_t mtime;
|
||||
} PackIndexEntry;
|
||||
|
||||
/* An mmap'd, loaded, and Section-7-validated pack file. `entries` and
|
||||
* `strings_base` point directly into `map` — this is why pack_load
|
||||
* bounds-checks every offset before returning: everything downstream
|
||||
* trusts these pointers without re-checking. */
|
||||
typedef struct Pack {
|
||||
int fd;
|
||||
void *map;
|
||||
@@ -179,24 +196,80 @@ typedef struct UpperStore {
|
||||
char *root_path; /* UPPER_DIR only, for error messages */
|
||||
} UpperStore;
|
||||
|
||||
/* `root_fd` is the UPPER_DIR containment root (Section 6.2); pass -1 for
|
||||
* UPPER_MEM, which needs none. Starts with one empty UpperSnapshot. */
|
||||
UpperStore *upper_new(UpperKind kind, int root_fd /* -1 for mem */, const char *root_path);
|
||||
|
||||
/* Releases the current snapshot's reference and, for UPPER_DIR, closes
|
||||
* root_fd. Every file handle the caller opened against this store must
|
||||
* already be closed — this does not track or forcibly close them. */
|
||||
void upper_free(UpperStore *u);
|
||||
|
||||
/* Wait-free: returns the current snapshot with the caller's own
|
||||
* reference counted in (see UpperStore.reclaim_gate above for why this
|
||||
* is not simply "load the pointer, then atomically increment"). The
|
||||
* caller must upper_release() it exactly once, even on an error path. */
|
||||
UpperSnapshot *upper_acquire(UpperStore *u);
|
||||
|
||||
/* Drops one reference; frees the snapshot's entries (and unrefs their
|
||||
* cells) only when this was the last reference. Safe to call with NULL. */
|
||||
void upper_release(UpperSnapshot *s);
|
||||
|
||||
/* Structural + content operations, all against paths already normalized
|
||||
* and relative to the mount (leading "/"). */
|
||||
|
||||
/* Binary search by full path within an already-acquired snapshot.
|
||||
* Returns 1 and sets *out on hit, leaving *out untouched on a miss. */
|
||||
int upper_lookup(UpperSnapshot *s, const char *path, const UpperEntry **out);
|
||||
|
||||
/* True if any live (non-whiteout) entry's name starts with `path` + "/" —
|
||||
* i.e. `path` is a directory that exists only implicitly, via a child,
|
||||
* with no explicit entry of its own (Section 9.3). */
|
||||
int upper_has_children(UpperSnapshot *s, const char *path);
|
||||
|
||||
/* Resets `c`'s content to empty in place (VFS_O_TRUNC on an
|
||||
* already-existing file) — a content operation, not a structural one, so
|
||||
* it never touches the snapshot pointer (Section 5.3). */
|
||||
void upper_cell_truncate(UpperStore *u, MutCell *c, const char *path);
|
||||
|
||||
/* Structural write: installs a new, empty ENTRY_FILE at `path`, or
|
||||
* (racing another creator) hands back the cell an equivalent concurrent
|
||||
* call already installed. `truncate_existing` is currently unused —
|
||||
* truncation of an *existing* entry is a content operation, handled by
|
||||
* upper_cell_truncate, not by this function. */
|
||||
int upper_create(UpperStore *u, const char *path, int truncate_existing, MutCell **cell_out, int *err);
|
||||
|
||||
/* Structural write: installs an ENTRY_DIR_MARKER at `path` (Section 9.3).
|
||||
* Fails with VFS_ERR_EXIST if anything, explicit or implicit, already
|
||||
* occupies that path. */
|
||||
int upper_mkdir(UpperStore *u, const char *path, int *err);
|
||||
|
||||
int upper_remove(UpperStore *u, const char *path, int had_lower, int *err); /* whiteout if had_lower */
|
||||
|
||||
/* Structural write: moves the entry at `from` to `to` (same cell,
|
||||
* new name — no data copy for a file already in the upper layer).
|
||||
* `had_lower_from` selects whiteout-vs-plain-removal for `from`, exactly
|
||||
* as `upper_remove`'s parameter of the same name does. */
|
||||
int upper_rename(UpperStore *u, const char *from, const char *to, int had_lower_from, int *err);
|
||||
|
||||
/* Structural write: installs a new ENTRY_FILE at `path` pre-populated
|
||||
* with `data`/`size` (Section 4.1 step 3's copy-up, and journal replay's
|
||||
* PUT records, both reuse this rather than duplicating "create then
|
||||
* write"). Racing with another copy-up of the same path is handled the
|
||||
* same way as upper_create. */
|
||||
int upper_copy_up(UpperStore *u, const char *path, const void *data, uint64_t size, int64_t mtime, MutCell **cell_out, int *err);
|
||||
|
||||
/* Content read: never touches the snapshot pointer or writer_lock.
|
||||
* UPPER_MEM copies out of `c`'s buffer under buf_lock's read side
|
||||
* (Section 5.7); UPPER_DIR opens `path` beneath the containment root and
|
||||
* pread(2)s it. Returns the byte count read (0 at end of file, per
|
||||
* vfs_read's contract) or -1 on error. */
|
||||
pfs_isize upper_cell_read(UpperStore *u, MutCell *c, const char *path, pfs_usize off, void *buf, pfs_usize n);
|
||||
|
||||
/* Content write: the Section 5.3 fast path. Never touches the snapshot
|
||||
* pointer; a growing UPPER_MEM write follows Section 5.7 (allocate new,
|
||||
* copy, publish, retire old — never realloc the live buffer in place).
|
||||
* Returns the byte count written, or -1 with *err set. */
|
||||
pfs_isize upper_cell_write(UpperStore *u, MutCell *c, const char *path, pfs_usize off, const void *buf, pfs_usize n, int *err);
|
||||
|
||||
/* Snapshot the whole store into pack-builder entries (compaction, dir-marker-aware). */
|
||||
|
||||
+137
@@ -302,3 +302,140 @@ int pack_write(const char *path, const PackBuildEntry *in, size_t count, int *er
|
||||
if (rename(tmp_path, path) < 0) { unlink(tmp_path); if (err) *err = VFS_ERR_IO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* ---- standalone read-only pack Backend (Section 2.1, 3.1) ----
|
||||
*
|
||||
* `backend_overlay_new` is how a pack is normally made writable (Sections
|
||||
* 4-5), but Section 2.1's architecture diagram lists `pack` as its own
|
||||
* backend kind too — "one file, indexed, read-only" — for the case where
|
||||
* no writable upper layer is wanted at all, e.g. mounting a shipped asset
|
||||
* pack somewhere a program never intends to write. This wraps the same
|
||||
* `Pack` load/lookup/range machinery used by the overlay as a plain,
|
||||
* read-only Backend: every mutating operation returns VFS_ERR_PERM rather
|
||||
* than being silently absorbed.
|
||||
*/
|
||||
|
||||
typedef struct PackFile {
|
||||
const Pack *pack;
|
||||
const PackIndexEntry *entry;
|
||||
pfs_usize pos;
|
||||
} PackFile;
|
||||
|
||||
static int packbe_open(Backend *b, const char *path, int flags, VfsFile **out) {
|
||||
Pack *p = (Pack *)b->state;
|
||||
if (flags & (VFS_O_WRONLY | VFS_O_RDWR | VFS_O_CREAT | VFS_O_TRUNC)) return VFS_ERR_PERM;
|
||||
|
||||
const PackIndexEntry *e;
|
||||
if (!pack_find(p, path, &e)) return VFS_ERR_NOENT;
|
||||
if (e->mode & PFS_MODE_DIR) return VFS_ERR_ISDIR;
|
||||
|
||||
PackFile *pf = (PackFile *)calloc(1, sizeof(PackFile));
|
||||
pf->pack = p;
|
||||
pf->entry = e;
|
||||
VfsFile *f = (VfsFile *)calloc(1, sizeof(VfsFile));
|
||||
f->backend = b;
|
||||
f->state = pf;
|
||||
*out = f;
|
||||
return VFS_OK;
|
||||
}
|
||||
|
||||
static pfs_isize packbe_read(VfsFile *f, void *buf, pfs_usize n) {
|
||||
PackFile *pf = (PackFile *)f->state;
|
||||
pfs_usize size = pf->entry->size;
|
||||
pfs_usize avail = pf->pos < size ? size - pf->pos : 0;
|
||||
pfs_usize to_copy = n < avail ? n : avail;
|
||||
if (to_copy) memcpy(buf, (const char *)pack_entry_data(pf->pack, pf->entry) + pf->pos, to_copy);
|
||||
pf->pos += to_copy;
|
||||
return (pfs_isize)to_copy;
|
||||
}
|
||||
|
||||
static pfs_isize packbe_write(VfsFile *f, const void *buf, pfs_usize n) {
|
||||
(void)f; (void)buf; (void)n;
|
||||
return VFS_ERR_PERM;
|
||||
}
|
||||
|
||||
static int packbe_close(VfsFile *f) {
|
||||
free(f->state);
|
||||
free(f);
|
||||
return VFS_OK;
|
||||
}
|
||||
|
||||
static int packbe_stat(Backend *b, const char *path, VfsStat *out) {
|
||||
Pack *p = (Pack *)b->state;
|
||||
const PackIndexEntry *e;
|
||||
if (pack_find(p, path, &e)) {
|
||||
out->size = e->size;
|
||||
out->mtime = e->mtime;
|
||||
out->kind = (e->mode & PFS_MODE_DIR) ? VFS_KIND_DIR : VFS_KIND_FILE;
|
||||
return VFS_OK;
|
||||
}
|
||||
char prefix[PFS_PATH_MAX];
|
||||
if (strcmp(path, "/") == 0) strcpy(prefix, "/"); else snprintf(prefix, sizeof(prefix), "%s/", path);
|
||||
size_t lo, hi;
|
||||
pack_range(p, prefix, &lo, &hi);
|
||||
if (hi > lo || strcmp(path, "/") == 0) {
|
||||
out->size = 0; out->mtime = 0; out->kind = VFS_KIND_DIR;
|
||||
return VFS_OK;
|
||||
}
|
||||
return VFS_ERR_NOENT;
|
||||
}
|
||||
|
||||
static int packbe_readdir(Backend *b, const char *path, VfsDir *out) {
|
||||
Pack *p = (Pack *)b->state;
|
||||
char prefix[PFS_PATH_MAX];
|
||||
if (strcmp(path, "/") == 0) strcpy(prefix, "/"); else snprintf(prefix, sizeof(prefix), "%s/", path);
|
||||
size_t plen = strlen(prefix);
|
||||
|
||||
size_t lo, hi;
|
||||
pack_range(p, prefix, &lo, &hi);
|
||||
if (lo == hi && strcmp(path, "/") != 0) {
|
||||
const PackIndexEntry *e;
|
||||
if (!pack_find(p, path, &e)) return VFS_ERR_NOENT;
|
||||
if (!(e->mode & PFS_MODE_DIR)) return VFS_ERR_NOTDIR;
|
||||
}
|
||||
|
||||
VfsDirEntry *entries = NULL;
|
||||
pfs_usize count = 0, cap = 0;
|
||||
for (size_t i = lo; i < hi; i++) {
|
||||
const char *name = pack_entry_name(p, &p->entries[i]);
|
||||
const char *restp = name + plen;
|
||||
const char *slash = strchr(restp, '/');
|
||||
size_t clen = slash ? (size_t)(slash - restp) : strlen(restp);
|
||||
if (clen == 0 || clen >= sizeof(entries[0].name)) continue;
|
||||
if (count > 0 && strncmp(entries[count - 1].name, restp, clen) == 0 &&
|
||||
entries[count - 1].name[clen] == '\0') continue; /* sorted -> dup is adjacent */
|
||||
if (count == cap) { cap = cap ? cap * 2 : 8; entries = (VfsDirEntry *)realloc(entries, cap * sizeof(VfsDirEntry)); }
|
||||
memcpy(entries[count].name, restp, clen);
|
||||
entries[count].name[clen] = '\0';
|
||||
entries[count].kind = slash ? VFS_KIND_DIR : ((p->entries[i].mode & PFS_MODE_DIR) ? VFS_KIND_DIR : VFS_KIND_FILE);
|
||||
count++;
|
||||
}
|
||||
out->entries = entries;
|
||||
out->count = count;
|
||||
return VFS_OK;
|
||||
}
|
||||
|
||||
static int packbe_mkdir(Backend *b, const char *path) { (void)b; (void)path; return VFS_ERR_PERM; }
|
||||
static int packbe_unlink(Backend *b, const char *path) { (void)b; (void)path; return VFS_ERR_PERM; }
|
||||
static int packbe_rename(Backend *b, const char *from, const char *to) { (void)b; (void)from; (void)to; return VFS_ERR_PERM; }
|
||||
static int packbe_sync(Backend *b) { (void)b; return VFS_ERR_PERM; }
|
||||
|
||||
static void packbe_free(Backend *b) {
|
||||
pack_close((Pack *)b->state);
|
||||
free(b);
|
||||
}
|
||||
|
||||
static const BackendOps PACK_OPS = {
|
||||
packbe_open, packbe_read, packbe_write, packbe_close, packbe_stat,
|
||||
packbe_readdir, packbe_mkdir, packbe_unlink, packbe_rename, packbe_sync, packbe_free
|
||||
};
|
||||
|
||||
Backend *backend_pack_new(const char *pack_path, int *err) {
|
||||
Pack *p;
|
||||
if (pack_load(pack_path, &p, err) < 0) return NULL;
|
||||
Backend *b = (Backend *)calloc(1, sizeof(Backend));
|
||||
b->ops = &PACK_OPS;
|
||||
b->state = p;
|
||||
return b;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user