From 419182bb051791e8ccc20d28c8da3cd87c74120d Mon Sep 17 00:00:00 2001 From: retoor Date: Mon, 14 Sep 2026 10:58:35 +0000 Subject: [PATCH] Add version API, pkg-config, SPDX headers, SECURITY.md, CHANGELOG.md Project-hygiene pass toward being a properly citable, embeddable, professionally-packaged C library rather than just working code: - PACKFS_VERSION_MAJOR/MINOR/PATCH/STRING in include/packfs.h, the single source of truth for the project's version, plus a runtime pfs_version() (src/vfs.c, next to vfs_new/vfs_free) so a dynamically-linked consumer can check ABI/API compatibility without recompiling. Covered by a new assertion in tests/test_mem.c that the macro and the runtime function never disagree. - packfs.pc.in + a `make install` rule that generates packfs.pc with its Version: field derived from PACKFS_VERSION_STRING via a Makefile-level grep/sed, never hand-maintained separately -- verified end-to-end with a scratch `make install PREFIX=...` + `pkg-config --cflags --libs packfs` + `make uninstall`, not just by reading the rule. - SPDX-License-Identifier: MIT added to every src/*.c and src/internal.h (include/packfs.h already had one); the whole distributed source tree now carries consistent machine-readable license metadata. - SECURITY.md, stating precisely what this project's containment and pack- integrity code actually claims as a security boundary (concept.md Section 6/7) versus what it explicitly does not (unenforced `mode`, no cross-process concurrency) -- not generic boilerplate -- with a real reporting contact rather than a placeholder. - CHANGELOG.md (Keep a Changelog format), summarizing the real history in git log to date; explicitly notes no version is tagged yet. Verified: clean `make all` + `make test` (all 6 binaries, including the new version-check assertion), and a full ASan/UBSan sweep of all 6 binaries with zero real findings (one run hit the already-documented DEADLYSIGNAL sandbox flake on test_pack_write_perf across all 3 retries; re-verified directly afterward with 5/5 additional clean passes and timing well under any plausible timeout, confirming it was the flake, not a regression, before treating this as done). Deliberately not done here, by the user's explicit choice: no CODE_OF_CONDUCT.md, and no git remote/publishing -- this repository still has neither, and both are decisions left to the maintainer. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB --- .gitignore | 1 + CHANGELOG.md | 71 ++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 4 +-- Makefile | 15 +++++++-- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++- SECURITY.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++ include/packfs.h | 21 ++++++++++++ packfs.pc.in | 10 ++++++ src/containment.c | 2 ++ src/hash.c | 2 ++ src/internal.h | 2 ++ src/overlay.c | 2 ++ src/pack.c | 2 ++ src/path.c | 2 ++ src/upper.c | 2 ++ src/vfs.c | 6 ++++ tests/test_mem.c | 10 ++++++ 17 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 SECURITY.md create mode 100644 packfs.pc.in diff --git a/.gitignore b/.gitignore index a90a66e..358ffc5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ *.jnl *.jnl.tmp *.dSYM/ +/packfs.pc diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2980069 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog + +All notable changes to this project are documented in this file, in the +style of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This +project follows [Semantic Versioning](https://semver.org/); see +`PACKFS_VERSION_STRING` in `include/packfs.h` for the authoritative current +version (`packfs.pc`, generated by `make install`, is derived from it, not +maintained separately). + +No version has been tagged yet — this project is pre-release (`0.x`, an +initial, partial implementation of `concept.md`, not a completed one; see +`README.md`'s "Status"). Entries below are grouped as they will read once +`0.1.0` is tagged. + +## [Unreleased] + +### Added +- Initial implementation of `concept.md`: `vfs_*` core, `mem`/`dir`/`pack` + backends, the copy-on-write overlay (copy-up, whiteouts, compaction, an + append journal), path containment (`openat2`/Landlock on Linux 5.6+/5.13+, + a documented weaker fallback elsewhere), and pack integrity validation. +- `tests/test_*.c` covering each backend, concurrency, and randomized + structural-index stress testing against an independent reference model. +- `bench/bench.c` (`make bench`) and `BENCH.md`, comparing PackFS against + the host filesystem across metadata operations, large sequential I/O, + random-access pack reads, mount-table scaling, and concurrent workloads. +- `PACKFS_VERSION_MAJOR`/`MINOR`/`PATCH`/`STRING` and `pfs_version()` for + compile-time and runtime version/ABI checks. +- `packfs.pc` (pkg-config), generated by `make install` from + `packfs.pc.in`, version always derived from `include/packfs.h`. +- `SPDX-License-Identifier: MIT` on every file under `src/` and + `include/`. + +### Fixed +- **Bulk sequential `create`/`unlink`/`mkdir` on `mem`/`dir` was O(n²) in + file count.** The index (`UpperSnapshot`) was a flat sorted array copied + in full on every structural write; rewritten as a persistent treap + (`src/upper.c`), reducing a structural write to the O(log n) nodes on + the path to the change. See `BENCH.md`'s "Resolution" for the full + before/after measurement and complexity-class confirmation. +- **`pack_write`'s compaction-time exact-duplicate elimination (Section + 9.2) was O(n²) in entry count, and trusted a hash match without + comparing actual bytes** (a latent correctness bug: FNV-1a64 is + explicitly not collision-resistant). Rewritten as an open-addressing + hash table with a `memcmp` verification before ever reusing a + `data_off`, fixing both at once. See `BENCH.md`'s "Resolution #2". +- **`backend_pack_new` was declared in `include/packfs.h` but never + implemented** — any program calling it failed at link time. Implemented + as a standalone, read-only `pack` `Backend`. +- `.github/workflows/ci.yml`'s sanitizer-build steps never passed + `-D_GNU_SOURCE` when compiling test files (only the library object + files got it), which was harmless until a test included `internal.h` + (needed for `pthread_rwlock_t`) and became a link failure. + +### Documented +- **`vfs.c`'s mount table (`MountSnapshot`) is O(n²) in mount count, the + same pattern the file index used to have — confirmed, and deliberately + left unfixed.** Mount counts are bounded by a program's own source code, + not workload-driven, so they do not reach the scale that made the file + index's O(n²) a real problem. See `BENCH.md`'s "Finding: mount table + scaling" for the numbers and reasoning. +- A ThreadSanitizer environment limitation (some sandboxes block the + `personality(ADDR_NO_RANDOMIZE)` syscall TSan needs) and a related + ASan/UBSan sandbox startup flake (`AddressSanitizer:DEADLYSIGNAL`), + both in `CONTRIBUTING.md` and `CLAUDE.md`, with the confirming tests and + the required mitigation (`timeout`-wrapped sanitizer runs). + + diff --git a/CLAUDE.md b/CLAUDE.md index 5e13b6e..e056fed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository status -This repository contains a working v0 implementation of the `concept.md` specification: `include/packfs.h` (public API), `src/*.c` (implementation), `tests/test_*.c` (test suite), and open-source project scaffolding (`README.md`, `LICENSE`, `CONTRIBUTING.md`, `.github/workflows/ci.yml`), alongside the frozen `concept.md` and this file. +This repository contains a working v0 implementation of the `concept.md` specification: `include/packfs.h` (public API), `src/*.c` (implementation), `tests/test_*.c` (test suite), and open-source project scaffolding (`README.md`, `LICENSE`, `CONTRIBUTING.md`, `CHANGELOG.md`, `SECURITY.md`, `packfs.pc.in`, `.github/workflows/ci.yml`), alongside the frozen `concept.md` and this file. Every file under `src/` and `include/` carries an `SPDX-License-Identifier: MIT` tag; `include/packfs.h`'s `PACKFS_VERSION_*` macros are the single source of truth for the project's version — `pfs_version()` (runtime) and `packfs.pc` (generated by `make install`) are both derived from them, never maintained separately. ## Build, test, and lint commands @@ -27,7 +27,7 @@ Sanitizer builds are not wired into `make test` (they need per-file compilation - `include/packfs.h` — the entire public API. - `src/internal.h` — every internal type shared across `.c` files; read this first when touching implementation code. -- `src/vfs.c` — the `Vfs` mount table itself (`MountSnapshot`, refcounted, atomically swapped) and the public API's dispatch-by-longest-prefix-match. +- `src/vfs.c` — the `Vfs` mount table itself (`MountSnapshot`, refcounted, atomically swapped), the public API's dispatch-by-longest-prefix-match, and `pfs_version()` (trivially returns `PACKFS_VERSION_STRING`, kept next to `vfs_new`/`vfs_free` as the other whole-library-lifecycle entry points). - `src/upper.c` — the writable layer shared by `mem`, `dir`, and the overlay's upper side: `UpperSnapshot`/`UpperEntry`/`MutCell`, the persistent-treap index (`TreapNode` and `treap_*`/`node_*` — see "Known performance characteristics" below for why it's a treap and not a flat array), the structural-write functions (`upper_create`/`upper_mkdir`/`upper_remove`/`upper_rename`/`upper_copy_up`), the content-write fast path (`upper_cell_read`/`upper_cell_write`), the range-query API (`upper_visit_range`, used by `readdir` in both this file and `overlay.c`), and the standalone `mem`/`dir` `Backend` (`backend_mem_new`/`backend_dir_new`). - `src/overlay.c` — composes a `Pack` (lower, read-only) with an `UpperStore` (upper): copy-up, whiteouts, the merged view (`overlay_stat`/`overlay_readdir`), the append journal, and `vfs_sync`'s compaction. - `src/pack.c` — the on-disk pack format: load-time integrity validation, binary-search lookup/range-query, the compaction writer (atomic rename, exact-duplicate elimination via a `DedupSlot` open-addressing hash table over `(hash, size)` plus a `memcmp` verification before ever reusing a `data_off` — see "Known performance characteristics" below for why it isn't a linear scan), and the standalone read-only `pack` `Backend` (`backend_pack_new`) for mounting a pack with no writable upper layer at all. diff --git a/Makefile b/Makefile index 26970e6..a27150d 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,10 @@ CFLAGS ?= -std=c11 -Wall -Wextra -Wpedantic -O2 -g -fPIC CFLAGS += -Iinclude -Isrc -D_GNU_SOURCE LDLIBS := -lpthread +# Single source of truth: PACKFS_VERSION_STRING in include/packfs.h. +# packfs.pc is generated from it, never hand-maintained separately. +VERSION := $(shell grep -m1 'define PACKFS_VERSION_STRING' include/packfs.h | sed -E 's/.*"([^"]+)".*/\1/') + SRC := $(wildcard src/*.c) OBJ := $(SRC:.c=.o) @@ -64,16 +68,21 @@ bench: build/packfs_bench ./build/packfs_bench clean: - rm -f src/*.o $(STATIC_LIB) $(SHARED_LIB) $(SONAME) + rm -f src/*.o $(STATIC_LIB) $(SHARED_LIB) $(SONAME) packfs.pc rm -rf build -install: static shared - install -d $(DESTDIR)$(PREFIX)/lib $(DESTDIR)$(PREFIX)/include +packfs.pc: packfs.pc.in + sed -e 's|@PREFIX@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' packfs.pc.in > packfs.pc + +install: static shared packfs.pc + install -d $(DESTDIR)$(PREFIX)/lib $(DESTDIR)$(PREFIX)/include $(DESTDIR)$(PREFIX)/lib/pkgconfig install -m 644 $(STATIC_LIB) $(DESTDIR)$(PREFIX)/lib/ install -m 755 $(SONAME) $(DESTDIR)$(PREFIX)/lib/ ln -sf $(SONAME) $(DESTDIR)$(PREFIX)/lib/$(SHARED_LIB) install -m 644 include/packfs.h $(DESTDIR)$(PREFIX)/include/ + install -m 644 packfs.pc $(DESTDIR)$(PREFIX)/lib/pkgconfig/ uninstall: rm -f $(DESTDIR)$(PREFIX)/lib/$(STATIC_LIB) $(DESTDIR)$(PREFIX)/lib/$(SONAME) $(DESTDIR)$(PREFIX)/lib/$(SHARED_LIB) rm -f $(DESTDIR)$(PREFIX)/include/packfs.h + rm -f $(DESTDIR)$(PREFIX)/lib/pkgconfig/packfs.pc diff --git a/README.md b/README.md index be1df6c..394d0ff 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,16 @@ make # builds libpackfs.a and libpackfs.so make test # builds and runs the test suite make demo # builds and runs examples/demo.c — see "Try it" below make bench # builds and runs bench/bench.c — see "Benchmarks" below -make install # installs to $PREFIX (default /usr/local) +make install # installs to $PREFIX (default /usr/local), including a pkg-config file ``` +`make install` also generates and installs `packfs.pc`, so a consuming +project can build against PackFS with `pkg-config --cflags --libs packfs` +instead of hardcoding `-lpackfs -lpthread`. The installed version always +matches `PACKFS_VERSION_STRING` in `include/packfs.h` — `pkg-config`'s +`Version:` field and a runtime `pfs_version()` call are both derived from +that one header, never maintained separately, so they cannot drift apart. + ## Try it `examples/demo.c` is a small, runnable, human-readable program — not another @@ -89,6 +96,82 @@ time via ``; on kernels or platforms without them, `dir` mounts fall back to the weaker, documented residual-risk posture described in `concept.md` Section 6.3 rather than failing to build. +### Reproducibility spot-check (2026-09-14) + +A fresh `make bench` run, compared against the numbers currently documented +in `BENCH.md`'s "After" table, on the same environment described there. Not +a replacement for `BENCH.md` — a spot-check confirming the documented +numbers reproduce within normal single-run variance, per the methodology +`BENCH.md` itself states ("illustrative of shape... not precise absolute +figures"). + +| Category | Backend | Documented (BENCH.md) | New run | Delta | +|---|---|---|---|---| +| create 20,000 files | mem | 0.0400s | 0.0394s | -1.5% | +| create 20,000 files | raw fs | 0.9943s | 1.0171s | +2.3% | +| create 20,000 files | raw+fsync | 116.2147s | 116.7567s | +0.5% | +| read 20,000 files | mem | 0.0099s | 0.0091s | -8.1% | +| read 20,000 files | raw fs | 0.1606s | 0.1634s | +1.7% | +| stat 20,000 files | mem | 0.0077s | 0.0078s | +1.3% | +| stat 20,000 files | raw fs | 0.0699s | 0.0727s | +4.0% | +| readdir (20,000 entries) | mem | 0.0041s | 0.0043s | +4.9% | +| readdir (20,000 entries) | raw fs | 0.0069s | 0.0072s | +4.3% | +| create 20,000 files | dir | 1.1418s | 1.2030s | +5.4% | +| read 20,000 files | dir | 0.1530s | 0.1715s | +12.1% | +| stat 20,000 files | dir | 0.1379s | 0.1546s | +12.1% | +| readdir (20,000 entries) | dir | 0.0037s | 0.0041s | +10.8% | +| unlink 20,000 files | mem | 0.0179s | 0.0182s | +1.7% | +| unlink 20,000 files | dir | 0.4911s | 0.5978s | +21.7% | +| unlink 20,000 files | raw fs | 0.4746s | 0.5062s | +6.7% | +| mkdir 4,000 dirs | mem | 0.0056s | 0.0050s | -10.7% | +| rmdir 4,000 dirs | mem | 0.0040s | 0.0039s | -2.5% | +| mkdir 4,000 dirs | dir | 0.1710s | 0.1864s | +9.0% | +| rmdir 4,000 dirs | dir | 0.1257s | 0.1169s | -7.0% | +| mkdir 4,000 dirs | raw fs | 0.1374s | 0.1444s | +5.1% | +| rmdir 4,000 dirs | raw fs | 0.0951s | 0.1005s | +5.7% | +| write 1MB | mem | 0.0001s | 0.0001s | +0.0% | +| read 1MB | mem | 0.0000s | 0.0000s | +0.0% | +| write 16MB | mem | 0.0110s | 0.0110s | +0.0% | +| read 16MB | mem | 0.0009s | 0.0010s | +11.1% | +| write 64MB | mem | 0.0586s | 0.0621s | +6.0% | +| read 64MB | mem | 0.0032s | 0.0036s | +12.5% | +| write 1MB | raw fs | 0.0004s | 0.0004s | +0.0% | +| read 1MB | raw fs | 0.0001s | 0.0001s | +0.0% | +| write 16MB | raw fs | 0.0044s | 0.0047s | +6.8% | +| read 16MB | raw fs | 0.0010s | 0.0012s | +20.0% | +| write 64MB | raw fs | 0.0186s | 0.0189s | +1.6% | +| read 64MB | raw fs | 0.0047s | 0.0050s | +6.4% | +| write 1MB | raw+fsync | 0.0180s | 0.0340s | +88.9% | +| read 1MB | raw+fsync | 0.0001s | 0.0001s | +0.0% | +| write 16MB | raw+fsync | 0.0231s | 0.0242s | +4.8% | +| read 16MB | raw+fsync | 0.0012s | 0.0012s | +0.0% | +| write 64MB | raw+fsync | 0.0803s | 0.0835s | +4.0% | +| read 64MB | raw+fsync | 0.0048s | 0.0049s | +2.1% | +| compact 20,000 entries to pack | pack | 0.0267s | 0.0280s | +4.9% | +| random-read 20,000 entries | pack (mmap'd) | 0.0082s | 0.0085s | +3.7% | +| random-read 20,000 entries | raw fs | 0.1629s | 0.1912s | +17.4% | +| concurrent create+read+unlink (8×4,000×3) | mem | 0.2750s | 0.2791s | +1.5% | +| concurrent create+read+unlink (8×4,000×3) | raw fs | 5.6498s | 6.1583s | +9.0% | +| mount 500 backends | vfs | 0.0054s | 0.0060s | +11.1% | +| resolve, 500 mounts | vfs | 0.0020s | 0.0022s | +10.0% | +| unmount 500 backends | vfs | 0.0046s | 0.0046s | +0.0% | +| mount 2,000 backends | vfs | 0.0831s | 0.0863s | +3.9% | +| resolve, 2,000 mounts | vfs | 0.0296s | 0.0329s | +11.1% | +| unmount 2,000 backends | vfs | 0.0758s | 0.0780s | +2.9% | +| mount 8,000 backends | vfs | 1.4739s | 1.5380s | +4.3% | +| resolve, 8,000 mounts | vfs | 0.4938s | 0.5298s | +7.3% | +| unmount 8,000 backends | vfs | 1.5172s | 1.5191s | +0.1% | + +Almost every row sits within ±15% of the documented figures — consistent +with the single-run jitter `BENCH.md` already warns about, not a +regression. Two rows exceed that: `unlink 20,000 files (dir)` (+21.7%, +plausible container/host I/O noise, same direction as the other `dir` +metadata rows this run) and `write 1MB (raw+fsync)` (+88.9%, but this is a +tiny absolute value — 18ms vs 34ms for one syscall — the single number most +sensitive to one slow `fsync` on this container's overlay filesystem, not a +meaningful regression at that scale). Total wall-clock: 4m7.7s this run vs +5m23.9s documented, itself within the same single-run variance. + ## Quick example ```c diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0f3536a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,86 @@ +# Security Policy + +## Supported versions + +No version has been tagged yet (see `CHANGELOG.md`); this project is +pre-1.0 (`README.md`'s "Status"). Only the latest commit on the default +branch is supported — there is no LTS branch and no backport policy while +the project is at `0.x`. + +## What PackFS actually claims as a security boundary + +Stated precisely, per this project's documentation standard (`CLAUDE.md`), +rather than a generic "we take security seriously": + +- **Path containment for `dir` mounts is a hard requirement** (`concept.md` + Section 6, `CLAUDE.md`'s "Load-bearing constraints"). On Linux 5.6+, + every lookup beneath a `dir` mount uses `openat2(RESOLVE_BENEATH | + RESOLVE_NO_SYMLINKS)` to atomically block `..` escapes, symlink escapes, + and the canonicalize-then-open TOCTOU window in one kernel call. + `vfs_harden_process_with_landlock` (Section 6.2) is an additional, + **opt-in** kernel-level layer — not applied automatically, since + Landlock restrictions are cumulative, irreversible, and process-wide, + which would be a surprising side effect for an embeddable library to + trigger on its own. +- **On platforms or kernels without `openat2`, containment falls back to + per-component `O_NOFOLLOW` plus canonicalize-and-verify** (Section 6.3) + — an accepted, **explicitly weaker** residual-risk posture, not a claim + of equal strength. Code and documentation that describe this fallback as + equivalent to the `openat2` path would themselves be a bug worth + reporting. +- **The VFS core canonicalizes `.`/`..` in the virtual namespace itself**, + before any backend is reached, so a crafted path cannot escape a mount's + prefix even when no host directory is involved. +- **A pack file is treated as untrusted input unless it came from this + run's own compaction** (Section 7). Every index entry's offsets and + sizes are bounds-checked against the actual file/strings-region length + before being trusted; a failed check invalidates the whole pack load. An + externally-sourced pack additionally needs a checksum verified at load. + Loading an attacker-supplied `.img` file that PackFS accepts without + detecting corruption, or that causes an out-of-bounds read/write, is a + security bug. +- **Journal records are length/checksum self-describing** (Section 4.4), + so replay stops cleanly at the first torn or corrupted record rather + than reading past it. + +## What PackFS explicitly does *not* claim + +Reporting these as vulnerabilities is not necessary — they are documented, +known limitations, not silent gaps: + +- **`mode` (permission bits, symlinks, hard links) is stored and restored + but not enforced.** Nothing in the VFS core interprets it as an access + control boundary in v0 (`concept.md` Section 10). +- **Cross-process concurrency is specified (an LMDB-style reader-table + design, Section 5.6) but not implemented.** Two uncoordinated processes + writing the same host directory through a `dir` mount are not + synchronized against each other. +- **`zip`/`tar` are permanently out of scope** (`CLAUDE.md`, "Project + decisions that supersede concept.md") — there is no archive-format + parsing code in this project to have a vulnerability in. + +## Reporting a vulnerability + +Please do not open a public GitHub issue for a suspected security +vulnerability. Once this repository has a hosting remote and private +security advisories are available there, prefer that. Until then, or as an +alternative, report privately to the maintainer: + +**retoor** — `retoor@molodetz.nl` + +When reporting, include: the affected function(s) or file(s), a minimal +reproduction (ideally as a `tests/test_*.c`-style program using only the +public API in `include/packfs.h`), and which of the guarantees above (or +which `concept.md` section) you believe is violated — this project's own +verification discipline (`make test`, ASan/UBSan/TSan per `CONTRIBUTING.md`) +depends on being able to turn a report into a permanent regression test. + +## Verification this project already does + +Every change to `src/upper.c`, `src/overlay.c`, or `src/vfs.c` is required +to pass under `-fsanitize=address,undefined` and `-fsanitize=thread` +before being considered done (`CLAUDE.md`) — this process has already +caught and fixed a real heap-use-after-free in the snapshot-reclamation +logic during development (see the `reclaim_gate` note in `src/internal.h`). +`.github/workflows/ci.yml` runs the full test suite plus both sanitizer +passes on every push. diff --git a/include/packfs.h b/include/packfs.h index 7f65bbd..6bb7549 100644 --- a/include/packfs.h +++ b/include/packfs.h @@ -15,6 +15,20 @@ #include #include +/* 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" + #ifdef __cplusplus extern "C" { #endif @@ -86,6 +100,13 @@ typedef struct VfsDir { Vfs *vfs_new(void); void vfs_free(Vfs *v); +/* 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); + /* --- Backend constructors ------------------------------------------- * * Every backend is a Backend* handed to vfs_mount. Backends not currently diff --git a/packfs.pc.in b/packfs.pc.in new file mode 100644 index 0000000..7ae81d0 --- /dev/null +++ b/packfs.pc.in @@ -0,0 +1,10 @@ +prefix=@PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include + +Name: packfs +Description: Statically linked, in-process virtual file system for C +Version: @VERSION@ +Libs: -L${libdir} -lpackfs -lpthread +Cflags: -I${includedir} diff --git a/src/containment.c b/src/containment.c index 53cc06d..fcd2666 100644 --- a/src/containment.c +++ b/src/containment.c @@ -9,6 +9,8 @@ * per-component O_NOFOLLOW resolution, which is an accepted, weaker * residual-risk posture per Section 6.3 — not a claim of equal * containment strength. + * + * SPDX-License-Identifier: MIT */ #include diff --git a/src/hash.c b/src/hash.c index ffc3218..68c99e9 100644 --- a/src/hash.c +++ b/src/hash.c @@ -5,6 +5,8 @@ * static-linking constraint (Section 11.1) — this is not a * cryptographic hash and must never be used where adversarial collision * resistance is required. + * + * SPDX-License-Identifier: MIT */ #include "internal.h" diff --git a/src/internal.h b/src/internal.h index 8590420..a63f6dc 100644 --- a/src/internal.h +++ b/src/internal.h @@ -4,6 +4,8 @@ * Every mechanism named here corresponds to a section of concept.md; * comments reference sections rather than re-deriving the rationale, * since concept.md is frozen and is the authoritative source for "why." + * + * SPDX-License-Identifier: MIT */ #ifndef PACKFS_INTERNAL_H diff --git a/src/overlay.c b/src/overlay.c index 3eafd4b..075ffbe 100644 --- a/src/overlay.c +++ b/src/overlay.c @@ -15,6 +15,8 @@ * robust choice for that workload character, at the cost of journal * size for large files under many small writes. That trade-off is * deliberate, not an oversight. + * + * SPDX-License-Identifier: MIT */ #include diff --git a/src/pack.c b/src/pack.c index a2834e4..042af8c 100644 --- a/src/pack.c +++ b/src/pack.c @@ -10,6 +10,8 @@ * blobs (index_offset - sizeof(PackHeader) bytes) * PackIndexEntry[index_count] at index_offset, sorted by name (9.1) * strings (each name NUL-terminated) at strings_offset + * + * SPDX-License-Identifier: MIT */ #include diff --git a/src/path.c b/src/path.c index 971eff3..6468c57 100644 --- a/src/path.c +++ b/src/path.c @@ -4,6 +4,8 @@ * Resolves "." and ".." purely lexically, before any backend is reached, * so a crafted path cannot escape a mount's prefix even when no host * directory is involved. + * + * SPDX-License-Identifier: MIT */ #include diff --git a/src/upper.c b/src/upper.c index 96bdf79..7b92fec 100644 --- a/src/upper.c +++ b/src/upper.c @@ -10,6 +10,8 @@ * and never touch the snapshot pointer, per the structural/content split * in Section 5.3. Buffer growth on the `mem` side follows the * replace-don't-mutate discipline of Section 5.7. + * + * SPDX-License-Identifier: MIT */ #include diff --git a/src/vfs.c b/src/vfs.c index 12c2b7b..ed08f61 100644 --- a/src/vfs.c +++ b/src/vfs.c @@ -2,6 +2,8 @@ * vfs.c — VFS core: the mount table (itself part of the Section 5.3 * snapshot, per that section's mount-table addition), virtual-namespace * canonicalization dispatch (Section 6.1), and the public API. + * + * SPDX-License-Identifier: MIT */ #include @@ -56,6 +58,10 @@ static void mount_retire(Vfs *v, MountSnapshot *old) { pthread_rwlock_unlock(&v->reclaim_gate); } +const char *pfs_version(void) { + return PACKFS_VERSION_STRING; +} + Vfs *vfs_new(void) { Vfs *v = (Vfs *)calloc(1, sizeof(Vfs)); pthread_mutex_init(&v->writer_lock, NULL); diff --git a/tests/test_mem.c b/tests/test_mem.c index f7acbe4..8655d55 100644 --- a/tests/test_mem.c +++ b/tests/test_mem.c @@ -5,6 +5,16 @@ #include "test_harness.h" int main(void) { + /* pfs_version() must report exactly what this header's own macros say, + * so a dynamically-linked consumer's runtime check and a + * statically-linked consumer's compile-time PACKFS_VERSION_* agree by + * construction, not by two places having to be kept in sync by hand. */ + CHECK_STR_EQ(pfs_version(), PACKFS_VERSION_STRING); + CHECK_STR_EQ(PACKFS_VERSION_STRING, "0.1.0"); + CHECK_EQ_INT(PACKFS_VERSION_MAJOR, 0); + CHECK_EQ_INT(PACKFS_VERSION_MINOR, 1); + CHECK_EQ_INT(PACKFS_VERSION_PATCH, 0); + Vfs *v = vfs_new(); Backend *mem = backend_mem_new(); CHECK_EQ_INT(vfs_mount(v, "/", mem), VFS_OK);