SchOS/kernel/fs/schfs/objw.c
Nexoniarz 6e2e9e8d05 schfs: snapshots
A snapshot is a tree root that is kept. The B-tree is copy-on-write, so
the blocks a committed root reaches are already immutable -- nothing
overwrites a block a committed tree can reach -- and retaining one costs
a record: (SNAP_OBJECTID, KEY_SNAP, id) -> struct schfs_snap.

The record names the root as it was *before* the record was added.
Inserting it rewrites the path under copy-on-write and produces a new
root, so a snapshot that included its own creation would name a block
nobody had written yet.

What makes it survive is the derivation. There is no free-space map on
disk; it is rebuilt at mount by walking the tree, and that walk now
covers every snapshot root as well as the live one. Without it a block
reachable only from a snapshot looks free, gets handed out, and
overwrites the data the snapshot exists to keep -- silently, surfacing
only when the snapshot is read. A snapshot's own tree may hold older
snapshot records; those are deliberately not followed, or deleting a
snapshot could be undone by an older one's memory of it.

Runtime frees are suspended while a snapshot exists. Knowing which freed
blocks a snapshot still needs requires per-extent reference counting,
which is a design and not a flag; until that exists the safe answer is to
free nothing and let the next mount's derivation reclaim what is
genuinely unreachable. Allocation is unaffected and stays correct -- it
works from the in-use bitmap the derivation filled. The running
free-blocks *counter* does drift from the truth while a snapshot is
present, which is cosmetic and is reported by the test rather than
asserted away.

Tested in schfs_objtest: after a snapshot, the live tree shows new
contents while the snapshot still shows the old ones; a file deleted from
the live tree is still readable through the snapshot; deleting the
snapshot removes the record and the space comes back at the next mount.

schfs-check passes including the round-trip; bootcheck 108 [ok].

Not yet: snapshots are not reachable by name from userspace -- there is
no mount-a-snapshot path, so this is the format and the mechanism, not
the feature a user sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:10:24 +02:00

