SchFS is a 64-bit format addressing 64 ZB. The stack under it was not, and the lowest limit was three orders of magnitude below the one anybody would have guessed: ide.c panicked on any block >= FSSIZE -- 150000 blocks, 614 MB ide.c computed the sector in an int, and addressed it with LBA28 bio.c took a 32-bit block number, capping any device at 16 TB schfs refused blocks past 0xffffffff, mirroring the cache The FSSIZE check is the one that mattered. FSSIZE describes the size of a v1 filesystem *image*; it had no business bounding a physical device, and it meant a disk larger than half a gigabyte panicked the kernel rather than being read. struct buf's blockno is uint64 now, and so is bread's. idestart computes the sector in 64 bits and picks LBA48 when it does not fit in 28 -- per request, so everything that fits stays on the LBA28 path every existing image has been read through. The 48-bit registers are written high half first, because each is a two-deep FIFO and the other order leaves the drive addressing whatever the low bytes alone describe. Verified with a 2 GB disk carrying a marker at 1 GiB -- block 262144, well past the old panic at 150000. The guest reads it back: "MARKER-PAST-THE-OLD-LIMIT". schfs-check passes including the round-trip; bootcheck 108 [ok]. Two limits deliberately left, both noted rather than guessed at: raw /dev/hdN access still goes through devsw's 32-bit offset, so a raw device is addressable to 4 GB; and usb_storage computes a 32-bit LBA, which is what SCSI READ(10) carries -- READ(16) is a different command, not a wider variable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
622 lines
19 KiB
C
622 lines
19 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 "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;
|
|
|
|
// 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);
|
|
}
|
|
|
|
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;
|
|
}
|