2026-09-14 05:44:08 +00:00
|
|
|
/*
|
|
|
|
|
* packfs.h — public API of PackFS.
|
|
|
|
|
*
|
|
|
|
|
* PackFS is a statically linked, in-process virtual file system. The design
|
|
|
|
|
* rationale for every decision in this header is recorded in concept.md,
|
|
|
|
|
* which this implementation follows; concept.md is frozen (see CLAUDE.md)
|
|
|
|
|
* and is not re-derived here.
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: MIT
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#ifndef PACKFS_H
|
|
|
|
|
#define PACKFS_H
|
|
|
|
|
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
2026-09-14 10:58:35 +00:00
|
|
|
/* Semantic versioning (semver.org). 0.x per semver's own definition means
|
|
|
|
|
* the public API may still change between minor versions without a major
|
|
|
|
|
* bump — consistent with this codebase's own "v0" status (README.md
|
|
|
|
|
* "Status"): an initial, partial implementation of concept.md, not a
|
|
|
|
|
* completed one. PACKFS_VERSION_STRING and pfs_version() always agree
|
|
|
|
|
* (the latter is generated from the former's components, not maintained
|
|
|
|
|
* separately) and both come from this header, so a statically-linked
|
|
|
|
|
* consumer's PACKFS_VERSION_* macros and a dynamically-linked consumer's
|
|
|
|
|
* pfs_version() call always describe the same build. */
|
|
|
|
|
#define PACKFS_VERSION_MAJOR 0
|
|
|
|
|
#define PACKFS_VERSION_MINOR 1
|
|
|
|
|
#define PACKFS_VERSION_PATCH 0
|
|
|
|
|
#define PACKFS_VERSION_STRING "0.1.0"
|
|
|
|
|
|
2026-09-14 05:44:08 +00:00
|
|
|
#ifdef __cplusplus
|
|
|
|
|
extern "C" {
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/* concept.md Section 8 leaves isize/usize as project-defined typedefs. */
|
|
|
|
|
typedef ptrdiff_t pfs_isize;
|
|
|
|
|
typedef size_t pfs_usize;
|
|
|
|
|
|
|
|
|
|
typedef struct Vfs Vfs;
|
|
|
|
|
typedef struct VfsFile VfsFile;
|
|
|
|
|
typedef struct Backend Backend;
|
|
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
|
VFS_OK = 0,
|
|
|
|
|
VFS_ERR_NOENT = -1, /* path does not exist */
|
|
|
|
|
VFS_ERR_EXIST = -2, /* path already exists */
|
|
|
|
|
VFS_ERR_NOTDIR = -3, /* path component is not a directory */
|
|
|
|
|
VFS_ERR_ISDIR = -4, /* operation not valid on a directory */
|
|
|
|
|
VFS_ERR_INVAL = -5, /* invalid argument (includes path escapes, see Section 6) */
|
|
|
|
|
VFS_ERR_IO = -6, /* host I/O error */
|
|
|
|
|
VFS_ERR_NOSPC = -7, /* out of space / allocation failure */
|
|
|
|
|
VFS_ERR_PERM = -8, /* operation not permitted (e.g. write to read-only backend) */
|
|
|
|
|
VFS_ERR_NOTEMPTY = -9, /* directory not empty */
|
|
|
|
|
VFS_ERR_CORRUPT = -10, /* pack failed integrity validation, Section 7 */
|
|
|
|
|
VFS_ERR_NOMOUNT = -11 /* no backend mounted for this path */
|
|
|
|
|
} VfsStatus;
|
|
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
|
VFS_KIND_FILE = 0,
|
|
|
|
|
VFS_KIND_DIR = 1
|
|
|
|
|
} VfsEntryKind;
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* Result of vfs_stat. For a directory, `size` is always 0 (directories
|
|
|
|
|
* carry no byte content) and `mtime` is 0 for one that exists only
|
|
|
|
|
* implicitly via a child's path (Section 9.3) rather than an explicit
|
|
|
|
|
* directory-marker entry. */
|
2026-09-14 05:44:08 +00:00
|
|
|
typedef struct VfsStat {
|
|
|
|
|
pfs_usize size;
|
|
|
|
|
int64_t mtime; /* seconds since epoch */
|
|
|
|
|
VfsEntryKind kind;
|
|
|
|
|
} VfsStat;
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* One child of a directory, as populated by vfs_readdir. `name` is the
|
|
|
|
|
* final path component only (not the full path), NUL-terminated. */
|
2026-09-14 05:44:08 +00:00
|
|
|
typedef struct VfsDirEntry {
|
|
|
|
|
char name[256];
|
|
|
|
|
VfsEntryKind kind;
|
|
|
|
|
} VfsDirEntry;
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* Result of vfs_readdir: `count` entries at `entries`, owned by the
|
|
|
|
|
* caller until passed to vfs_dir_free. A freshly zeroed VfsDir
|
|
|
|
|
* (`{0}`/`memset`) is always safe to pass to vfs_dir_free even if
|
|
|
|
|
* vfs_readdir was never called on it or returned an error. */
|
2026-09-14 05:44:08 +00:00
|
|
|
typedef struct VfsDir {
|
|
|
|
|
VfsDirEntry *entries;
|
|
|
|
|
pfs_usize count;
|
|
|
|
|
} VfsDir;
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* Flags for vfs_open, bitwise-OR'd. VFS_O_RDONLY is 0, i.e. the absence
|
|
|
|
|
* of VFS_O_WRONLY/VFS_O_RDWR, matching POSIX's convention. */
|
2026-09-14 05:44:08 +00:00
|
|
|
#define VFS_O_RDONLY 0x00
|
|
|
|
|
#define VFS_O_WRONLY 0x01
|
|
|
|
|
#define VFS_O_RDWR 0x02
|
2026-09-14 05:54:52 +00:00
|
|
|
#define VFS_O_CREAT 0x04 /* create the file if it does not already exist */
|
|
|
|
|
#define VFS_O_TRUNC 0x08 /* reset an existing file's content to empty */
|
2026-09-14 05:44:08 +00:00
|
|
|
|
|
|
|
|
/* --- VFS lifecycle -------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
Vfs *vfs_new(void);
|
|
|
|
|
void vfs_free(Vfs *v);
|
|
|
|
|
|
2026-09-14 10:58:35 +00:00
|
|
|
/* Returns PACKFS_VERSION_STRING of the linked library (not necessarily the
|
|
|
|
|
* header a caller compiled against, for a dynamically-linked consumer) —
|
|
|
|
|
* an ABI/API compatibility check available at runtime, not just compile
|
|
|
|
|
* time. Always non-NULL, always a byte-identical string literal, never
|
|
|
|
|
* allocated: never free() the result. */
|
|
|
|
|
const char *pfs_version(void);
|
|
|
|
|
|
2026-09-14 05:44:08 +00:00
|
|
|
/* --- Backend constructors -------------------------------------------
|
|
|
|
|
*
|
|
|
|
|
* Every backend is a Backend* handed to vfs_mount. Backends not currently
|
|
|
|
|
* mounted anywhere may be freed with backend_free; a backend still mounted
|
|
|
|
|
* must be unmounted first.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/* mem: default writable layer (Section 3.1). */
|
|
|
|
|
Backend *backend_mem_new(void);
|
|
|
|
|
|
|
|
|
|
/* dir: capability-scoped host directory, containment per Section 6. */
|
|
|
|
|
Backend *backend_dir_new(const char *host_path, int *err);
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/*
|
|
|
|
|
* pack: a shipped tree in one file, read-only, loaded and validated per
|
|
|
|
|
* Section 7 (Section 2.1, 3.1). Every mutating operation on a Backend
|
|
|
|
|
* returned by this constructor (open with a write flag, mkdir, unlink,
|
|
|
|
|
* rename, sync) returns VFS_ERR_PERM rather than being silently absorbed.
|
|
|
|
|
* To make a pack writable, wrap it in an overlay via backend_overlay_new
|
|
|
|
|
* instead of mounting it directly.
|
|
|
|
|
*/
|
2026-09-14 05:44:08 +00:00
|
|
|
Backend *backend_pack_new(const char *pack_path, int *err);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* overlay: copy-on-write overlay per Sections 4-5. `pack_path` is the pack
|
|
|
|
|
* image backing the read-only lower layer: if the file exists it is
|
|
|
|
|
* loaded and validated (Section 7) at mount time; NULL means "start with
|
|
|
|
|
* an empty lower layer" (vfs_sync then requires a non-NULL path to have
|
|
|
|
|
* been set — pass one via backend_overlay_new even for a fresh overlay
|
|
|
|
|
* that has no pack file yet). `upper` must be a `mem` or `dir` backend,
|
|
|
|
|
* not otherwise mounted; the overlay does not take ownership of it — free
|
|
|
|
|
* it explicitly after unmounting the overlay. A journal at
|
|
|
|
|
* `pack_path` + ".jnl" (Section 4.4) is replayed at mount time if present,
|
|
|
|
|
* and rewritten (not appended past its watermark) on every vfs_sync
|
|
|
|
|
* (Section 5.3).
|
|
|
|
|
*/
|
|
|
|
|
Backend *backend_overlay_new(const char *pack_path, Backend *upper, int *err);
|
|
|
|
|
|
|
|
|
|
void backend_free(Backend *b);
|
|
|
|
|
|
|
|
|
|
/* --- Mounting --------------------------------------------------------
|
|
|
|
|
*
|
|
|
|
|
* The mount table is itself part of the atomically-swapped snapshot
|
|
|
|
|
* (Section 5.3): vfs_mount/vfs_unmount are structural writes, serialized
|
|
|
|
|
* against each other and against every other structural write, and never
|
|
|
|
|
* observed mid-change by a concurrent path resolution.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* Mounts `b` at `prefix` (longest-prefix match resolves overlapping
|
|
|
|
|
* mounts, Section 2.2). Returns VFS_ERR_INVAL if `prefix` fails
|
|
|
|
|
* virtual-namespace canonicalization (Section 6.1), VFS_ERR_EXIST if a
|
|
|
|
|
* mount already exists at exactly this prefix, else VFS_OK. */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_mount(Vfs *v, const char *prefix, Backend *b);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Detaches the backend mounted at exactly `prefix`. Returns
|
|
|
|
|
* VFS_ERR_NOMOUNT if nothing is mounted there. The detached Backend is
|
|
|
|
|
* not freed — call backend_free explicitly once no other thread can
|
|
|
|
|
* still be resolving a path through it. */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_unmount(Vfs *v, const char *prefix);
|
|
|
|
|
|
|
|
|
|
/* --- File operations -------------------------------------------------- */
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* Opens `path` per the resolved backend's semantics. VFS_O_CREAT creates
|
|
|
|
|
* a new file if none exists; VFS_O_TRUNC resets an existing file's
|
|
|
|
|
* content to empty. Returns NULL and sets *err on failure (e.g.
|
|
|
|
|
* VFS_ERR_NOENT if the path doesn't exist and VFS_O_CREAT wasn't given,
|
|
|
|
|
* VFS_ERR_ISDIR if the path names a directory, VFS_ERR_PERM if the
|
|
|
|
|
* backend or the specific path is read-only). */
|
2026-09-14 05:44:08 +00:00
|
|
|
VfsFile *vfs_open(Vfs *v, const char *path, int flags, int *err);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Reads up to `n` bytes from the file's current position into `buf`,
|
|
|
|
|
* advancing that position by the number of bytes actually read. Returns
|
|
|
|
|
* the byte count (0 at end of file), or a negative VfsStatus on error. */
|
2026-09-14 05:44:08 +00:00
|
|
|
pfs_isize vfs_read(VfsFile *f, void *buf, pfs_usize n);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Writes `n` bytes from `buf` at the file's current position, advancing
|
|
|
|
|
* that position by the number of bytes actually written; a write past
|
|
|
|
|
* the current end of the file extends it. Returns the byte count
|
|
|
|
|
* written, or a negative VfsStatus on error (e.g. VFS_ERR_PERM for a
|
|
|
|
|
* file opened read-only or served from a read-only backend). */
|
2026-09-14 05:44:08 +00:00
|
|
|
pfs_isize vfs_write(VfsFile *f, const void *buf, pfs_usize n);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Closes a file handle from vfs_open, releasing every resource it held.
|
|
|
|
|
* `f` must not be used again afterward. Always returns VFS_OK. */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_close(VfsFile *f);
|
|
|
|
|
|
2026-09-14 05:54:52 +00:00
|
|
|
/* Fills `out` with `path`'s size, kind, and mtime. Returns VFS_ERR_NOENT
|
|
|
|
|
* if nothing exists at that path in the resolved backend. */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_stat(Vfs *v, const char *path, VfsStat *out);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Lists the immediate children of the directory at `path` into `out`
|
|
|
|
|
* (caller-owned until passed to vfs_dir_free). Returns VFS_ERR_NOENT if
|
|
|
|
|
* `path` doesn't exist, VFS_ERR_NOTDIR if it names a file. */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_readdir(Vfs *v, const char *path, VfsDir *out);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Frees the entries array a prior vfs_readdir populated into `*d` and
|
|
|
|
|
* zeroes it; safe to call on an already-freed or zeroed VfsDir. */
|
2026-09-14 05:44:08 +00:00
|
|
|
void vfs_dir_free(VfsDir *d);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Creates an empty directory at `path` (Section 9.3: this writes an
|
|
|
|
|
* explicit, compaction-surviving marker, since an empty directory has no
|
|
|
|
|
* file beneath it to imply its existence). Returns VFS_ERR_EXIST if
|
|
|
|
|
* `path` already names something. */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_mkdir(Vfs *v, const char *path);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Removes the file or empty directory at `path`. Returns VFS_ERR_NOENT
|
|
|
|
|
* if nothing exists there, VFS_ERR_NOTEMPTY for a non-empty directory,
|
|
|
|
|
* VFS_ERR_PERM if the resolved backend is read-only. Against an overlay,
|
|
|
|
|
* removing a path that exists in the read-only lower pack writes a
|
|
|
|
|
* whiteout rather than failing (Section 4.2). */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_unlink(Vfs *v, const char *path);
|
2026-09-14 05:54:52 +00:00
|
|
|
|
|
|
|
|
/* Moves the entry at `from` to `to` within the same mount; both paths
|
|
|
|
|
* must resolve to the same backend (VFS_ERR_INVAL otherwise — v0 does
|
|
|
|
|
* not support a cross-mount rename). */
|
2026-09-14 05:44:08 +00:00
|
|
|
int vfs_rename(Vfs *v, const char *from, const char *to);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* vfs_sync compacts the overlay mounted exactly at `overlay_prefix`
|
|
|
|
|
* (Section 4.1 step 5, Section 5.3): it freezes the current upper
|
|
|
|
|
* snapshot, writes a new pack, and replaces the journal with only the
|
|
|
|
|
* records written after the watermark (Section 5.3, Section 4.4).
|
|
|
|
|
*/
|
|
|
|
|
int vfs_sync(Vfs *v, const char *overlay_prefix);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Optional, opt-in, process-wide Landlock hardening (Section 6.2). This
|
|
|
|
|
* is deliberately NOT applied automatically by vfs_mount: Landlock
|
|
|
|
|
* restrictions are cumulative, irreversible, and apply to the whole
|
|
|
|
|
* calling process/thread, which would be a surprising side effect for an
|
|
|
|
|
* embeddable library to trigger on its own. Call it once, after all `dir`
|
|
|
|
|
* mounts the process will ever need are in place, if the embedding
|
|
|
|
|
* application wants the whole process confined to exactly those
|
|
|
|
|
* directories at the kernel level. Returns VFS_OK on success; if the
|
|
|
|
|
* running kernel does not support Landlock, it returns VFS_ERR_PERM and
|
|
|
|
|
* changes nothing — this is the documented residual-risk fallback of
|
|
|
|
|
* Section 6.3, not a fatal error.
|
|
|
|
|
*/
|
|
|
|
|
int vfs_harden_process_with_landlock(Vfs *v);
|
|
|
|
|
|
|
|
|
|
#ifdef __cplusplus
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#endif /* PACKFS_H */
|