SchOS/kernel/fs/schfs/kmount.c
Nexoniarz 3919f1446e schfs: mount a snapshot by name, read-only
`mount -t schfs @golden /mnt` mounts a snapshot. The source argument
finally means something: sys_mount ignored it outright, because there was
one volume and nothing to choose between. `@name` rather than a path
because a snapshot is not a device -- there is no node in /dev for it and
there should not be, since it lives inside the volume already mounted.

A second volume rather than a parameter threaded through all 23 schfs_k*
entry points. Only the read side can ever address it, so the write path
keeps talking to the live volume and needed no change; the four read
entry points grew _dev variants and pick the volume from the device the
inode already carries. One fs_ops table serves both, because every entry
takes an inode and dispatches on it -- a second table would be the same
pointers under a different name.

It needs its own device number: a snapshot's object ids are the *same
numbers* as the live tree's, so one device would make the two views alias
in the inode cache and hand a reader whichever was cached first.

Three things this turned up, none of them new but all of them reachable
for the first time:

  - sys_mount refused with `dev == SCHFS_DEV`, meaning "do not mount SchFS
    onto SchFS". A snapshot's mount point is necessarily on the live root,
    so that refused the entire feature. The real condition is mounting a
    device onto *itself*, which is `dev == fsdev`.
  - createat allocates an inode and then panics if dirlink fails --
    reasonable about a writable filesystem, a crash on a read-only one.
    The refusal now happens before anything is allocated. Unlink and
    rename already handled a refusal properly.
  - a refused create reported ENOENT, so writing to a snapshot said "No
    such file or directory" -- the sentence a shell prints for every
    create failure. It reports EROFS now.

Verified in the running system against an image built with
--snapshot golden: the mount succeeds, ls lists the snapshot's tree,
/mnt2/etc/motd reads back through it, and a write is refused with
"Read-only file system".

One at a time, and no umount path for it yet: mount_del_by_root removes
the mount table entry, but ksnap_unmount is only called on the failure
paths. schfs-check passes; bootcheck 108 [ok].

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

