SchOS/kernel/fs/schfs/obj.c
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:05:12 +02:00

587 lines
20 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// SchFS object layer, read side: inodes, directories and file data.
//
// Everything here is a record in the one B-tree, which is the point of the
// btrfs-style design — there is no inode table, no directory block format and
// no indirect-block scheme to get wrong, only key types:
//
// (ino, INODE, 0) struct schfs_inode
// (dir, DIRENT, hash) packed list of schfs_dirent_rec + name
// (ino, EXTENT, fileblk) struct schfs_extent
// (CSUM, CSUM, diskblk) uint32 CRC32c of that data block
//
// Kernel code reaches all of this through a caller-supplied cursor, so no
// function here puts a 4096-byte block on the stack.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "core/types.h"
#include "core/defs.h"
#endif
// Point a cursor at the volume's current tree, keeping the caller's block
// buffer. Called before every operation rather than once at open, because the
// tree root moves on every commit under copy-on-write: a cursor that cached the
// old root would keep reading a stale — though perfectly valid — filesystem.
static void
cur_bind(struct schfs_cursor *c, struct schfs_vol *v)
{
c->t = &v->tree;
c->depth = 0;
c->valid = 0;
}
void
schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset)
{
memset(k, 0, sizeof(*k));
k->objectid = objectid;
k->type = type;
k->offset = offset;
}
// FNV-1a, 64-bit. Directory lookup is a tree descent on this value, so it wants
// to spread short, highly similar names ("ls", "ln", "cp") across the keyspace.
// FNV does that, is a dozen lines, and needs no table — worth more here than
// the marginally better distribution of something larger.
//
// Collisions are handled by the packed dirent list rather than assumed away;
// see struct schfs_dirent_rec.
// Test-only: narrows the hash so collisions become common.
//
// With a full 64-bit hash, two names in one directory colliding is so unlikely
// that the packed-list code which handles it would never run — and untested
// code that only executes on rare inputs is exactly the code that loses a file
// years later. Setting this to a small mask makes every directory a pile of
// collisions, so the round-trip test exercises the path properly. Always ~0
// in the kernel and in real images.
uint64_t schfs_hash_mask = ~0ULL;
uint64_t
schfs_namehash(const char *name, unsigned len)
{
uint64_t h = 1469598103934665603ULL; /* FNV offset basis */
for (unsigned i = 0; i < len; i++) {
h ^= (uint8_t)name[i];
h *= 1099511628211ULL; /* FNV prime */
}
// Key ordering is by this value, so a directory's entries come back in an
// order that has nothing to do with their names. That is fine for readdir
// (POSIX promises no order) and is what makes lookup one descent.
return h & schfs_hash_mask;
}
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
/* Inode cache.
*
* Sixteen entries, not one. `ls` is the shape that matters: it stats every name
* in a directory, so a single entry never hits and the whole directory costs
* two tree descents per file -- one to find the name, one to read the inode.
* Sixteen is enough that a directory walk keeps hitting while a second reader
* elsewhere does not evict everything.
*
* Round-robin replacement, deliberately: an LRU needs bookkeeping on every hit,
* and the access pattern here is a walk rather than a working set, so the
* cleverer policy buys nothing.
*
* Invalidation is blunt and total -- see schfs_cache_invalidate().
*/
#define INO_CACHE_N 16
static struct {
int valid;
const struct schfs_vol *vol;
uint64_t ino;
struct schfs_inode in;
} ino_cache[INO_CACHE_N];
static unsigned ino_cache_next;
int
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out)
{
for (unsigned i = 0; i < INO_CACHE_N; i++) {
if (ino_cache[i].valid && ino_cache[i].vol == v &&
ino_cache[i].ino == ino) {
*out = ino_cache[i].in;
return 0;
}
}
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
cur_bind(c, v);
unsigned len = sizeof(*out);
int r = schfs_btree_lookup_cur(c, &k, out, &len);
if (r != 0)
return r;
// A short record means an image written by an older or broken tool. Reject
// it rather than acting on a half-filled struct.
if (len != sizeof(*out))
return -2;
unsigned slot = ino_cache_next++ % INO_CACHE_N;
ino_cache[slot].valid = 1;
ino_cache[slot].vol = v;
ino_cache[slot].ino = ino;
ino_cache[slot].in = *out;
return 0;
}
int
schfs_snap_get(struct schfs_vol *v, struct schfs_cursor *c, uint64_t id,
struct schfs_snap *out)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, id);
cur_bind(c, v);
unsigned len = sizeof(*out);
int r = schfs_btree_lookup_cur(c, &k, out, &len);
if (r != 0)
return r;
if (len != sizeof(*out))
return -2;
return 0;
}
/* ------------------------------------------------------------------ */
/* Directories */
/* ------------------------------------------------------------------ */
// Walk a packed dirent list, calling fn per entry. Stops early if fn returns
// non-zero. Returns fn's value, or -2 if the list is malformed.
static int
dirent_walk(const void *val, unsigned len,
int (*fn)(const struct schfs_dirent_rec *, const char *, void *),
void *arg)
{
unsigned off = 0;
while (off < len) {
if (len - off < sizeof(struct schfs_dirent_rec))
return -2;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)((const char *)val + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (d->namelen == 0 || d->namelen > SCHFS_NAME_MAX || off + step > len)
return -2;
int r = fn(d, (const char *)d + sizeof(*d), arg);
if (r)
return r;
off += step;
}
return 0;
}
struct find_ctx {
const char *name;
unsigned namelen;
uint64_t ino;
uint16_t type;
};
static int
find_one(const struct schfs_dirent_rec *d, const char *nm, void *arg)
{
struct find_ctx *f = (struct find_ctx *)arg;
if (d->namelen != f->namelen)
return 0;
if (memcmp(nm, f->name, f->namelen) != 0)
return 0;
f->ino = d->objectid;
f->type = d->type;
return 1; /* found; stop */
}
int
schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
schfs_mkkey(&k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
cur_bind(c, v);
int r = schfs_cursor_seek(c, &k);
if (r == -2)
return -2;
if (r != 0)
return -1;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got || schfs_key_cmp(got, &k) != 0)
return -1;
unsigned len;
const void *val = schfs_cursor_val(c, &len);
if (!val)
return -2;
struct find_ctx f;
f.name = name; f.namelen = namelen; f.ino = 0; f.type = 0;
r = dirent_walk(val, len, find_one, &f);
if (r == -2)
return -2;
if (r != 1)
return -1; /* hash matched, name did not */
if (out_ino) *out_ino = f.ino;
if (out_type) *out_type = f.type;
return 0;
}
struct iter_ctx {
// The hash-aware form. schfs_dir_iterate's plain callback is wrapped in one
// of these, so there is one walk rather than two.
schfs_dir_fn fn;
void *arg;
struct schfs_cursor *cur;
uint64_t dir;
int stop;
};
// Hand each entry of one packed dirent list to the caller.
//
// The list lives in the cursor's block buffer, and almost every readdir caller
// immediately looks each entry up — to stat it, to recurse into it — through a
// cursor that may well share that buffer. Walking the list in place meant the
// name and every record after it turned to garbage the moment the callback
// touched the filesystem, which is how an extracted tree once ended up with
// directories reparented under their own children.
//
// The obvious fix, copying the whole item aside, costs SCHFS_MAX_INLINE (just
// over 1 KB) of stack — a quarter of the kernel's entire 4096-byte stack, on a
// path that also runs under procread's 2 KB buffer. So only the current entry
// is copied, and the item is re-fetched from the cursor each time round:
// schfs_cursor_val re-reads the block, which restores whatever the callback
// disturbed.
static int
iter_item(const struct schfs_key *k, const void *val, unsigned len, void *arg)
{
struct iter_ctx *ic = (struct iter_ctx *)arg;
(void)val;
// The range scan runs until something stops it, so stopping at the first
// key that is no longer this directory's is what bounds the walk.
if (k->objectid != ic->dir || k->type != SCHFS_KEY_DIRENT)
return 1;
if (len > SCHFS_MAX_INLINE)
return 1;
unsigned off = 0;
while (off < len) {
unsigned now;
const char *v = (const char *)schfs_cursor_val(ic->cur, &now);
if (!v || now != len) {
ic->stop = -2;
return 1;
}
if (len - off < sizeof(struct schfs_dirent_rec)) {
ic->stop = -2;
return 1;
}
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v + off);
unsigned namelen = d->namelen;
unsigned step = SCHFS_DIRENT_LEN(namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (namelen == 0 || namelen > SCHFS_NAME_MAX || off + step > len) {
ic->stop = -2;
return 1;
}
char name[SCHFS_NAME_MAX];
uint64_t oid = d->objectid;
uint16_t type = d->type;
memmove(name, (const char *)d + sizeof(*d), namelen);
int r = ic->fn(oid, type, name, namelen, k->offset, ic->arg);
if (r) {
ic->stop = r;
return 1;
}
off += step;
}
return 0;
}
// Walk a directory from a given point in its key space.
//
// `start_hash` is where to resume: entries are keyed (dir, DIRENT, hash), so
// seeking straight to a hash is what makes a partial listing continue in one
// descent instead of re-walking everything before it. See schfs_vfs_getdents.
int
schfs_dir_iterate_from(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
uint64_t start_hash, schfs_dir_fn fn, void *arg)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, start_hash);
struct iter_ctx ic;
ic.fn = fn; ic.arg = arg; ic.cur = c; ic.dir = dir; ic.stop = 0;
cur_bind(c, v);
int r = schfs_btree_range(c, &start, iter_item, &ic);
if (r == -2)
return -2;
return ic.stop == 1 ? 0 : ic.stop;
}
// The plain form, for callers that want the whole directory and do not care
// where in the key space an entry sat.
struct plain_ctx {
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *);
void *arg;
};
static int
plain_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
uint64_t hash, void *arg)
{
struct plain_ctx *p = arg;
(void)hash;
return p->fn(ino, type, name, namelen, p->arg);
}
int
schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
struct plain_ctx p = { fn, arg };
return schfs_dir_iterate_from(v, c, dir, 0, plain_entry, &p);
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
/* --------------------------------------------------------------------------
* Read caches: recently-read inodes (see schfs_inode_get) and the last extent.
*
* Every read used to descend the B-tree twice per block -- once for the inode
* and once for the extent -- so an 834 KB file cost 418 descents, each a tree
* walk plus a 4 KB copy out of the buffer cache per node visited. Sequential
* reads ask for the same inode every time and usually stay inside one extent,
* so both answers are already known.
*
* One entry each, not a table: the access pattern being optimised is one file
* read straight through, and a single entry catches all of it without needing
* eviction policy or a hash. A second reader simply misses.
*
* Correctness rests entirely on invalidation, so it is deliberately blunt:
* every write entry point in objw.c calls schfs_cache_invalidate() before it
* touches anything. A cache that is merely usually right is worse than none at
* all here -- a stale extent hands back another file's data.
*
* The volume pointer is part of the key because the host tools can hold more
* than one volume open, and two images would otherwise share these.
* ------------------------------------------------------------------------ */
static struct {
int valid;
const struct schfs_vol *vol;
uint64_t ino;
uint64_t first; /* first file block covered */
uint64_t nblocks; /* how many, from `first` */
uint64_t disk; /* disk block backing `first` */
} ext_cache;
void
schfs_cache_invalidate(void)
{
// Retires every cursor's resident block as well as the two caches below:
// an in-place rewrite leaves a stale copy that is indistinguishable from
// the real thing by inspection, so the only safe signal is "something
// changed". See cur_load in btree.c.
schfs_tree_epoch++;
for (unsigned i = 0; i < INO_CACHE_N; i++)
ino_cache[i].valid = 0;
ext_cache.valid = 0;
}
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
// corruption. Extents are keyed by the file block they start at, so the record
// governing block B is the last one at or before (ino, EXTENT, B).
static int
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t fblk, uint64_t *out)
{
if (ext_cache.valid && ext_cache.vol == v && ext_cache.ino == ino &&
fblk >= ext_cache.first && fblk < ext_cache.first + ext_cache.nblocks) {
*out = ext_cache.disk + (fblk - ext_cache.first);
return 1;
}
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
cur_bind(c, v);
int r = schfs_btree_lookup_le(c, &k);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got)
return 0;
// lookup_le can land on some other object's records entirely; a sparse file
// whose first extent starts past this block looks the same way.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
unsigned len;
const struct schfs_extent *e =
(const struct schfs_extent *)schfs_cursor_val(c, &len);
if (!e || len != sizeof(*e))
return -2;
if (fblk >= got->offset + e->nblocks)
return 0; /* hole past the end of this extent */
ext_cache.valid = 1;
ext_cache.vol = v;
ext_cache.ino = ino;
ext_cache.first = got->offset;
ext_cache.nblocks = e->nblocks;
ext_cache.disk = e->disk_block;
*out = e->disk_block + (fblk - got->offset);
return 1;
}
// Verify a data block against its checksum record. Tree nodes carry their
// checksum in the parent pointer; data blocks have no parent, so the check must
// be an explicit lookup. Returns 0 if good, -2 if it fails, and 0 as well when
// no record exists — an image may legitimately predate checksums for a block,
// and refusing to read data over a *missing* checksum would turn an upgrade
// into a total loss.
static int
csum_check(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dblk,
const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t want;
unsigned len = sizeof(want);
cur_bind(c, v);
int r = schfs_btree_lookup_cur(c, &k, &want, &len);
if (r == -1)
return 0;
if (r != 0 || len != sizeof(want))
return -2;
if (schfs_crc32c(0, data, SCHFS_BLKSIZE) != want)
return -2;
return 0;
}
long
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk)
{
/* schfs_inode_get caches for itself now, so this is a hit on every chunk
* after the first without a second cache here to keep coherent. */
struct schfs_inode in;
int r = schfs_inode_get(v, c, ino, &in);
if (r != 0)
return r;
if (off >= in.size)
return 0;
if (off + n > in.size)
n = (unsigned)(in.size - off);
char *dst = (char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t fblk = (off + done) / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)((off + done) % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
uint64_t disk = 0;
int m = extent_map(v, c, ino, fblk, &disk);
if (m == -2)
return -2;
if (m == 0) {
memset(dst + done, 0, want); /* hole reads as zeros */
} else {
if (v->dev.bread(v->dev.ctx, disk, dblk) != 0)
return -2;
if (csum_check(v, c, disk, dblk) != 0)
return -2;
memmove(dst + done, (char *)dblk + inblk, want);
}
done += want;
}
return (long)done;
}
/* ------------------------------------------------------------------ */
/* Path resolution */
/* ------------------------------------------------------------------ */
uint64_t
schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path)
{
uint64_t cur = v->root_objectid;
while (*path == '/')
path++;
while (*path) {
const char *s = path;
while (*path && *path != '/')
path++;
unsigned len = (unsigned)(path - s);
while (*path == '/')
path++;
if (len == 1 && s[0] == '.')
continue;
if (len == 2 && s[0] == '.' && s[1] == '.') {
// ".." is synthetic: directories store a parent id rather than an
// entry, so walking up is an inode read, not a lookup.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
cur = in.parent ? in.parent : v->root_objectid;
continue;
}
// Every component but the last must be a directory. Checking here is
// what makes "cat /etc/passwd/x" fail rather than silently succeed.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
if (in.type != SCHFS_T_DIR)
return 0;
uint64_t next;
if (schfs_dir_lookup(v, c, cur, s, len, &next, 0) != 0)
return 0;
cur = next;
}
return cur;
}