SchOS/kernel/fs/schfs/btree.c
Nexoniarz d0f7acf9eb Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
2026-08-09 13:47:01 +02:00

1181 lines
38 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// SchFS copy-on-write B-tree — see btree.h for the contract.
//
// Leaf layout follows btrfs: the item array grows forward from just past the
// header while the values grow backward from the end of the block, so a leaf is
// full when the two meet. That lets one node hold a few large values or many
// small ones without fixing a record size in the format.
//
// Every mutation rewrites the path from the touched leaf up to the root. A node
// that already belongs to the transaction group being built is rewritten in
// place; anything older is copied first, so nothing reachable from a published
// root is ever modified and a crash at any instant leaves the previous tree
// intact.
#include "btree.h"
// memmove throughout rather than memcpy: a caller may legitimately hand
// leaf_put a value that points into the very block being written (the cursor
// hands out pointers into its own buffer), and the kernel declares memmove
// but not memcpy.
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* CRC32c (Castagnoli), table-free so this stays usable very early. */
/* ------------------------------------------------------------------ */
uint32_t
schfs_crc32c(uint32_t crc, const void *buf, unsigned len)
{
const uint8_t *p = (const uint8_t *)buf;
crc = ~crc;
while (len--) {
crc ^= *p++;
for (int i = 0; i < 8; i++)
crc = (crc >> 1) ^ (0x82F63B78u & (uint32_t)(-(int32_t)(crc & 1)));
}
return ~crc;
}
/* ------------------------------------------------------------------ */
/* Keys */
/* ------------------------------------------------------------------ */
int
schfs_key_cmp(const struct schfs_key *a, const struct schfs_key *b)
{
if (a->objectid < b->objectid) return -1;
if (a->objectid > b->objectid) return 1;
if (a->type < b->type) return -1;
if (a->type > b->type) return 1;
if (a->offset < b->offset) return -1;
if (a->offset > b->offset) return 1;
return 0;
}
/* ------------------------------------------------------------------ */
/* Node accessors */
/* ------------------------------------------------------------------ */
/* Short names for the accessors btree.h publishes. */
#define HDR(b) SCHFS_HDR(b)
#define KPTRS(b) SCHFS_KPTRS(b)
#define ITEMS(b) SCHFS_ITEMS(b)
// Bytes still usable in a leaf: the gap between the end of the item array and
// the start of the value area.
static int
leaf_free_space(const void *blk)
{
const struct schfs_node_hdr *h = (const struct schfs_node_hdr *)blk;
const struct schfs_item *it = (const struct schfs_item *)
((const char *)blk + sizeof(*h));
int items_end = (int)sizeof(*h) + h->nritems * (int)sizeof(struct schfs_item);
int data_start = SCHFS_BLKSIZE;
for (int i = 0; i < h->nritems; i++)
if ((int)it[i].offset < data_start)
data_start = (int)it[i].offset;
return data_start - items_end;
}
// First key held anywhere in this subtree's node. The parent files a child
// under exactly this key; see the note on the separator invariant below.
static void
node_first_key(const void *blk, struct schfs_key *out)
{
if (HDR(blk)->nritems == 0)
memset(out, 0, sizeof(*out));
else if (HDR(blk)->level)
*out = KPTRS(blk)[0].key;
else
*out = ITEMS(blk)[0].key;
}
static void
node_seal(void *blk, uint64_t blocknr, uint64_t gen, uint32_t *crc_out)
{
struct schfs_node_hdr *h = HDR(blk);
h->magic = SCHFS_NODE_MAGIC;
h->blocknr = blocknr;
h->generation = gen;
h->crc = 0;
uint32_t c = schfs_crc32c(0, blk, SCHFS_BLKSIZE);
h->crc = c;
if (crc_out)
*crc_out = c;
}
// Read a block and validate it against the checksum recorded in its parent.
// want_crc of 0 means "no expectation" (used for the root, whose checksum lives
// in the uberblock and is passed in separately).
static int
node_read(struct schfs_bdev *dev, uint64_t blk, uint32_t want_crc, void *out)
{
if (dev->bread(dev->ctx, blk, out) != 0)
return -2;
struct schfs_node_hdr *h = HDR(out);
if (h->magic != SCHFS_NODE_MAGIC)
return -2;
// Self-pointer check: a block written to the wrong address still has a
// valid checksum, so only this catches a misdirected write.
if (h->blocknr != blk)
return -2;
// Two bounds the rest of the file then takes for granted.
if (h->level > SCHFS_MAX_LEVEL)
return -2;
if (h->level ? h->nritems > SCHFS_MAX_KPTR : h->nritems > SCHFS_MAX_ITEMS)
return -2;
uint32_t stored = h->crc;
h->crc = 0;
uint32_t calc = schfs_crc32c(0, out, SCHFS_BLKSIZE);
h->crc = stored;
if (calc != stored)
return -2;
if (want_crc && want_crc != stored)
return -2;
return 0;
}
int
schfs_node_read(struct schfs_bdev *dev, uint64_t blk, uint32_t want_crc,
void *out)
{
return node_read(dev, blk, want_crc, out);
}
// Index of the first entry with key >= k, plus whether it matched exactly.
static int
node_search(const void *blk, const struct schfs_key *k, int *exact)
{
const struct schfs_node_hdr *h = (const struct schfs_node_hdr *)blk;
int lo = 0, hi = h->nritems - 1, found = -1;
*exact = 0;
while (lo <= hi) {
int mid = (lo + hi) / 2;
const struct schfs_key *mk = h->level
? &((const struct schfs_kptr *)((const char *)blk + sizeof(*h)))[mid].key
: &((const struct schfs_item *)((const char *)blk + sizeof(*h)))[mid].key;
int c = schfs_key_cmp(mk, k);
if (c == 0) { *exact = 1; return mid; }
if (c < 0) { found = mid; lo = mid + 1; }
else { hi = mid - 1; }
}
// For an internal node we want the child that could contain k, i.e. the
// last entry whose key is <= k (or the first child if k precedes all keys).
return h->level ? (found < 0 ? 0 : found) : lo;
}
// Where a *new* separator key belongs in an internal node: the first index
// whose key is greater than k. Distinct from node_search, which answers "which
// child owns k" and would place a key smaller than every separator at index 1.
static int
kptr_insert_pos(const void *blk, const struct schfs_key *k)
{
const struct schfs_node_hdr *h = (const struct schfs_node_hdr *)blk;
const struct schfs_kptr *kp = (const struct schfs_kptr *)
((const char *)blk + sizeof(*h));
int lo = 0, hi = h->nritems;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (schfs_key_cmp(&kp[mid].key, k) < 0)
lo = mid + 1;
else
hi = mid;
}
return lo;
}
// Decide where a node's new contents go.
//
// A block stamped with the transaction group now being built was allocated
// after the last commit, so no published root can reach it: rewriting it in
// place is invisible to any reader of the committed tree and costs nothing.
// Anything older must be copied, which is the copy-on-write rule itself.
//
// Without this every single insert copies the entire root-to-leaf path, so a
// transaction touching a thousand records burns a thousand paths' worth of
// blocks before it can commit and release any of them.
static uint64_t
cow_block(struct schfs_btree *t, const void *blk, uint64_t old)
{
if (HDR(blk)->generation == t->generation)
return old;
uint64_t nb = t->dev->balloc(t->dev->ctx);
if (nb == 0)
return 0;
t->dev->bfree(t->dev->ctx, old);
return nb;
}
// Seal a node into `blk_nr` and write it out.
static int
node_flush(struct schfs_btree *t, void *blk, uint64_t blk_nr, uint32_t *crc)
{
node_seal(blk, blk_nr, t->generation, crc);
return t->dev->bwrite(t->dev->ctx, blk_nr, blk) == 0 ? 0 : -2;
}
/* ------------------------------------------------------------------ */
/* Create */
/* ------------------------------------------------------------------ */
int
schfs_btree_create(struct schfs_btree *t, struct schfs_bdev *dev,
struct schfs_ctx *x)
{
t->dev = dev;
uint64_t b = dev->balloc(dev->ctx);
if (b == 0)
return -3;
memset(x->a, 0, SCHFS_BLKSIZE);
HDR(x->a)->level = 0;
HDR(x->a)->nritems = 0;
if (node_flush(t, x->a, b, &t->root_crc) != 0)
return -2;
t->root = b;
t->root_gen = t->generation;
return 0;
}
/* ------------------------------------------------------------------ */
/* Cursor */
/* */
/* One block buffer, owned by the caller, plus a path of block numbers. */
/* Moving back up re-reads the parent instead of keeping it in memory, */
/* which trades a buffer-cache hit for seven pages of stack. */
/* ------------------------------------------------------------------ */
void
schfs_cursor_init(struct schfs_cursor *c, struct schfs_btree *t, void *blk)
{
c->t = t;
c->blk = blk;
c->depth = 0;
c->valid = 0;
}
// Load path[d]'s block into the cursor buffer, verifying it against the
// checksum recorded alongside it.
static int
cur_load(struct schfs_cursor *c, int d)
{
return node_read(c->t->dev, c->path[d].blocknr, c->path[d].crc, c->blk);
}
// From the node currently in the buffer at depth `d`, walk down to a leaf,
// taking the first entry of each node (rightmost=0) or the last (rightmost=1).
// Leaves the leaf loaded and path[c->depth].index set. Returns 0, or -1 if the
// leaf reached is empty, or -2 on corruption.
static int
cur_descend(struct schfs_cursor *c, int d, int rightmost)
{
for (;;) {
struct schfs_node_hdr *h = HDR(c->blk);
if (h->level == 0) {
if (h->nritems == 0)
return -1;
c->depth = d;
c->path[d].index = rightmost ? h->nritems - 1 : 0;
return 0;
}
if (h->nritems == 0)
return -2;
int i = rightmost ? h->nritems - 1 : 0;
c->path[d].index = i;
struct schfs_kptr *kp = &KPTRS(c->blk)[i];
if (d + 1 > SCHFS_MAX_LEVEL)
return -2;
c->path[d + 1].blocknr = kp->blocknr;
c->path[d + 1].crc = kp->crc;
d++;
if (cur_load(c, d) != 0)
return -2;
}
}
// Shared body of next/prev. `dir` is +1 or -1.
static int
cur_step(struct schfs_cursor *c, int dir)
{
if (!c->valid)
return -1;
for (;;) {
// Try to move within the leaf we are already holding.
if (cur_load(c, c->depth) != 0) {
c->valid = 0;
return -2;
}
int n = HDR(c->blk)->nritems;
int i = c->path[c->depth].index + dir;
if (i >= 0 && i < n) {
c->path[c->depth].index = i;
return 0;
}
// Off the end of this leaf: climb until a parent has another child in
// the direction of travel, then descend to that subtree's edge.
int d = c->depth - 1;
for (; d >= 0; d--) {
if (cur_load(c, d) != 0) {
c->valid = 0;
return -2;
}
int pn = HDR(c->blk)->nritems;
int pi = c->path[d].index + dir;
if (pi >= 0 && pi < pn) {
c->path[d].index = pi;
struct schfs_kptr *kp = &KPTRS(c->blk)[pi];
c->path[d + 1].blocknr = kp->blocknr;
c->path[d + 1].crc = kp->crc;
if (cur_load(c, d + 1) != 0) {
c->valid = 0;
return -2;
}
int r = cur_descend(c, d + 1, dir < 0);
if (r == -2) {
c->valid = 0;
return -2;
}
// r == -1 means that subtree bottomed out in an empty leaf. The
// outer loop simply steps again from there. Delete frees leaves
// that empty, so this is defensive rather than routine.
if (r == 0)
return 0;
break;
}
}
if (d < 0) {
c->valid = 0;
return -1; /* ran off the end of the tree */
}
}
}
int
schfs_cursor_next(struct schfs_cursor *c)
{
return cur_step(c, +1);
}
int
schfs_cursor_prev(struct schfs_cursor *c)
{
return cur_step(c, -1);
}
int
schfs_cursor_seek(struct schfs_cursor *c, const struct schfs_key *start)
{
c->depth = 0;
c->valid = 0;
c->path[0].blocknr = c->t->root;
c->path[0].crc = c->t->root_crc;
if (cur_load(c, 0) != 0)
return -2;
if (start == NULL) {
int r = cur_descend(c, 0, 0);
if (r == -2)
return -2;
if (r == -1)
return -1; /* empty tree */
c->valid = 1;
return 0;
}
int d = 0;
for (;;) {
struct schfs_node_hdr *h = HDR(c->blk);
int exact;
int i = node_search(c->blk, start, &exact);
if (h->level == 0) {
c->depth = d;
if (i < h->nritems) {
c->path[d].index = i;
c->valid = 1;
return 0;
}
// Every key in this leaf precedes `start`; the answer, if any, is
// the next record in the tree. Park on the last item and step.
if (h->nritems == 0) {
// Empty leaf: fabricate a position so cur_step can climb out.
c->path[d].index = 0;
c->valid = 1;
return schfs_cursor_next(c);
}
c->path[d].index = h->nritems - 1;
c->valid = 1;
return schfs_cursor_next(c);
}
if (h->nritems == 0)
return -2;
c->path[d].index = i;
struct schfs_kptr *kp = &KPTRS(c->blk)[i];
if (d + 1 > SCHFS_MAX_LEVEL)
return -2;
c->path[d + 1].blocknr = kp->blocknr;
c->path[d + 1].crc = kp->crc;
d++;
if (cur_load(c, d) != 0)
return -2;
}
}
const struct schfs_key *
schfs_cursor_key(struct schfs_cursor *c)
{
if (!c->valid)
return NULL;
if (cur_load(c, c->depth) != 0) {
c->valid = 0;
return NULL;
}
if (c->path[c->depth].index >= HDR(c->blk)->nritems)
return NULL;
return &ITEMS(c->blk)[c->path[c->depth].index].key;
}
const void *
schfs_cursor_val(struct schfs_cursor *c, unsigned *len)
{
if (!c->valid)
return NULL;
if (cur_load(c, c->depth) != 0) {
c->valid = 0;
return NULL;
}
if (c->path[c->depth].index >= HDR(c->blk)->nritems)
return NULL;
struct schfs_item *it = &ITEMS(c->blk)[c->path[c->depth].index];
// A value must lie wholly inside the block and clear the item array. The
// checksum already proves the block is the one that was written, but not
// that the tool which wrote it was sane, and this pointer is about to be
// handed to a caller that will read `size` bytes from it.
int items_end = (int)sizeof(struct schfs_node_hdr)
+ HDR(c->blk)->nritems * (int)sizeof(struct schfs_item);
if ((int)it->offset < items_end ||
(int)it->offset + (int)it->size > SCHFS_BLKSIZE)
return NULL;
if (len)
*len = it->size;
return (const char *)c->blk + it->offset;
}
int
schfs_btree_lookup_cur(struct schfs_cursor *c, const struct schfs_key *k,
void *buf, unsigned *len)
{
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 vlen;
const void *v = schfs_cursor_val(c, &vlen);
if (!v)
return -2;
if (buf && len) {
unsigned n = vlen < *len ? vlen : *len;
memmove(buf, v, n);
}
if (len)
*len = vlen;
return 0;
}
int
schfs_btree_lookup(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, void *buf, unsigned *len)
{
struct schfs_cursor c;
schfs_cursor_init(&c, t, x->a);
return schfs_btree_lookup_cur(&c, k, buf, len);
}
int
schfs_btree_lookup_le(struct schfs_cursor *c, const struct schfs_key *k)
{
int r = schfs_cursor_seek(c, k);
if (r == -2)
return -2;
if (r == -1) {
// Nothing at or after k, so the last record in the tree — if there is
// one — is the predecessor. Descend rightmost from the root, which is
// O(depth) rather than stepping the whole tree.
c->depth = 0;
c->valid = 0;
c->path[0].blocknr = c->t->root;
c->path[0].crc = c->t->root_crc;
if (cur_load(c, 0) != 0)
return -2;
if (cur_descend(c, 0, 1) != 0)
return -1;
c->valid = 1;
return 0;
}
const struct schfs_key *got = schfs_cursor_key(c);
if (got && schfs_key_cmp(got, k) == 0)
return 0; /* exact hit */
return schfs_cursor_prev(c); /* -1 if k precedes everything */
}
/* ------------------------------------------------------------------ */
/* Leaf edit helpers */
/* ------------------------------------------------------------------ */
// Rebuild a leaf's value area compactly, using `tmp` as staging. Deleting and
// replacing leave holes; rather than track them, the leaf is repacked whenever
// it is modified, which is cheap next to the block write that follows.
static void
leaf_compact(void *blk, void *tmp)
{
struct schfs_node_hdr *h = HDR(blk);
struct schfs_item *it = ITEMS(blk);
int n = h->nritems;
int wr = SCHFS_BLKSIZE;
memmove(tmp, blk, SCHFS_BLKSIZE);
for (int i = 0; i < n; i++) {
struct schfs_item *src = &((struct schfs_item *)
((char *)tmp + sizeof(*h)))[i];
wr -= (int)src->size;
memmove((char *)blk + wr, (char *)tmp + src->offset, src->size);
it[i].key = src->key;
it[i].size = src->size;
it[i].offset = (uint32_t)wr;
}
}
// Insert (or replace) one item in an already-copied leaf. Returns 0, or -1 if
// it does not fit and the leaf must be split. `tmp` is repacking scratch.
static int
leaf_put(void *blk, const struct schfs_key *k, const void *val, unsigned len,
void *tmp)
{
struct schfs_node_hdr *h = HDR(blk);
struct schfs_item *it = ITEMS(blk);
int exact;
int i = node_search(blk, k, &exact);
if (exact) {
// Replace: drop the old item, then fall through to a clean insert so
// the size may change freely.
for (int j = i; j < h->nritems - 1; j++)
it[j] = it[j + 1];
h->nritems--;
leaf_compact(blk, tmp);
i = node_search(blk, k, &exact);
}
if (h->nritems >= SCHFS_MAX_ITEMS)
return -1;
int need = (int)sizeof(struct schfs_item) + (int)len;
if (need > leaf_free_space(blk))
return -1;
for (int j = h->nritems; j > i; j--)
it[j] = it[j - 1];
int data_start = SCHFS_BLKSIZE;
for (int j = 0; j < h->nritems; j++) {
int idx = (j >= i) ? j + 1 : j;
if ((int)it[idx].offset < data_start)
data_start = (int)it[idx].offset;
}
int off = data_start - (int)len;
it[i].key = *k;
it[i].offset = (uint32_t)off;
it[i].size = len;
memmove((char *)blk + off, val, len);
h->nritems++;
return 0;
}
// Remove the item at index i.
static void
leaf_drop(void *blk, int i, void *tmp)
{
struct schfs_node_hdr *h = HDR(blk);
struct schfs_item *it = ITEMS(blk);
for (int j = i; j < h->nritems - 1; j++)
it[j] = it[j + 1];
h->nritems--;
leaf_compact(blk, tmp);
}
/* ------------------------------------------------------------------ */
/* Internal-node edit helpers */
/* ------------------------------------------------------------------ */
// Insert a (key -> child) pair into an internal node that is already ours to
// write. Returns 0, or -1 if it is full.
static int
internal_put(void *blk, const struct schfs_key *k, uint64_t child,
uint32_t crc, uint64_t gen)
{
struct schfs_node_hdr *h = HDR(blk);
struct schfs_kptr *kp = KPTRS(blk);
if (h->nritems >= SCHFS_MAX_KPTR)
return -1;
int i = kptr_insert_pos(blk, k);
for (int j = h->nritems; j > i; j--)
kp[j] = kp[j - 1];
kp[i].key = *k;
kp[i].blocknr = child;
kp[i].crc = crc;
kp[i].generation = gen;
kp[i].pad = 0;
h->nritems++;
return 0;
}
static void
internal_drop(void *blk, int i)
{
struct schfs_node_hdr *h = HDR(blk);
struct schfs_kptr *kp = KPTRS(blk);
for (int j = i; j < h->nritems - 1; j++)
kp[j] = kp[j + 1];
h->nritems--;
}
/* ------------------------------------------------------------------ */
/* Split */
/* ------------------------------------------------------------------ */
// Move the upper half of `blk` into `nb`, which the caller then seals and
// writes at the returned block number. `tmp` is only touched for leaves.
// Returns 0 if no block could be allocated.
static uint64_t
node_split(struct schfs_btree *t, void *blk, void *nb, void *tmp,
struct schfs_key *split_key)
{
struct schfs_node_hdr *h = HDR(blk);
int n = h->nritems;
int half = n / 2;
uint64_t newb = t->dev->balloc(t->dev->ctx);
if (newb == 0)
return 0;
memset(nb, 0, SCHFS_BLKSIZE);
HDR(nb)->level = h->level;
HDR(nb)->nritems = 0;
if (h->level) {
struct schfs_kptr *src = KPTRS(blk);
struct schfs_kptr *dst = KPTRS(nb);
for (int i = half; i < n; i++)
dst[i - half] = src[i];
HDR(nb)->nritems = (uint16_t)(n - half);
h->nritems = (uint16_t)half;
*split_key = dst[0].key;
} else {
struct schfs_item *src = ITEMS(blk);
struct schfs_item *dst = ITEMS(nb);
int wr = SCHFS_BLKSIZE;
for (int i = half; i < n; i++) {
wr -= (int)src[i].size;
memmove((char *)nb + wr, (char *)blk + src[i].offset, src[i].size);
dst[i - half].key = src[i].key;
dst[i - half].size = src[i].size;
dst[i - half].offset = (uint32_t)wr;
}
HDR(nb)->nritems = (uint16_t)(n - half);
h->nritems = (uint16_t)half;
leaf_compact(blk, tmp);
*split_key = dst[0].key;
}
return newb;
}
/* ------------------------------------------------------------------ */
/* Insert */
/* */
/* The separator invariant: kp[i].key is exactly the first key held in */
/* child i. Every path rewrite therefore refreshes the parent's key as */
/* well as its pointer, which is the whole reason a split key can */
/* always be filed immediately after the child it split. */
/* */
/* The weaker "kp[i].key <= everything in child i" would also make */
/* lookups work, and an earlier version relied on it. It breaks on */
/* insert: descending inserts push a child's first key below its */
/* parent's key for it, and splitting that child then yields a */
/* separator smaller than kp[0].key, which has no valid position in a */
/* sorted node. Keeping the keys exact removes the case entirely. */
/* ------------------------------------------------------------------ */
struct path_ent {
uint64_t blocknr;
uint32_t crc;
int index;
};
int
schfs_btree_insert(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, const void *val, unsigned len)
{
struct path_ent path[SCHFS_MAX_LEVEL + 1];
int depth = 0;
if (len > SCHFS_MAX_INLINE)
return -1;
/* Descend, remembering the path so it can be rewritten on the way back up. */
uint64_t cur = t->root;
uint32_t crc = t->root_crc;
for (;;) {
if (depth > SCHFS_MAX_LEVEL)
return -2;
if (node_read(t->dev, cur, crc, x->a) != 0)
return -2;
path[depth].blocknr = cur;
path[depth].crc = crc;
path[depth].index = 0;
if (HDR(x->a)->level == 0)
break;
if (HDR(x->a)->nritems == 0)
return -2;
int exact;
int i = node_search(x->a, k, &exact);
path[depth].index = i;
cur = KPTRS(x->a)[i].blocknr;
crc = KPTRS(x->a)[i].crc;
depth++;
}
int root_level = HDR(x->a)->level + depth;
struct schfs_key split_key;
uint64_t sibling = 0;
uint32_t sib_crc = 0;
int split = 0;
if (leaf_put(x->a, k, val, len, x->c) != 0) {
sibling = node_split(t, x->a, x->b, x->c, &split_key);
if (sibling == 0)
return -3;
split = 1;
/* The item belongs in whichever half now covers its key. */
if (schfs_key_cmp(k, &split_key) >= 0) {
if (leaf_put(x->b, k, val, len, x->c) != 0)
return -1; /* value too big even for an empty leaf */
} else {
if (leaf_put(x->a, k, val, len, x->c) != 0)
return -1;
}
/* node_split may have moved the sibling's first key. */
node_first_key(x->b, &split_key);
if (node_flush(t, x->b, sibling, &sib_crc) != 0)
return -2;
}
uint64_t newb = cow_block(t, x->a, path[depth].blocknr);
if (newb == 0)
return -3;
uint32_t child_crc;
if (node_flush(t, x->a, newb, &child_crc) != 0)
return -2;
uint64_t child = newb;
struct schfs_key child_first;
node_first_key(x->a, &child_first);
/* Walk back up, rewriting each parent to point at the new child. */
for (int d = depth - 1; d >= 0; d--) {
if (node_read(t->dev, path[d].blocknr, path[d].crc, x->a) != 0)
return -2;
struct schfs_kptr *kp = KPTRS(x->a);
kp[path[d].index].key = child_first;
kp[path[d].index].blocknr = child;
kp[path[d].index].crc = child_crc;
kp[path[d].index].generation = t->generation;
if (split) {
if (internal_put(x->a, &split_key, sibling, sib_crc,
t->generation) != 0) {
struct schfs_key up_key;
uint64_t up = node_split(t, x->a, x->b, x->c, &up_key);
if (up == 0)
return -3;
if (schfs_key_cmp(&split_key, &up_key) >= 0)
internal_put(x->b, &split_key, sibling, sib_crc, t->generation);
else
internal_put(x->a, &split_key, sibling, sib_crc, t->generation);
node_first_key(x->b, &up_key);
uint32_t up_crc;
if (node_flush(t, x->b, up, &up_crc) != 0)
return -2;
sibling = up;
sib_crc = up_crc;
split_key = up_key;
} else {
split = 0;
}
}
uint64_t pnew = cow_block(t, x->a, path[d].blocknr);
if (pnew == 0)
return -3;
if (node_flush(t, x->a, pnew, &child_crc) != 0)
return -2;
child = pnew;
node_first_key(x->a, &child_first);
}
/* A split that reached the root means the tree gets one level taller. */
if (split) {
if (root_level + 1 > SCHFS_MAX_LEVEL)
return -2;
uint64_t rnew = t->dev->balloc(t->dev->ctx);
if (rnew == 0)
return -3;
memset(x->a, 0, SCHFS_BLKSIZE);
HDR(x->a)->level = (uint16_t)(root_level + 1);
HDR(x->a)->nritems = 2;
struct schfs_kptr *kp = KPTRS(x->a);
kp[0].key = child_first; kp[0].blocknr = child; kp[0].crc = child_crc;
kp[0].generation = t->generation;
kp[1].key = split_key; kp[1].blocknr = sibling; kp[1].crc = sib_crc;
kp[1].generation = t->generation;
if (node_flush(t, x->a, rnew, &child_crc) != 0)
return -2;
child = rnew;
}
t->root = child;
t->root_crc = child_crc;
t->root_gen = t->generation;
return 0;
}
/* ------------------------------------------------------------------ */
/* Delete */
/* */
/* Emptied nodes are freed rather than left in place. Without that a */
/* filesystem that is filled and emptied repeatedly accumulates empty */
/* leaves that every scan still has to walk, and the tree never gets */
/* shorter no matter how few records remain. */
/* */
/* Nodes are still not *rebalanced* — an underfull node keeps its */
/* records until it is refilled or emptied. Merging half-full siblings */
/* is the hardest part of B-tree deletion and buys only density, so it */
/* stays out until something measures a need for it. */
/* ------------------------------------------------------------------ */
int
schfs_btree_delete(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k)
{
struct path_ent path[SCHFS_MAX_LEVEL + 1];
int depth = 0;
uint64_t cur = t->root;
uint32_t crc = t->root_crc;
for (;;) {
if (depth > SCHFS_MAX_LEVEL)
return -2;
if (node_read(t->dev, cur, crc, x->a) != 0)
return -2;
path[depth].blocknr = cur;
path[depth].crc = crc;
path[depth].index = 0;
if (HDR(x->a)->level == 0)
break;
if (HDR(x->a)->nritems == 0)
return -2;
int exact;
int i = node_search(x->a, k, &exact);
path[depth].index = i;
cur = KPTRS(x->a)[i].blocknr;
crc = KPTRS(x->a)[i].crc;
depth++;
}
int exact;
int i = node_search(x->a, k, &exact);
if (!exact)
return -1;
leaf_drop(x->a, i, x->c);
uint64_t child = 0;
uint32_t child_crc = 0;
struct schfs_key child_first;
memset(&child_first, 0, sizeof(child_first));
// An emptied leaf below the root is unlinked from its parent instead of
// being rewritten; `drop` carries that decision up the path.
int drop = (HDR(x->a)->nritems == 0 && depth > 0);
if (drop) {
t->dev->bfree(t->dev->ctx, path[depth].blocknr);
} else {
uint64_t newb = cow_block(t, x->a, path[depth].blocknr);
if (newb == 0)
return -3;
if (node_flush(t, x->a, newb, &child_crc) != 0)
return -2;
child = newb;
node_first_key(x->a, &child_first);
}
for (int d = depth - 1; d >= 0; d--) {
if (node_read(t->dev, path[d].blocknr, path[d].crc, x->a) != 0)
return -2;
if (drop) {
internal_drop(x->a, path[d].index);
if (HDR(x->a)->nritems == 0 && d > 0) {
t->dev->bfree(t->dev->ctx, path[d].blocknr);
continue; /* keep unlinking upward */
}
// A root that lost its last child becomes an empty leaf; leaving it
// an internal node with no children makes every descent read a node
// it cannot use and report corruption.
if (HDR(x->a)->nritems == 0)
HDR(x->a)->level = 0;
drop = 0;
} else {
struct schfs_kptr *kp = KPTRS(x->a);
kp[path[d].index].key = child_first;
kp[path[d].index].blocknr = child;
kp[path[d].index].crc = child_crc;
kp[path[d].index].generation = t->generation;
}
uint64_t pnew = cow_block(t, x->a, path[d].blocknr);
if (pnew == 0)
return -3;
if (node_flush(t, x->a, pnew, &child_crc) != 0)
return -2;
child = pnew;
node_first_key(x->a, &child_first);
}
t->root = child;
t->root_crc = child_crc;
t->root_gen = t->generation;
// Collapse a root that is down to one child. A tree grows a level per
// split; without this it never gives one back, so a directory that briefly
// held a million entries keeps paying five block reads per lookup forever.
for (;;) {
if (node_read(t->dev, t->root, t->root_crc, x->a) != 0)
return -2;
if (HDR(x->a)->level == 0 || HDR(x->a)->nritems != 1)
break;
uint64_t old = t->root;
t->root = KPTRS(x->a)[0].blocknr;
t->root_crc = KPTRS(x->a)[0].crc;
t->dev->bfree(t->dev->ctx, old);
}
t->root_gen = HDR(x->a)->generation;
return 0;
}
/* ------------------------------------------------------------------ */
/* Range scan */
/* ------------------------------------------------------------------ */
int
schfs_btree_range(struct schfs_cursor *c, const struct schfs_key *start,
int (*fn)(const struct schfs_key *, const void *,
unsigned, void *),
void *arg)
{
int r = schfs_cursor_seek(c, start);
if (r == -2)
return -2;
if (r != 0)
return 0; /* nothing at or after start */
for (;;) {
const struct schfs_key *k = schfs_cursor_key(c);
if (!k)
return 0;
unsigned len;
const void *v = schfs_cursor_val(c, &len);
if (!v)
return -2;
// The key is copied out before the callback runs. Callbacks routinely
// want to look something else up — "list a directory and stat each
// entry" is the whole point of a range scan — and any other tree
// operation may reuse this same block buffer. See the warning on
// schfs_btree_range in btree.h: the *value* pointer has the same
// hazard and must be copied by the callback if it needs it to survive.
struct schfs_key kc = *k;
int s = fn(&kc, v, len, arg);
if (s)
return s;
r = schfs_cursor_next(c);
if (r == -2)
return -2;
if (r != 0)
return 0;
}
}
/* ------------------------------------------------------------------ */
/* Host-only: whole-tree walk and structural check */
/* ------------------------------------------------------------------ */
#ifdef SCHFS_HOSTED
static int
walk(struct schfs_btree *t, uint64_t blk, uint32_t crc, int level_expect,
int (*fn)(const struct schfs_key *, const void *, unsigned, void *),
void *arg)
{
char b[SCHFS_BLKSIZE];
if (node_read(t->dev, blk, crc, b) != 0)
return -2;
if (level_expect >= 0 && HDR(b)->level != level_expect)
return -2;
if (HDR(b)->level == 0) {
struct schfs_item *it = ITEMS(b);
for (int i = 0; i < HDR(b)->nritems; i++) {
int r = fn(&it[i].key, b + it[i].offset, it[i].size, arg);
if (r)
return r;
}
return 0;
}
struct schfs_kptr *kp = KPTRS(b);
for (int i = 0; i < HDR(b)->nritems; i++) {
int r = walk(t, kp[i].blocknr, kp[i].crc, HDR(b)->level - 1, fn, arg);
if (r)
return r;
}
return 0;
}
int
schfs_btree_iterate(struct schfs_btree *t,
int (*fn)(const struct schfs_key *, const void *,
unsigned, void *),
void *arg)
{
return walk(t, t->root, t->root_crc, -1, fn, arg);
}
struct check_ctx {
struct schfs_key last;
int have_last;
};
static int
check_order(const struct schfs_key *k, const void *v, unsigned len, void *arg)
{
struct check_ctx *c = (struct check_ctx *)arg;
(void)v; (void)len;
if (c->have_last && schfs_key_cmp(&c->last, k) >= 0)
return -1; /* out of order or duplicated */
c->last = *k;
c->have_last = 1;
return 0;
}
// Verify the separator invariant the descent relies on: kp[i].key is exactly
// the first key in child i, every key in subtree i lies in
// [kp[i].key, kp[i+1].key), and no leaf value escapes its block.
static int
check_bounds(struct schfs_btree *t, uint64_t blk, uint32_t crc,
int level_expect, const struct schfs_key *lo,
const struct schfs_key *hi)
{
char b[SCHFS_BLKSIZE];
if (node_read(t->dev, blk, crc, b) != 0)
return -2;
if (level_expect >= 0 && HDR(b)->level != level_expect)
return -2;
int n = HDR(b)->nritems;
if (HDR(b)->level == 0) {
struct schfs_item *it = ITEMS(b);
int items_end = (int)sizeof(struct schfs_node_hdr)
+ n * (int)sizeof(struct schfs_item);
for (int i = 0; i < n; i++) {
if (i > 0 && schfs_key_cmp(&it[i - 1].key, &it[i].key) >= 0)
return -3; /* leaf keys out of order */
if (lo && schfs_key_cmp(&it[i].key, lo) < 0)
return -4; /* record is below its subtree's range */
if (hi && schfs_key_cmp(&it[i].key, hi) >= 0)
return -5; /* record is above its subtree's range */
if ((int)it[i].offset < items_end ||
(int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
return -6; /* value overlaps the item array or runs off the block */
}
return 0;
}
if (n == 0)
return -7; /* internal node with no children */
struct schfs_kptr *kp = KPTRS(b);
for (int i = 0; i < n; i++) {
if (i > 0 && schfs_key_cmp(&kp[i - 1].key, &kp[i].key) >= 0)
return -3; /* child pointers out of order */
if (lo && i == 0 && schfs_key_cmp(&kp[0].key, lo) != 0)
return -8; /* first key does not match the parent's */
int r = check_bounds(t, kp[i].blocknr, kp[i].crc, HDR(b)->level - 1,
&kp[i].key,
i + 1 < n ? &kp[i + 1].key : hi);
if (r)
return r;
}
return 0;
}
int
schfs_btree_check(struct schfs_btree *t)
{
struct check_ctx c;
c.have_last = 0;
/* walk() verifies magic, self-pointer, checksum and level at every node;
this adds the global key ordering invariant... */
int r = schfs_btree_iterate(t, check_order, &c);
if (r)
return r;
/* ...and this the separator invariant the descent depends on. */
return check_bounds(t, t->root, t->root_crc, -1, NULL, NULL);
}
#endif /* SCHFS_HOSTED */