SchOS/tools/schfs_fsck.c
Nexoniarz 754d96ebc5 tools: group scripts by what runs them
tools/windows/   .ps1   (pwsh, so usable from Linux and macOS too)
  tools/posix/     .sh
  tools/python/    .py
  tools/           .c and .pl

The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.

Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.

Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:

  - schfs_fsck.c did not compile under mingw at all: --extract uses
    POSIX mkdir(path,mode), symlink() and link(), none of which mingw
    has. That took the whole tool with it, and `make schfs-check` with
    it. mkdir maps to _mkdir; symlink and link fail loudly rather than
    writing a regular file in their place, because an extracted tree
    that differs from the image while reporting success defeats the
    point of --extract.
  - the round-trip test then failed at `ln -s`, since Git Bash cannot
    create symlinks without developer mode. It skips with a message now.
    Not a fudge: mkschfs's mingw build cannot see a symlink either, so
    the test would have compared a tree with none against an image with
    none and called it a pass -- coverage reporting itself as present
    while being absent.

Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.
2026-08-09 14:38:02 +02:00

757 lines
25 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// schfs_fsck — inspect, verify and extract a SchFS image.
//
// "fsck" is a slight misnomer: copy-on-write means there is nothing to repair.
// A crash leaves either the old tree or the new one, so a volume is either
// consistent or it has no valid uberblock at all. What this does is *check* --
// and, just as usefully, read an image back out so its contents can be compared
// against whatever produced it.
//
// schfs_fsck img --info uberblock ring and volume summary
// schfs_fsck img --check full structural verification
// schfs_fsck img --ls PATH list a directory
// schfs_fsck img --stat PATH one object's metadata
// schfs_fsck img --cat PATH a file's contents to stdout
// schfs_fsck img --extract DIR the whole tree onto the host
// schfs_fsck img --tree every path in the volume
//
// --extract is what makes the round-trip test possible, and the reason this
// reads the image through the same code the kernel will use: if that code is
// wrong, the extracted tree differs from the source tree and diff says so.
#define _FILE_OFFSET_BITS 64
#define SCHFS_HOSTED 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
/* --extract on Windows.
*
* mingw has no POSIX mkdir(path, mode), no symlink() and no link(), because
* Windows has no equivalent that can be created without either a privilege or
* a reparse point. Everything else here -- --info, --check, --ls, --stat,
* --cat, --tree -- is pure reading and works unchanged; it was only --extract
* that would not compile, which took the whole tool with it and with it
* `make schfs-check`.
*
* Directories map cleanly: _mkdir takes no mode, and Windows has no POSIX
* permission bits to apply anyway. Symlinks and hard links do not map, so they
* fail loudly and say why. Silently writing a regular file in their place would
* produce an extracted tree that differs from the image while reporting
* success, which is worse than not extracting at all -- the entire point of
* --extract is comparing the result against what produced it.
*/
#ifdef _WIN32
#include <direct.h>
#define mkdir(path, mode) _mkdir(path)
static int symlink(const char *target, const char *path)
{
(void)target;
fprintf(stderr, "schfs_fsck: %s: cannot create a symlink on Windows\n", path);
errno = ENOSYS;
return -1;
}
static int link(const char *from, const char *to)
{
(void)from;
fprintf(stderr, "schfs_fsck: %s: cannot create a hard link on Windows\n", to);
errno = ENOSYS;
return -1;
}
#endif
#include "../kernel/fs/schfs/btree.c"
#include "../kernel/fs/schfs/space.c"
#include "../kernel/fs/schfs/vol.c"
#include "../kernel/fs/schfs/obj.c"
/* ------------------------------------------------------------------ */
struct imgdev {
FILE *f;
uint64_t nblocks;
};
static struct imgdev IMG;
static int
img_read(void *ctx, uint64_t b, void *buf)
{
struct imgdev *d = ctx;
if (b >= d->nblocks)
return -1;
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
return -1;
if (fread(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
return -1;
return 0;
}
static int
img_write(void *ctx, uint64_t b, const void *buf)
{
(void)ctx; (void)b; (void)buf;
return -1; /* this tool never writes */
}
static uint64_t img_alloc(void *ctx) { (void)ctx; return 0; }
static void img_nofree(void *ctx, uint64_t b) { (void)ctx; (void)b; }
// Pages for the allocation map --check derives. Nothing is written back; this
// is a check on the map, not a use of it.
static void *img_palloc(void) { return calloc(1, SCHFS_BLKSIZE); }
static struct schfs_vol VOL;
static char CURBLK[SCHFS_BLKSIZE];
static char DATABLK[SCHFS_BLKSIZE];
// Read-only, so only the first of these is ever touched — but a volume owns all
// three for its whole life, and handing it two null pointers would turn a
// mistake into a crash rather than a compile error.
static char CTX_A[SCHFS_BLKSIZE];
static char CTX_B[SCHFS_BLKSIZE];
static char CTX_C[SCHFS_BLKSIZE];
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
static const char *
typename(uint16_t t)
{
switch (t) {
case SCHFS_T_FILE: return "file";
case SCHFS_T_DIR: return "dir";
case SCHFS_T_DEV: return "dev";
case SCHFS_T_SYMLINK: return "symlink";
default: return "?";
}
}
/* ------------------------------------------------------------------ */
/* --info */
/* ------------------------------------------------------------------ */
static int
do_info(void)
{
printf("uberblock ring (%d slots from block %d)\n",
SCHFS_UBER_COUNT, SCHFS_UBER_START);
int valid = 0, best = -1;
uint64_t bestgen = 0;
for (int i = 0; i < SCHFS_UBER_COUNT; i++) {
struct schfs_uberblock *u = (struct schfs_uberblock *)CTX_A;
if (img_read(&IMG, SCHFS_UBER_START + i, u) != 0)
continue;
if (u->magic != SCHFS_MAGIC)
continue;
uint32_t stored = u->crc;
u->crc = 0;
uint32_t calc = schfs_crc32c(0, u, sizeof(*u));
u->crc = stored;
if (calc != stored) {
printf(" slot %3d: magic ok, CHECKSUM BAD (generation %llu)\n",
i, (unsigned long long)u->generation);
continue;
}
valid++;
if (best < 0 || u->generation > bestgen) {
bestgen = u->generation;
best = i;
}
}
printf(" %d slot%s valid; newest is slot %d at generation %llu\n\n",
valid, valid == 1 ? "" : "s", best, (unsigned long long)bestgen);
printf("volume\n");
printf(" label %s\n", VOL.label);
printf(" uuid ");
for (int i = 0; i < 16; i++)
printf("%02x%s", VOL.uuid[i], (i == 3 || i == 5 || i == 7 || i == 9) ? "-" : "");
printf("\n");
printf(" block size %d bytes\n", SCHFS_BLKSIZE);
printf(" total blocks %llu (%llu KB)\n",
(unsigned long long)VOL.total_blocks,
(unsigned long long)VOL.total_blocks * SCHFS_BLKSIZE / 1024);
printf(" free blocks %llu (%llu KB)\n",
(unsigned long long)VOL.free_blocks,
(unsigned long long)VOL.free_blocks * SCHFS_BLKSIZE / 1024);
printf(" tree root block %llu, generation %llu\n",
(unsigned long long)VOL.tree.root,
(unsigned long long)VOL.tree.generation);
printf(" next object id %llu\n", (unsigned long long)VOL.next_objectid);
printf(" root object id %llu\n", (unsigned long long)VOL.root_objectid);
return 0;
}
/* ------------------------------------------------------------------ */
/* --check */
/* ------------------------------------------------------------------ */
struct tally {
long inodes, dirents, extents, csums, other;
long long bytes;
int bad;
};
static int
count_rec(const struct schfs_key *k, const void *val, unsigned len, void *arg)
{
struct tally *t = arg;
(void)val;
switch (k->type) {
case SCHFS_KEY_INODE:
t->inodes++;
if (len != sizeof(struct schfs_inode)) {
fprintf(stderr, " object %llu: inode record is %u bytes, expected %zu\n",
(unsigned long long)k->objectid, len,
sizeof(struct schfs_inode));
t->bad++;
} else {
t->bytes += ((const struct schfs_inode *)val)->size;
}
break;
case SCHFS_KEY_DIRENT: t->dirents++; break;
case SCHFS_KEY_EXTENT:
t->extents++;
if (len != sizeof(struct schfs_extent)) {
fprintf(stderr, " object %llu: extent record is %u bytes\n",
(unsigned long long)k->objectid, len);
t->bad++;
}
break;
case SCHFS_KEY_CSUM: t->csums++; break;
default: t->other++; break;
}
return 0;
}
/* Verify every data block against its stored checksum, by reading every file
in full. This is the part a metadata-only check would miss entirely. */
static long files_checked, blocks_checked;
// Chain of object ids from the root down to whatever is being checked.
//
// A directory that is its own ancestor makes any recursive walk run until the
// stack runs out, and a segfault is a terrible way to be told the filesystem
// has a loop. Directories cannot be hard-linked, so a repeat in this chain is
// always a fault -- and the first version of mkschfs produced exactly one, by
// handing a subdirectory the root's object id.
#define MAX_DEPTH 64
static uint64_t chain[MAX_DEPTH];
static int chain_n;
static int
chain_push(uint64_t ino, const char *path)
{
for (int i = 0; i < chain_n; i++)
if (chain[i] == ino) {
fprintf(stderr, " %s: directory loop -- object %llu is its own "
"ancestor\n", path, (unsigned long long)ino);
return -1;
}
if (chain_n >= MAX_DEPTH) {
fprintf(stderr, " %s: directory nesting deeper than %d\n",
path, MAX_DEPTH);
return -1;
}
chain[chain_n++] = ino;
return 0;
}
static void chain_pop(void) { if (chain_n) chain_n--; }
static int check_dir(uint64_t ino, const char *path);
static int
check_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
const char *parent = arg;
char path[8192];
snprintf(path, sizeof(path), "%s%s%.*s",
parent, strcmp(parent, "/") == 0 ? "" : "/", (int)namelen, name);
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
fprintf(stderr, " %s: entry points at object %llu, which has no inode\n",
path, (unsigned long long)ino);
return -1;
}
if (in.type != type)
fprintf(stderr, " %s: dirent says %s, inode says %s\n",
path, typename(type), typename(in.type));
if (in.type == SCHFS_T_DIR)
return check_dir(ino, path);
uint64_t off = 0;
while (off < in.size) {
// A full block is requested every time, even at the tail. Clamping
// here instead would mean nothing ever tests that schfs_file_read
// stops at end-of-file -- and the kernel's read(2) will depend on
// exactly that, since a real program just asks for a bufferful.
char buf[SCHFS_BLKSIZE];
unsigned want = sizeof(buf);
long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK);
if (got <= 0) {
fprintf(stderr, " %s: read failed at offset %llu (%ld)\n",
path, (unsigned long long)off, got);
return -1;
}
off += (uint64_t)got;
blocks_checked++;
}
files_checked++;
return 0;
}
static int
check_dir(uint64_t ino, const char *path)
{
if (chain_push(ino, path) != 0)
return -1;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
int r = schfs_dir_iterate(&VOL, &c, ino, check_entry, (void *)path);
chain_pop();
return r;
}
static int
do_check(void)
{
printf("checking %s\n", VOL.label);
if (schfs_btree_check(&VOL.tree) != 0) {
fprintf(stderr, " FAIL: tree structure is invalid\n");
return 1;
}
printf(" [ok] tree structure, checksums and key ordering\n");
struct tally t;
memset(&t, 0, sizeof(t));
if (schfs_btree_iterate(&VOL.tree, count_rec, &t) != 0) {
fprintf(stderr, " FAIL: could not walk every record\n");
return 1;
}
if (t.bad) {
fprintf(stderr, " FAIL: %d malformed records\n", t.bad);
return 1;
}
printf(" [ok] %ld inodes, %ld dirent items, %ld extents, %ld data checksums\n",
t.inodes, t.dirents, t.extents, t.csums);
if (check_dir(VOL.root_objectid, "/") != 0) {
fprintf(stderr, " FAIL: walking the directory tree found a problem\n");
return 1;
}
printf(" [ok] %ld files read end to end, every data block matched its checksum\n",
files_checked);
// Rebuild the allocation map the way a mount would, and hold it against the
// count the writer left in the uberblock. The map is derived rather than
// stored, so nothing else would ever notice if the scan and the writer
// disagreed — right up until a mounted volume handed out a block that was
// still in use.
if (schfs_space_init(&VOL.space, VOL.total_blocks, img_palloc) != 0) {
fprintf(stderr, " FAIL: could not build an allocation map\n");
return 1;
}
if (schfs_space_scan(&VOL) != 0) {
fprintf(stderr, " FAIL: could not walk the tree to derive free space\n");
return 1;
}
if (VOL.space.free_blocks != VOL.free_blocks) {
fprintf(stderr, " FAIL: uberblock says %llu blocks free, walking the "
"tree says %llu\n",
(unsigned long long)VOL.free_blocks,
(unsigned long long)VOL.space.free_blocks);
return 1;
}
printf(" [ok] free space derived from the tree matches the uberblock "
"(%llu of %llu blocks in use)\n",
(unsigned long long)(VOL.total_blocks - VOL.space.free_blocks),
(unsigned long long)VOL.total_blocks);
return 0;
}
/* ------------------------------------------------------------------ */
/* --ls / --stat / --cat / --tree */
/* ------------------------------------------------------------------ */
static int
print_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
(void)arg;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
printf(" ? %.*s\n", (int)namelen, name);
return 0;
}
printf(" %-7s %6llu %4o %2u links %.*s\n",
typename(type), (unsigned long long)in.size, in.mode & 07777,
in.nlink, (int)namelen, name);
return 0;
}
static uint64_t
resolve(const char *path)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
uint64_t ino = schfs_path_lookup(&VOL, &c, path);
if (ino == 0)
fprintf(stderr, "schfs_fsck: %s: no such file or directory\n", path);
return ino;
}
static int
do_ls(const char *path)
{
uint64_t ino = resolve(path);
if (!ino)
return 1;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
printf("%s\n", path);
return schfs_dir_iterate(&VOL, &c, ino, print_entry, NULL) != 0;
}
static int
do_stat(const char *path)
{
uint64_t ino = resolve(path);
if (!ino)
return 1;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
fprintf(stderr, "schfs_fsck: %s: inode unreadable\n", path);
return 1;
}
printf("path: %s\n", path);
printf("objectid: %llu\n", (unsigned long long)ino);
printf("type: %s\n", typename(in.type));
printf("size: %llu\n", (unsigned long long)in.size);
printf("blocks: %llu\n", (unsigned long long)in.blocks);
printf("mode: %04o\n", in.mode & 07777);
printf("nlink: %u\n", in.nlink);
printf("uid: %u\n", in.uid);
printf("gid: %u\n", in.gid);
printf("mtime: %llu\n", (unsigned long long)in.mtime);
printf("parent: %llu\n", (unsigned long long)in.parent);
if (in.type == SCHFS_T_DEV)
printf("rdev: %u:%u\n", in.rdev_major, in.rdev_minor);
return 0;
}
static int
do_cat(const char *path)
{
uint64_t ino = resolve(path);
if (!ino)
return 1;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, ino, &in) != 0)
return 1;
uint64_t off = 0;
while (off < in.size) {
// A full block is requested every time, even at the tail. Clamping
// here instead would mean nothing ever tests that schfs_file_read
// stops at end-of-file -- and the kernel's read(2) will depend on
// exactly that, since a real program just asks for a bufferful.
char buf[SCHFS_BLKSIZE];
unsigned want = sizeof(buf);
long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK);
if (got <= 0) {
fprintf(stderr, "schfs_fsck: %s: read error at %llu\n",
path, (unsigned long long)off);
return 1;
}
fwrite(buf, 1, (size_t)got, stdout);
off += (uint64_t)got;
}
return 0;
}
static int tree_walk(uint64_t ino, const char *path);
static int
tree_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
const char *parent = arg;
char path[8192];
snprintf(path, sizeof(path), "%s%s%.*s",
parent, strcmp(parent, "/") == 0 ? "" : "/", (int)namelen, name);
printf("%-8s %s\n", typename(type), path);
if (type == SCHFS_T_DIR)
return tree_walk(ino, path);
return 0;
}
static int
tree_walk(uint64_t ino, const char *path)
{
if (chain_push(ino, path) != 0)
return -1;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
int r = schfs_dir_iterate(&VOL, &c, ino, tree_entry, (void *)path);
chain_pop();
return r;
}
/* ------------------------------------------------------------------ */
/* --extract */
/* ------------------------------------------------------------------ */
// Objects already written out, so a hard link on the image becomes a hard link
// on the host rather than a second copy. Without this the round-trip test's
// link-count check could not distinguish dedup from duplication.
struct seen_ent { uint64_t ino; char *path; };
static struct seen_ent *seen;
static int seen_n, seen_cap;
static const char *
seen_lookup(uint64_t ino)
{
for (int i = 0; i < seen_n; i++)
if (seen[i].ino == ino)
return seen[i].path;
return NULL;
}
static void
seen_add(uint64_t ino, const char *path)
{
if (seen_n == seen_cap) {
seen_cap = seen_cap ? seen_cap * 2 : 128;
seen = realloc(seen, (size_t)seen_cap * sizeof(*seen));
if (!seen) { perror("realloc"); exit(1); }
}
seen[seen_n].ino = ino;
seen[seen_n].path = strdup(path);
seen_n++;
}
static long xfiles, xdirs, xlinks, xsymlinks;
static int extract_dir(uint64_t ino, const char *hostpath);
static int
extract_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
const char *parent = arg;
char host[8192];
(void)type; /* the inode is authoritative, not the dirent */
snprintf(host, sizeof(host), "%s/%.*s", parent, (int)namelen, name);
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
fprintf(stderr, "schfs_fsck: %s: inode %llu unreadable\n",
host, (unsigned long long)ino);
return -1;
}
if (in.type == SCHFS_T_DIR) {
if (mkdir(host, in.mode & 07777) != 0 && errno != EEXIST) {
perror(host);
return -1;
}
xdirs++;
return extract_dir(ino, host);
}
if (in.type == SCHFS_T_SYMLINK) {
char target[4096];
if (in.size >= sizeof(target)) {
fprintf(stderr, "schfs_fsck: %s: symlink target too long\n", host);
return -1;
}
long got = schfs_file_read(&VOL, &c, ino, 0, target,
(unsigned)in.size, DATABLK);
if (got < 0 || (uint64_t)got != in.size) {
fprintf(stderr, "schfs_fsck: %s: could not read symlink target\n", host);
return -1;
}
target[in.size] = '\0';
unlink(host);
if (symlink(target, host) != 0) { perror(host); return -1; }
xsymlinks++;
return 0;
}
if (in.type == SCHFS_T_DEV) {
// Creating a real device node needs privileges the build does not have,
// so record it as an empty placeholder. The round-trip tree contains no
// device nodes; --check is what validates them.
FILE *f = fopen(host, "wb");
if (!f) { perror(host); return -1; }
fclose(f);
return 0;
}
// A second name for an object already written: make it a real hard link so
// the extracted tree has the same link structure as the image.
const char *first = seen_lookup(ino);
if (first) {
unlink(host);
if (link(first, host) != 0) { perror(host); return -1; }
xlinks++;
return 0;
}
FILE *f = fopen(host, "wb");
if (!f) { perror(host); return -1; }
uint64_t off = 0;
while (off < in.size) {
// A full block is requested every time, even at the tail. Clamping
// here instead would mean nothing ever tests that schfs_file_read
// stops at end-of-file -- and the kernel's read(2) will depend on
// exactly that, since a real program just asks for a bufferful.
char buf[SCHFS_BLKSIZE];
unsigned want = sizeof(buf);
long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK);
if (got <= 0) {
fprintf(stderr, "schfs_fsck: %s: read error at %llu\n",
host, (unsigned long long)off);
fclose(f);
return -1;
}
fwrite(buf, 1, (size_t)got, f);
off += (uint64_t)got;
}
fclose(f);
if (chmod(host, in.mode & 07777) != 0)
perror(host);
seen_add(ino, host);
xfiles++;
return 0;
}
static int
extract_dir(uint64_t ino, const char *hostpath)
{
if (chain_push(ino, hostpath) != 0)
return -1;
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
int r = schfs_dir_iterate(&VOL, &c, ino, extract_entry, (void *)hostpath);
chain_pop();
return r;
}
static int
do_extract(const char *dir)
{
if (mkdir(dir, 0755) != 0 && errno != EEXIST) {
perror(dir);
return 1;
}
if (extract_dir(VOL.root_objectid, dir) != 0)
return 1;
printf("extracted %ld dirs, %ld files, %ld hard links, %ld symlinks into %s\n",
xdirs, xfiles, xlinks, xsymlinks, dir);
return 0;
}
/* ------------------------------------------------------------------ */
int
main(int argc, char **argv)
{
if (argc < 3) {
fprintf(stderr,
"usage: schfs_fsck img.schfs <action>\n"
" --info uberblock ring and volume summary\n"
" --check full structural and data verification\n"
" --ls PATH list a directory\n"
" --stat PATH one object's metadata\n"
" --cat PATH a file's contents\n"
" --tree every path in the volume\n"
" --extract DIR write the whole tree onto the host\n");
return 2;
}
const char *imgpath = argv[1];
const char *action = argv[2];
const char *arg = argc > 3 ? argv[3] : NULL;
// Must match whatever mkschfs used, or every directory lookup misses.
for (int i = 3; i + 1 < argc; i++)
if (strcmp(argv[i], "--hash-bits") == 0) {
int bits = atoi(argv[i + 1]);
schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1);
if (arg == argv[i])
arg = NULL;
}
FILE *f = fopen(imgpath, "rb");
if (!f) { perror(imgpath); return 1; }
if (fseeko(f, 0, SEEK_END) != 0) { perror(imgpath); return 1; }
off_t sz = ftello(f);
IMG.f = f;
IMG.nblocks = (uint64_t)sz / SCHFS_BLKSIZE;
struct schfs_bdev bd;
bd.ctx = &IMG;
bd.bread = img_read;
bd.bwrite = img_write;
bd.balloc = img_alloc;
bd.bfree = img_nofree;
bd.balloc_run = NULL; /* read-only: nothing is ever allocated */
// No page allocator: this tool never writes, and a volume without an
// allocation map cannot allocate even by accident.
int r = schfs_vol_open(&VOL, &bd, &CTX, NULL);
if (r == -1) {
fprintf(stderr, "schfs_fsck: %s: no valid uberblock -- not a SchFS "
"volume, or never successfully written\n", imgpath);
return 1;
}
if (r != 0) {
fprintf(stderr, "schfs_fsck: %s: newest uberblock names an unreadable "
"tree root\n", imgpath);
return 1;
}
if (strcmp(action, "--info") == 0)
return do_info();
if (strcmp(action, "--check") == 0)
return do_check();
if (strcmp(action, "--tree") == 0)
return tree_walk(VOL.root_objectid, "");
if (strcmp(action, "--ls") == 0 && arg)
return do_ls(arg);
if (strcmp(action, "--stat") == 0 && arg)
return do_stat(arg);
if (strcmp(action, "--cat") == 0 && arg)
return do_cat(arg);
if (strcmp(action, "--extract") == 0 && arg)
return do_extract(arg);
fprintf(stderr, "schfs_fsck: unknown or incomplete action '%s'\n", action);
return 2;
}