1007 lines
33 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, write side: create, link, unlink, rename, write,
// truncate.
//
// The same portable C as the read side, compiled both hosted (mkschfs) and
// freestanding (the kernel mount), so an image the tools produce and one the
// kernel produces come from a single implementation rather than two that agree
// by luck. Everything below is expressed as records in the one B-tree; there is
// no inode table, no directory block format and no bitmap to keep in step.
//
// Two rules run through the whole file.
//
// Nothing here may put a block on the stack. A SchFS block is 4096 bytes and so
// is the entire kernel stack, so every function borrows the volume's three
// scratch buffers (v->ctx) instead. Where a cursor is needed, ctx.a doubles as
// its buffer — which is safe only because no path below holds a cursor across a
// tree mutation, and every one of them copies what it needs out first.
//
// Nothing reachable from a committed root is ever overwritten. For tree blocks
// the B-tree enforces that; for *data* blocks it is enforced here, by
// schfs_extent.gen. A run allocated during the group being built may be written
// in place, because no published root can see it. Anything older is copied to a
// fresh block and the extent record split around it.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
uint64_t
schfs_alloc_objectid(struct schfs_vol *v)
{
return v->next_objectid++;
}
/* ------------------------------------------------------------------ */
/* Snapshots */
/* */
/* A snapshot is a tree root that is kept. Copy-on-write means the */
/* blocks that root reaches are already immutable -- nothing overwrites */
/* a block a committed tree can reach -- so retaining one costs a */
/* record, and keeping it alive is entirely schfs_space_scan's job. */
/* ------------------------------------------------------------------ */
// Take a snapshot of the volume as it is committed *now*.
//
// The root recorded is the one in v->tree before this call, which is the
// committed state. It cannot be the root this call produces: adding the record
// rewrites the path to it under copy-on-write, so that root does not exist
// until afterwards, and a snapshot naming it would name a block nobody had
// written yet.
//
// The caller commits. Until it does, the record is not on disk and the
// snapshot does not exist -- which is the correct failure mode, because a
// snapshot that survived a crash while the tree it names did not would be a
// dangling root.
int
schfs_snap_create(struct schfs_vol *v, uint64_t id, const char *name,
unsigned namelen)
{
schfs_cache_invalidate();
if (namelen >= SCHFS_SNAP_NAME_MAX)
return -1;
struct schfs_snap sn;
memset(&sn, 0, sizeof sn);
sn.root = v->tree.root;
sn.root_crc = v->tree.root_crc;
sn.root_generation = v->tree.root_gen;
sn.created = v->tree.generation;
for (unsigned i = 0; i < namelen; i++)
sn.name[i] = name[i];
struct schfs_key k;
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, id);
int r = schfs_btree_insert(&v->tree, &v->ctx, &k, &sn, sizeof sn);
if (r == 0)
v->space.flags |= SCHFS_SPACE_RETAIN_ALL;
return r;
}
// Forget a snapshot. The blocks it was holding are not freed here: they stop
// being reachable, and the next derivation at mount simply does not mark them.
// That is the whole deletion -- there is no reference count to decrement,
// because there never was one.
int
schfs_snap_delete(struct schfs_vol *v, uint64_t id)
{
schfs_cache_invalidate();
struct schfs_key k;
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, id);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
if (r != 0)
return r;
// Stop retaining. If other snapshots remain, the next mount's derivation
// sets the flag again -- it is the walk that knows, not this function, and
// guessing here would mean keeping a second count to disagree with it.
// Until that mount the allocator is merely conservative, which is the safe
// direction.
v->space.flags &= ~(unsigned)SCHFS_SPACE_RETAIN_ALL;
return 0;
}
int
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
{
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
}
// Read an inode using the volume's own scratch block. Every mutation below is a
// read-modify-write of one of these, and each one that took a cursor buffer
// from its caller was one more place to get the aliasing rules wrong.
static int
inode_load(struct schfs_vol *v, uint64_t ino, struct schfs_inode *out)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
return schfs_inode_get(v, &c, ino, out);
}
/* ------------------------------------------------------------------ */
/* Directory entries */
/* */
/* One key per name hash, holding a packed list of records, because two */
/* names in a directory can hash the same and the second insert would */
/* otherwise silently replace the first. */
/* ------------------------------------------------------------------ */
// Load the packed list for `name`'s hash into v->dirbuf. Returns its length, or
// 0 if there is no such key, or a negative error.
static int
dirlist_load(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, struct schfs_key *k)
{
schfs_mkkey(k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
unsigned len = sizeof(v->dirbuf);
int r = schfs_btree_lookup(&v->tree, &v->ctx, k, v->dirbuf, &len);
if (r == -2)
return -2;
if (r != 0)
return 0;
if (len > sizeof(v->dirbuf))
return -2; /* stored value larger than can exist */
return (int)len;
}
// Offset of `name` within the packed list, or -1 if absent, -2 if malformed.
static int
dirlist_find(const char *list, unsigned len, const char *name, unsigned namelen)
{
for (unsigned off = 0; off + sizeof(struct schfs_dirent_rec) <= len; ) {
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(list + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
if (step == 0 || off + step > len)
return -2;
if (d->namelen == namelen &&
memcmp(list + off + sizeof(*d), name, namelen) == 0)
return (int)off;
off += step;
}
return -1;
}
int
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type)
{
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
// Reject a duplicate outright. Without this, adding a name twice produces a
// directory with two entries that lookup can never tell apart.
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at >= 0)
return -1;
unsigned add = SCHFS_DIRENT_LEN(namelen);
if (len + add > sizeof(v->dirbuf))
return -4; /* collision list will not fit in one item */
struct schfs_dirent_rec *d = (struct schfs_dirent_rec *)(v->dirbuf + len);
memset(d, 0, add);
d->objectid = child;
d->type = type;
d->namelen = (uint16_t)namelen;
memmove(v->dirbuf + len + sizeof(*d), name, namelen);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
}
int
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
if (len == 0)
return -1;
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at < 0)
return -1;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v->dirbuf + at);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
memmove(v->dirbuf + at, v->dirbuf + at + step, len - (unsigned)at - step);
len -= step;
// An emptied list means the key itself goes, not a zero-length value: a key
// that exists with nothing in it would make the directory scan stop on a
// record readdir cannot describe.
if (len == 0)
return schfs_btree_delete(&v->tree, &v->ctx, &k);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
}
int
schfs_dir_empty(struct schfs_vol *v, uint64_t dir)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_cursor_seek(&c, &start);
if (r == -2)
return -2;
if (r != 0)
return 1; /* nothing at or after: empty */
const struct schfs_key *got = schfs_cursor_key(&c);
if (!got)
return 1;
return (got->objectid == dir && got->type == SCHFS_KEY_DIRENT) ? 0 : 1;
}
/* ------------------------------------------------------------------ */
/* Data-block checksums */
/* */
/* Tree nodes carry their checksum in their parent's pointer. Data */
/* blocks have no parent, so theirs live under one reserved object id, */
/* exactly as btrfs keeps a checksum tree. */
/* ------------------------------------------------------------------ */
static int
csum_put(struct schfs_vol *v, uint64_t dblk, const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);
return schfs_btree_insert(&v->tree, &v->ctx, &k, &c, sizeof(c));
}
static int
csum_del(struct schfs_vol *v, uint64_t dblk)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r; /* absent is not a failure */
}
/* ------------------------------------------------------------------ */
/* Extents */
/* ------------------------------------------------------------------ */
static int
extent_put(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
return schfs_btree_insert(&v->tree, &v->ctx, &k, e, sizeof(*e));
}
static int
extent_del(struct schfs_vol *v, uint64_t ino, uint64_t fblk)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
// The extent covering file block `fblk`, if any. 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). Returns 1 and fills *start and *e, 0 for a hole, -2
// on corruption.
static int
extent_find(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
uint64_t *start, struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
// lookup_le happily lands on some other object's records; so does a sparse
// file whose first extent starts past this block.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
if (fblk >= got->offset + found->nblocks)
return 0;
*start = got->offset;
*e = *found;
return 1;
}
// The last extent record belonging to `ino`, or 0 if it has none.
static int
extent_last(struct schfs_vol *v, uint64_t ino, uint64_t *start,
struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, ~0ULL);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
*start = got->offset;
*e = *found;
return 1;
}
// Record a newly allocated single block, merging it into the preceding extent
// when the two are adjacent both in the file and on the disk.
//
// This is what keeps extents worth having. Sequential writes get consecutive
// blocks from the allocator, so without merging a one-megabyte file carries 256
// extent records instead of one, and the tree ends up mostly describing where
// the data is rather than holding it.
static int
extent_grow(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
if (fblk > 0) {
uint64_t pstart;
struct schfs_extent p;
int m = extent_find(v, ino, fblk - 1, &pstart, &p);
if (m == -2)
return -2;
if (m == 1 &&
pstart + p.nblocks == fblk &&
p.disk_block + p.nblocks == e->disk_block &&
p.refs == e->refs && p.gen == e->gen) {
p.nblocks++;
return extent_put(v, ino, pstart, &p);
}
}
return extent_put(v, ino, fblk, e);
}
// Release a run of data blocks and the checksum records that describe them.
// Shared runs are left alone: another file still points at them, and proper
// refcounting needs an extent tree that nothing creates yet.
static int
extent_release(struct schfs_vol *v, const struct schfs_extent *e,
uint64_t first, uint64_t n)
{
if (e->refs != 1)
return 0;
for (uint64_t i = 0; i < n; i++) {
int r = csum_del(v, first + i);
if (r != 0)
return r;
v->dev.bfree(v->dev.ctx, first + i);
}
return 0;
}
// Drop everything from file block `from` onward. Used by truncate and by the
// deletion of an object.
static int
extents_drop_from(struct schfs_vol *v, uint64_t ino, struct schfs_inode *in,
uint64_t from)
{
for (;;) {
uint64_t start;
struct schfs_extent e;
int m = extent_last(v, ino, &start, &e);
if (m == -2)
return -2;
if (m == 0)
return 0; /* no extents left */
if (start + e.nblocks <= from)
return 0; /* nothing beyond the cut */
if (start >= from) {
int r = extent_release(v, &e, e.disk_block, e.nblocks);
if (r != 0)
return r;
r = extent_del(v, ino, start);
if (r != 0)
return r;
if (in->blocks >= e.nblocks)
in->blocks -= e.nblocks;
continue; /* there may be earlier ones too */
}
// The cut falls inside this run: keep its head, release its tail.
uint64_t keep = from - start;
uint64_t tail = e.nblocks - keep;
int r = extent_release(v, &e, e.disk_block + keep, tail);
if (r != 0)
return r;
e.nblocks = keep;
r = extent_put(v, ino, start, &e);
if (r != 0)
return r;
if (in->blocks >= tail)
in->blocks -= tail;
return 0;
}
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
int
schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks)
{
if (nblocks == 0)
return 0;
// Take the whole run before touching the tree.
//
// This is what makes extents worth having when the final size is already
// known. Allocating a data block, then inserting its extent record -- which
// itself allocates blocks for the copy-on-write path -- then allocating the
// next data block interleaves metadata with data, so consecutive file
// blocks are never physically adjacent. Grabbing the run up front keeps it
// contiguous and a whole file becomes one record.
uint64_t first;
if (v->dev.balloc_run) {
first = v->dev.balloc_run(v->dev.ctx, nblocks);
if (first == 0)
return -3;
} else {
first = v->dev.balloc(v->dev.ctx);
if (first == 0)
return -3;
uint64_t prev = first;
for (uint64_t i = 1; i < nblocks; i++) {
uint64_t b = v->dev.balloc(v->dev.ctx);
if (b == 0)
return -3;
if (b != prev + 1)
return -2; /* allocator is not handing out a run */
prev = b;
}
}
struct schfs_extent e;
memset(&e, 0, sizeof(e));
e.disk_block = first;
e.nblocks = nblocks;
e.refs = 1;
e.gen = v->tree.generation;
// Deliberately does not touch the inode. Re-reading and rewriting it here
// costs two extra tree inserts per file, and every insert copies its whole
// path. The caller knows the block count before it writes the inode the
// first time, so it sets in.blocks there instead.
return extent_put(v, ino, 0, &e);
}
// Where the next write to file block `fblk` should land.
struct wblk {
uint64_t disk; // block to write
uint64_t src; // block holding the current contents, 0 for a hole
int fresh; // a block that did not exist before
};
static int
block_for_write(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
struct wblk *w)
{
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 0) {
uint64_t d = v->dev.balloc(v->dev.ctx);
if (d == 0)
return -3;
struct schfs_extent ne;
memset(&ne, 0, sizeof(ne));
ne.disk_block = d;
ne.nblocks = 1;
ne.refs = 1;
ne.gen = v->tree.generation;
int r = extent_grow(v, ino, fblk, &ne);
if (r != 0)
return r;
w->disk = d;
w->src = 0;
w->fresh = 1;
return 0;
}
uint64_t old = e.disk_block + (fblk - start);
// Allocated by the group being built, and owned outright: no published root
// can reach it, so it may simply be overwritten.
if (e.gen == v->tree.generation && e.refs == 1) {
w->disk = old;
w->src = old;
w->fresh = 0;
return 0;
}
// Otherwise copy this one block out of the run, and split the record around
// it: a head covering what came before, the new block, and a tail covering
// what came after. No deletion is needed — when the block being replaced is
// the run's first, the middle record simply overwrites the original key.
uint64_t nd = v->dev.balloc(v->dev.ctx);
if (nd == 0)
return -3;
if (fblk > start) {
struct schfs_extent head = e;
head.nblocks = fblk - start;
int r = extent_put(v, ino, start, &head);
if (r != 0)
return r;
}
{
struct schfs_extent mid;
memset(&mid, 0, sizeof(mid));
mid.disk_block = nd;
mid.nblocks = 1;
mid.refs = 1;
mid.gen = v->tree.generation;
int r = extent_put(v, ino, fblk, &mid);
if (r != 0)
return r;
}
if (fblk + 1 < start + e.nblocks) {
struct schfs_extent tail = e;
tail.disk_block = e.disk_block + (fblk + 1 - start);
tail.nblocks = start + e.nblocks - (fblk + 1);
int r = extent_put(v, ino, fblk + 1, &tail);
if (r != 0)
return r;
}
// The block just replaced rejoins the free map at the next commit, not now:
// until then the committed tree still points at it and a crash lands there.
int r = extent_release(v, &e, old, 1);
if (r != 0)
return r;
w->disk = nd;
w->src = old;
w->fresh = 0;
return 0;
}
long
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk)
{
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
const char *src = (const char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t at = off + done;
uint64_t fblk = at / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)(at % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
// A whole-block write needs no read: every byte is about to be
// replaced. A partial one has to start from whatever is there, or from
// zeros where the file had a hole.
if (inblk != 0 || want != SCHFS_BLKSIZE) {
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
}
memmove((char *)dblk + inblk, src + done, want);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
// The checksum is rewritten every time the block changes, not just when
// it is allocated, or a topped-up tail block fails its own check on the
// very next read.
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
done += want;
}
if (off + n > in.size)
in.size = off + n;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
return (long)done;
}
int
schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r == -1 ? -2 : r;
long got = schfs_file_write(v, ino, in.size, buf, n, dblk);
if (got < 0)
return (int)got;
return got == (long)n ? 0 : -2;
}
int
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk)
{
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (size < in.size) {
uint64_t keep = (size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
r = extents_drop_from(v, ino, &in, keep);
if (r != 0)
return r;
// Whole blocks past the new end are gone; the block the new size falls
// *inside* has to have its tail zeroed. It is tempting not to bother,
// since a read stops at in.size and can never see those bytes — but
// truncating down and then growing again is required to read back
// zeros, and without this it reads back whatever the file used to hold.
unsigned tail = (unsigned)(size % SCHFS_BLKSIZE);
if (tail != 0) {
uint64_t fblk = size / SCHFS_BLKSIZE;
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 1) { /* a hole is already zeros */
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
memset((char *)dblk + tail, 0, SCHFS_BLKSIZE - tail);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
}
}
}
// Growing needs nothing at all: the range with no extent records reads back
// as zeros, which is a sparse file for free.
in.size = size;
in.gen = v->tree.generation;
return schfs_inode_put(v, ino, &in);
}
// Release an object entirely: its data, its checksums and its inode record.
int
schfs_object_free(struct schfs_vol *v, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r == -1)
return 0; /* already gone */
if (r != 0)
return r;
r = extents_drop_from(v, ino, &in, 0);
if (r != 0)
return r;
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
/* ------------------------------------------------------------------ */
/* Namespace */
/* ------------------------------------------------------------------ */
// A directory's link count is 2 (itself and its own ".") plus one per
// subdirectory, because each subdirectory's ".." points back at it. Neither
// entry is stored, but the count still has to match what POSIX tools expect or
// "find -type d" prunes real directories and du walks the tree wrongly.
static int
parent_nlink_adjust(struct schfs_vol *v, uint64_t dir, int delta)
{
struct schfs_inode in;
int r = inode_load(v, dir, &in);
if (r != 0)
return r;
if (delta < 0 && in.nlink < (uint32_t)(-delta))
return -2;
in.nlink = (uint32_t)((int64_t)in.nlink + delta);
return schfs_inode_put(v, dir, &in);
}
int
schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_inode parent;
int r = inode_load(v, dir, &parent);
if (r != 0)
return r;
if (parent.type != SCHFS_T_DIR)
return -1;
uint64_t ino = schfs_alloc_objectid(v);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.parent = dir;
in.mode = mode;
in.nlink = type == SCHFS_T_DIR ? 2 : 1;
in.rdev_major = rdev_major;
in.rdev_minor = rdev_minor;
in.atime = in.mtime = in.ctime = now;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
// The entry goes in last, so a failure leaves an unreferenced object rather
// than a name pointing at nothing. Neither survives a crash — none of this
// is visible until the transaction commits — but an unreachable inode is
// the harmless one of the two if a later step in the same group fails.
r = schfs_dir_add(v, dir, name, namelen, ino, type);
if (r != 0)
return r;
if (type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, +1);
if (r != 0)
return r;
}
if (out_ino)
*out_ino = ino;
return 0;
}
int
schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
// Directories are not hard-linkable. Allowing it makes cycles possible, and
// a cycle turns every recursive walk into an infinite one.
if (in.type == SCHFS_T_DIR)
return -1;
r = schfs_dir_add(v, dir, name, namelen, ino, in.type);
if (r != 0)
return r;
in.nlink++;
in.ctime = in.mtime;
return schfs_inode_put(v, ino, &in);
}
int
schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, dir, name, namelen, &ino, &type);
if (r != 0)
return r;
}
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
int e = schfs_dir_empty(v, ino);
if (e == -2)
return -2;
if (e == 0)
return -5; /* directory not empty */
}
r = schfs_dir_del(v, dir, name, namelen);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, -1);
if (r != 0)
return r;
return schfs_object_free(v, ino);
}
if (in.nlink > 1) {
in.nlink--;
return schfs_inode_put(v, ino, &in);
}
return schfs_object_free(v, ino);
}
int
schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, olddir, oldname, oldlen, &ino, &type);
if (r != 0)
return r;
}
// Renaming a directory into its own subtree would detach that subtree from
// the root and leave it reachable only from itself. Walking up from the new
// parent is the cheap way to catch it, since every directory records its
// own parent.
if (type == SCHFS_T_DIR && newdir != olddir) {
uint64_t up = newdir;
for (int i = 0; i <= SCHFS_MAX_LEVEL * 8 && up != 0; i++) {
if (up == ino)
return -1;
struct schfs_inode t;
if (inode_load(v, up, &t) != 0)
break;
if (up == v->root_objectid)
break;
up = t.parent;
}
}
// A target that already exists is replaced, which means removing it first —
// and refusing when that would silently discard a non-empty directory.
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
uint64_t victim;
if (schfs_dir_lookup(v, &c, newdir, newname, newlen, &victim, 0) == 0) {
if (victim == ino)
return 0; /* renaming a name onto itself */
int r = schfs_unlink(v, newdir, newname, newlen);
if (r != 0)
return r;
}
}
int r = schfs_dir_add(v, newdir, newname, newlen, ino, type);
if (r != 0)
return r;
r = schfs_dir_del(v, olddir, oldname, oldlen);
if (r != 0)
return r;
if (newdir != olddir) {
if (type == SCHFS_T_DIR) {
// ".." is synthesised from the inode's parent field, so moving a
// directory means rewriting that field and moving the link its ".."
// contributes to each parent's count.
struct schfs_inode in;
r = inode_load(v, ino, &in);
if (r != 0)
return r;
in.parent = newdir;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
r = parent_nlink_adjust(v, olddir, -1);
if (r != 0)
return r;
r = parent_nlink_adjust(v, newdir, +1);
if (r != 0)
return r;
}
}
return 0;
}