The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
819 lines
27 KiB
C
819 lines
27 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"
|
|
// Included for schfs_index_reload, which schfs_vol_open calls at mount. This
|
|
// tool never writes, so the rest of attr.c is dead weight here -- but linking
|
|
// against a filesystem whose mount path is missing a function is worse.
|
|
#include "../kernel/fs/schfs/attr.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;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------- */
|
|
/* --snapshots */
|
|
/* */
|
|
/* A snapshot is a retained tree root, stored as an ordinary record under */
|
|
/* SCHFS_SNAP_OBJECTID. Listing them is a range scan over that objectid. */
|
|
/* --------------------------------------------------------------------- */
|
|
|
|
static int
|
|
do_snapshots(void)
|
|
{
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, 0);
|
|
|
|
int r = schfs_cursor_seek(&c, &k);
|
|
if (r == -2) {
|
|
fprintf(stderr, "schfs_fsck: tree is unreadable\n");
|
|
return 1;
|
|
}
|
|
|
|
int n = 0;
|
|
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 *sn =
|
|
(const struct schfs_snap *)schfs_cursor_val(&c, &len);
|
|
if (sn && len == sizeof(*sn)) {
|
|
if (n == 0)
|
|
printf("%-6s %-24s %-12s %s\n",
|
|
"id", "name", "root", "created at");
|
|
// The name is bounded but not guaranteed terminated by a bad
|
|
// image, so it is printed with an explicit precision.
|
|
printf("%-6llu %-24.*s %-12llu generation %llu\n",
|
|
(unsigned long long)got->offset,
|
|
SCHFS_SNAP_NAME_MAX, sn->name,
|
|
(unsigned long long)sn->root,
|
|
(unsigned long long)sn->created);
|
|
n++;
|
|
}
|
|
r = schfs_cursor_next(&c);
|
|
}
|
|
|
|
if (n == 0)
|
|
printf("no snapshots\n");
|
|
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"
|
|
" --snapshots list the retained tree roots\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;
|
|
memset(&bd, 0, sizeof(bd)); /* optional hooks must read as absent */
|
|
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);
|
|
if (strcmp(action, "--snapshots") == 0)
|
|
return do_snapshots();
|
|
|
|
fprintf(stderr, "schfs_fsck: unknown or incomplete action '%s'\n", action);
|
|
return 2;
|
|
}
|