808 lines
24 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// SchFS in the kernel: block device, mount, and the transaction bracket.
//
// The filesystem code itself (btree.c, space.c, vol.c, obj.c, objw.c) is the
// same portable C the host tools compile, so nothing here re-implements the
// format. This file is only the three things that cannot be shared: a block
// device backed by the buffer cache, the boot-time mount, and the rule about
// when a group of changes becomes durable.
//
// The running root filesystem is still the v1 code in kernel/fs/fs.c, so a
// SchFS volume that fails to mount costs nothing — the system boots exactly as
// before and says so on the console.
//
// ---------------------------------------------------------------------------
// Transactions
//
// Every entry point below that changes anything is one transaction group: it
// takes the volume lock, does its work, and commits before returning. That
// makes an operation durable the moment it returns, and — because a commit is a
// single uberblock write that either lands or does not — indivisible against a
// crash. There is no journal to replay and nothing to undo, because none of the
// work is reachable until that one block is written.
//
// The cost is one uberblock write per operation. Batching several operations
// into a group would amortise it, and the structure here is deliberately shaped
// so that becomes a change to where commit is called rather than a redesign;
// but a filesystem that loses the last few operations on a crash is a much
// worse default than a slow one.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "file.h" /* SCHFS_DEV, SCHFS_SNAP_DEV */
#include "schfs_int.h"
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
// filesystem block spans SCHFS_BPB consecutive buffers. Keeping the buffer
// cache at its own block size avoids disturbing the v1 filesystem, the log and
// the IDE driver, all of which assume BSIZE.
#define SCHFS_BPB (SCHFS_BLKSIZE / BSIZE)
struct schfs_kdev {
uint dev;
};
static struct schfs_kdev kdev;
static struct schfs_vol kvol;
static int kmounted;
// A mounted snapshot: the same block device with an older tree root, read-only.
//
// A second volume rather than a parameter threaded through all 23 entry points,
// because only the read side can ever address it -- a snapshot cannot be
// written to, so every mutation keeps talking to kvol and needs no change.
static struct schfs_vol ksnapvol;
static int ksnap_mounted;
static char *ksnapcurblk;
static char *ksnapdatablk;
// Block buffers, allocated once at mount.
//
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
// SCHFS_BLKSIZE buffer and the kernel stack is one 4096-byte page (KSTACKSIZE),
// so a single such local overflows it. kalloc returns exactly one page, which
// is exactly one SchFS block, so each of these fits with nothing to spare.
//
// kcurblk and kdatablk serve the read paths; kctx* are the three the volume
// keeps for mutations. They are separate pages because a write path runs a
// cursor of its own inside the volume's buffers while the caller's cursor is
// still positioned in kcurblk.
static char *kcurblk;
static char *kdatablk;
static char *kctxa, *kctxb, *kctxc;
// Those buffers are shared by every caller, so every entry point below holds
// this lock for the whole operation. A sleeplock rather than a spinlock because
// bread() sleeps waiting on the disk.
//
// It also serialises transactions, which is not incidental: a group being built
// rewrites its own blocks in place, so two operations interleaving inside one
// group would each see the other's half-finished tree.
static struct sleeplock schfs_lock;
// Seconds, for inode timestamps. The kernel has no wall clock wired up, so this
// matches what fs.c does for the v1 filesystem rather than inventing a second,
// differently-wrong answer.
static uint64_t
know(void)
{
return (uint64_t)uptime();
}
// Start and finish one transaction. `changed` says whether anything was
// written, so a read does not pay for an uberblock write.
static void
ktx_begin(void)
{
acquiresleep(&schfs_lock);
}
static int
ktx_end(int changed, int r)
{
if (changed && r >= 0) {
int c = schfs_vol_commit(&kvol);
if (c != 0)
r = c;
}
releasesleep(&schfs_lock);
return r;
}
static int
kd_read(void *ctx, uint64_t blk, void *out)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
// The buffer cache takes 64-bit block numbers now, so this no longer caps the
// volume; the check remains as an overflow guard on the multiply below.
if (blk > (~0ULL) / SCHFS_BPB)
return -1;
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove((char *)out + i * BSIZE, b->data, BSIZE);
brelse(b);
}
return 0;
}
static int
kd_write(void *ctx, uint64_t blk, const void *in)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
if (blk > (~0ULL) / SCHFS_BPB)
return -1;
// Straight through the buffer cache, with no log. The xv6 log exists to make
// multi-block updates atomic; copy-on-write makes them atomic by never
// modifying a block the committed tree can reach, so logging every write
// would double the I/O to re-solve a problem the format has already solved.
//
// bread before the overwrite because the cache has no "give me this block
// without reading it" entry point. It costs a read the first time a block is
// touched and nothing afterwards.
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove(b->data, (const char *)in + i * BSIZE, BSIZE);
bwrite(b);
brelse(b);
}
return 0;
}
// Allocation goes through the volume's space manager. These take no ctx of
// their own because a schfs_bdev has one ctx shared by every hook and the block
// I/O above needs it for the device number.
static uint64_t
kd_alloc(void *ctx)
{
(void)ctx;
return schfs_vol_alloc(&kvol);
}
static uint64_t
kd_alloc_run(void *ctx, uint64_t n)
{
(void)ctx;
return schfs_vol_alloc_run(&kvol, n);
}
static void
kd_free(void *ctx, uint64_t b)
{
(void)ctx;
schfs_vol_free(&kvol, b);
}
// Pages for the allocation bitmap: one per 32768 blocks of volume, taken once
// at mount and never given back.
static void *
kd_palloc(void)
{
return kalloc();
}
// Try to mount a SchFS volume from `dev`. Never panics: a disk that is not
// SchFS, or is an unreadable SchFS, simply leaves the volume unmounted.
void
schfs_kmount(uint dev)
{
if (kmounted)
return;
initsleeplock(&schfs_lock, "schfs");
if (kcurblk == 0) kcurblk = kalloc();
if (kdatablk == 0) kdatablk = kalloc();
if (kctxa == 0) kctxa = kalloc();
if (kctxb == 0) kctxb = kalloc();
if (kctxc == 0) kctxc = kalloc();
if (kcurblk == 0 || kdatablk == 0 ||
kctxa == 0 || kctxb == 0 || kctxc == 0) {
cprintf("schfs: no memory for block buffers\n");
return;
}
kdev.dev = dev;
struct schfs_bdev bd;
bd.ctx = &kdev;
bd.bread = kd_read;
bd.bwrite = kd_write;
bd.balloc = kd_alloc;
bd.bfree = kd_free;
bd.balloc_run = kd_alloc_run;
struct schfs_ctx cx;
cx.a = kctxa;
cx.b = kctxb;
cx.c = kctxc;
// The uberblock scan doubles as the probe: no valid uberblock means this is
// simply not a SchFS volume, which is not an error worth reporting loudly.
// Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted.
int r = schfs_vol_open(&kvol, &bd, &cx, kd_palloc);
if (r == -1)
return;
if (r == -4) {
cprintf("schfs: device %d is larger than %d GB and cannot be mapped\n",
dev, (int)(SCHFS_MAX_BLOCKS * SCHFS_BLKSIZE / (1024 * 1024 * 1024)));
return;
}
if (r == -5) {
cprintf("schfs: no memory for the allocation map of device %d\n", dev);
return;
}
if (r != 0) {
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
dev);
return;
}
kmounted = 1;
cprintf("schfs: mounted '%s' on device %d, %d MB, generation %d\n",
kvol.label, dev,
(int)(kvol.total_blocks * SCHFS_BLKSIZE / (1024 * 1024)),
(int)kvol.generation);
}
// Which volume a device number names. The VFS hands an inode to every fs_ops
// call and an inode carries its device, so this is all the dispatch a
// read-only second view needs.
static struct schfs_vol *
vol_for(uint dev)
{
return dev == SCHFS_SNAP_DEV ? &ksnapvol : &kvol;
}
static char *
curblk_for(uint dev)
{
return dev == SCHFS_SNAP_DEV ? ksnapcurblk : kcurblk;
}
static char *
datablk_for(uint dev)
{
return dev == SCHFS_SNAP_DEV ? ksnapdatablk : kdatablk;
}
static int
mounted_for(uint dev)
{
return dev == SCHFS_SNAP_DEV ? ksnap_mounted : kmounted;
}
// Mount a snapshot by name, read-only.
//
// The snapshot's records live in the *live* tree, so the name is resolved
// there; what comes back is a root, and the second volume is a copy of the
// first with its tree pointed at it. Nothing else differs -- same device, same
// node format, same checksums.
int
schfs_ksnap_mount(const char *name, unsigned namelen)
{
if (!kmounted)
return -1;
if (ksnap_mounted)
return -2; /* one at a time, for now */
if (ksnapcurblk == 0) ksnapcurblk = kalloc();
if (ksnapdatablk == 0) ksnapdatablk = kalloc();
if (ksnapcurblk == 0 || ksnapdatablk == 0)
return -3;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
struct schfs_key k;
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, 0);
int found = 0;
struct schfs_snap sn;
int r = schfs_cursor_seek(&c, &k);
while (r == 0) {
const struct schfs_key *got = schfs_cursor_key(&c);
if (!got || got->objectid != SCHFS_SNAP_OBJECTID ||
got->type != SCHFS_KEY_SNAP)
break;
unsigned len;
const struct schfs_snap *p = (const struct schfs_snap *)
schfs_cursor_val(&c, &len);
if (p && len == sizeof(*p)) {
unsigned i = 0;
while (i < namelen && i < SCHFS_SNAP_NAME_MAX && p->name[i] == name[i])
i++;
if (i == namelen &&
(namelen == SCHFS_SNAP_NAME_MAX || p->name[namelen] == 0)) {
sn = *p;
found = 1;
break;
}
}
r = schfs_cursor_next(&c);
}
if (!found) {
releasesleep(&schfs_lock);
return -4;
}
// Same volume, older root. The device, the space map and the block callbacks
// are all the live volume's -- a snapshot is not a separate filesystem, it is
// a second way into this one.
ksnapvol = kvol;
ksnapvol.tree.root = sn.root;
ksnapvol.tree.root_crc = sn.root_crc;
ksnapvol.tree.root_gen = sn.root_generation;
// Its own scratch, or a read through the snapshot would tread on a cursor
// the live volume still has positioned.
ksnapvol.ctx.a = ksnapcurblk;
ksnap_mounted = 1;
releasesleep(&schfs_lock);
return 0;
}
void
schfs_ksnap_unmount(void)
{
acquiresleep(&schfs_lock);
ksnap_mounted = 0;
releasesleep(&schfs_lock);
}
uint64
schfs_ksnap_root(void)
{
return ksnap_mounted ? ksnapvol.root_objectid : 0;
}
// The read path, addressable per device so a snapshot can be read through the
// same VFS. Each is the existing call with the volume chosen rather than
// assumed; the originals below stay as the live-volume spelling, because most
// callers have no business knowing there is a choice.
long
schfs_kread_dev(uint dev, uint64_t ino, uint64_t off, void *buf, unsigned n)
{
if (!mounted_for(dev))
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
long r = schfs_file_read(vol_for(dev), &c, ino, off, buf, n, datablk_for(dev));
releasesleep(&schfs_lock);
return r;
}
int
schfs_kstat_dev(uint dev, uint64_t ino, struct schfs_inode *out)
{
if (!mounted_for(dev))
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
int r = schfs_inode_get(vol_for(dev), &c, ino, out);
releasesleep(&schfs_lock);
return r;
}
uint64
schfs_klookup_at_dev(uint dev, uint64_t dir, const char *name, unsigned len)
{
if (!mounted_for(dev))
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
uint64_t ino = 0;
uint16_t type = 0;
int r = schfs_dir_lookup(vol_for(dev), &c, dir, name, len, &ino, &type);
releasesleep(&schfs_lock);
return r == 0 ? ino : 0;
}
int
schfs_kreaddir_dev(uint dev, uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
if (!mounted_for(dev))
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
int r = schfs_dir_iterate(vol_for(dev), &c, dir, fn, arg);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kmounted(void)
{
return kmounted;
}
// Resolve a path on the mounted volume. Returns the object id, or 0.
uint64_t
schfs_klookup(const char *path)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t r = schfs_path_lookup(&kvol, &c, path);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kstat(uint64_t ino, struct schfs_inode *out)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_inode_get(&kvol, &c, ino, out);
releasesleep(&schfs_lock);
return r;
}
long
schfs_kread(uint64_t ino, uint64_t off, void *buf, unsigned n)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
long r = schfs_file_read(&kvol, &c, ino, off, buf, n, kdatablk);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kreaddir(uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
if (!kmounted)
return -1;
// NOTE: fn must not call back into schfs_k* -- the lock is not recursive and
// the cursor buffer is shared. Callers that need per-entry detail collect the
// entries first, then look them up after the walk.
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_dir_iterate(&kvol, &c, dir, fn, arg);
releasesleep(&schfs_lock);
return r;
}
struct schfs_vol *
schfs_kvol(void)
{
return kmounted ? &kvol : 0;
}
/* ------------------------------------------------------------------ */
/* Write side */
/* */
/* Each of these is one transaction: everything it does is invisible */
/* until it commits, and a crash part way through leaves the volume at */
/* the state before the call. That is the entire failure story — there */
/* is no partial state to detect and nothing to roll back. */
/* ------------------------------------------------------------------ */
int
schfs_kcreate(uint64_t dir, const char *name, unsigned namelen, uint16_t type,
uint32_t mode, uint16_t major, uint16_t minor, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_create(&kvol, dir, name, namelen, type, mode, major, minor,
know(), out);
return ktx_end(1, r);
}
long
schfs_kwrite(uint64_t ino, uint64_t off, const void *buf, unsigned n)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
long r = schfs_file_write(&kvol, ino, off, buf, n, kdatablk);
// ktx_end speaks in error codes, and a write speaks in byte counts. Passing
// the count straight through told it the transaction had failed with status
// 17, which it duly reported -- and a short write is a panic one layer up.
int e = ktx_end(1, r < 0 ? (int)r : 0);
return e == 0 ? r : (long)e;
}
int
schfs_ktruncate(uint64_t ino, uint64_t size)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_file_truncate(&kvol, ino, size, kdatablk);
return ktx_end(1, r);
}
int
schfs_kunlink(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_unlink(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
int
schfs_klink(uint64_t dir, const char *name, unsigned namelen, uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_link(&kvol, dir, name, namelen, ino);
return ktx_end(1, r);
}
int
schfs_krename(uint64_t olddir, const char *oldname, unsigned oldlen,
uint64_t newdir, const char *newname, unsigned newlen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_rename(&kvol, olddir, oldname, oldlen,
newdir, newname, newlen);
return ktx_end(1, r);
}
// Write back an inode record the caller has modified — the one way to change
// mode, ownership or timestamps. The caller reads it with schfs_kstat, edits
// the struct, and hands it back.
int
schfs_ksetattr(uint64_t ino, const struct schfs_inode *in)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_inode_put(&kvol, ino, in);
return ktx_end(1, r);
}
// Flush without changing anything. Nothing needs this yet — every operation
// above commits already — but sync(2) has to mean something.
int
schfs_ksync(void)
{
if (!kmounted || kvol.readonly)
return 0;
ktx_begin();
return ktx_end(1, 0);
}
/* ------------------------------------------------------------------ */
/* Pieces the VFS needs */
/* */
/* The VFS keeps its own link count and decides when an object dies, */
/* so it needs the directory entry and the object as separate */
/* operations rather than the combined create/unlink above. Two layers */
/* counting links independently is exactly how a filesystem loses a */
/* file that something still has open. */
/* ------------------------------------------------------------------ */
// Resolve one name inside a directory. 0 if there is no such entry.
uint64_t
schfs_klookup_at(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t ino = 0;
if (schfs_dir_lookup(&kvol, &c, dir, name, namelen, &ino, 0) != 0)
ino = 0;
releasesleep(&schfs_lock);
return ino;
}
// 1 if the directory holds no entries, 0 if it does, -1 on error.
int
schfs_kdirempty(uint64_t dir)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
int r = schfs_dir_empty(&kvol, dir);
releasesleep(&schfs_lock);
return r == -2 ? -1 : r;
}
// An object with no name and no links yet. Nothing can reach it until the
// caller links it, and nothing reaches the disk until the transaction commits,
// so a caller that gives up in between leaves nothing behind.
int
schfs_kalloc_object(uint16_t type, uint32_t mode, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
uint64_t ino = schfs_alloc_objectid(&kvol);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.mode = mode;
in.nlink = 0;
in.atime = in.mtime = in.ctime = know();
in.gen = kvol.tree.generation;
int r = schfs_inode_put(&kvol, ino, &in);
if (r == 0 && out)
*out = ino;
return ktx_end(1, r);
}
int
schfs_kdiradd(uint64_t dir, const char *name, unsigned namelen, uint64_t ino,
uint16_t type)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_add(&kvol, dir, name, namelen, ino, type);
return ktx_end(1, r);
}
// Remove one directory entry, leaving the object alone. The VFS decrements the
// link count itself and calls schfs_kfree when it reaches zero.
int
schfs_kdirdel(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_del(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
// Release an object's data, checksums and inode record. Called from iput once
// the last name is gone and nothing has it open.
int
schfs_kfree(uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_object_free(&kvol, ino);
return ktx_end(1, r);
}
uint64_t
schfs_kroot(void)
{
return kmounted ? kvol.root_objectid : 0;
}
/* ------------------------------------------------------------------ */
/* /proc/schfs */
/* */
/* Phase 2 has no VFS integration -- SchFS files are not reachable */
/* through open(2) yet -- so this is how the mount is observable from */
/* userland at all. It reports the volume and lists the root directory, */
/* which is enough to confirm from inside a running SchOS that the */
/* kernel really did parse an image mkschfs wrote. */
/* ------------------------------------------------------------------ */
static void
kp_puts(char *buf, int cap, int *len, const char *s)
{
while (*s && *len < cap - 1)
buf[(*len)++] = *s++;
}
static void
kp_putu(char *buf, int cap, int *len, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0) { if (*len < cap - 1) buf[(*len)++] = '0'; return; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i--) { if (*len < cap - 1) buf[(*len)++] = tmp[i]; }
}
struct kp_ctx {
char *buf;
int cap;
int *len;
int count;
};
static int
kp_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
struct kp_ctx *c = arg;
c->count++;
// Only the first handful, and no per-entry stat: schfs_kreaddir holds the
// lock across the walk, so calling schfs_kstat from here would deadlock.
if (c->count > 16)
return 0;
kp_puts(c->buf, c->cap, c->len, " ");
kp_puts(c->buf, c->cap, c->len,
type == SCHFS_T_DIR ? "dir " :
type == SCHFS_T_SYMLINK ? "symlink " :
type == SCHFS_T_DEV ? "dev " : "file ");
for (unsigned i = 0; i < namelen && *c->len < c->cap - 1; i++)
c->buf[(*c->len)++] = name[i];
kp_puts(c->buf, c->cap, c->len, " #");
kp_putu(c->buf, c->cap, c->len, ino);
kp_puts(c->buf, c->cap, c->len, "\n");
return 0;
}
int
schfs_kinfo(char *buf, int cap)
{
int len = 0;
if (!kmounted) {
kp_puts(buf, cap, &len, "no SchFS volume mounted\n");
return len;
}
kp_puts(buf, cap, &len, "label: ");
kp_puts(buf, cap, &len, kvol.label);
kp_puts(buf, cap, &len, "\nblocksize: ");
kp_putu(buf, cap, &len, SCHFS_BLKSIZE);
kp_puts(buf, cap, &len, "\ntotal_blocks: ");
kp_putu(buf, cap, &len, kvol.total_blocks);
kp_puts(buf, cap, &len, "\nfree_blocks: ");
kp_putu(buf, cap, &len, kvol.free_blocks);
kp_puts(buf, cap, &len, "\ngeneration: ");
kp_putu(buf, cap, &len, kvol.generation);
kp_puts(buf, cap, &len, "\ntree_root: ");
kp_putu(buf, cap, &len, kvol.tree.root);
kp_puts(buf, cap, &len, "\nroot_objectid: ");
kp_putu(buf, cap, &len, kvol.root_objectid);
kp_puts(buf, cap, &len, "\n\nroot directory:\n");
struct kp_ctx c = { buf, cap, &len, 0 };
if (schfs_kreaddir(kvol.root_objectid, kp_entry, &c) != 0) {
kp_puts(buf, cap, &len, " (directory read failed)\n");
return len;
}
if (c.count > 16) {
kp_puts(buf, cap, &len, " ... ");
kp_putu(buf, cap, &len, (uint64)(c.count - 16));
kp_puts(buf, cap, &len, " more\n");
}
return len;
}