From 36614b69c569f1c8f89a4cf714bde8b41ac7e397 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Mon, 10 Aug 2026 19:33:54 +0200 Subject: [PATCH] schfs_fsck: list snapshots, and mkschfs can make one --snapshots is a range scan over SCHFS_SNAP_OBJECTID, printing each retained root's id, name, block and the generation it was taken at. The name is printed with an explicit precision: it is bounded but a damaged image need not have terminated it. mkschfs grows --snapshot NAME, which takes one of the finished tree. It runs after the commit, because a snapshot names a *committed* root -- taking it before would record a root that exists only in memory. Verified end to end: an image built with --snapshot golden lists the snapshot and still passes --check. An image without one prints "no snapshots" rather than nothing at all. Co-Authored-By: Claude Opus 5 --- tools/mkschfs.c | 19 ++++++++++++++- tools/schfs_fsck.c | 59 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/tools/mkschfs.c b/tools/mkschfs.c index be24376..adb2b56 100644 --- a/tools/mkschfs.c +++ b/tools/mkschfs.c @@ -590,6 +590,10 @@ int main(int argc, char **argv) { const char *out = NULL, *sysroot = NULL, *label = "schos"; + // Take a snapshot of the finished tree. Mostly for testing the snapshot + // paths end to end -- an image that ships with one is a perfectly ordinary + // thing to want, and it is one call. + const char *snapname = NULL; uint64_t nblocks = 6000; for (int i = 1; i < argc; i++) { @@ -599,6 +603,8 @@ main(int argc, char **argv) nblocks = strtoull(argv[++i], NULL, 0); else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc) label = argv[++i]; + else if (strcmp(argv[i], "--snapshot") == 0 && i + 1 < argc) + snapname = argv[++i]; else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) { if (nexcludes >= MAXEXCLUDE) { fprintf(stderr, "mkschfs: too many --exclude paths (max %d)\n", @@ -628,7 +634,7 @@ main(int argc, char **argv) if (!out || !sysroot) { fprintf(stderr, "usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\n" - " [--exclude PATH]...\n" + " [--exclude PATH]... [--snapshot NAME]\n" " BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n" " --exclude omits a sysroot-relative path and its subtree\n", SCHFS_BLKSIZE, (int)(6000 * SCHFS_BLKSIZE / (1024 * 1024))); @@ -689,6 +695,17 @@ main(int argc, char **argv) r = schfs_vol_commit(&VOL); if (r != 0) die("committing the uberblock", r); + // A snapshot of the finished tree, if one was asked for. After the commit + // above, because a snapshot names a *committed* root -- taking it before + // would record a root that only exists in memory. + if (snapname) { + unsigned n = (unsigned)strlen(snapname); + r = schfs_snap_create(&VOL, 1, snapname, n); + if (r != 0) die("creating the snapshot", r); + r = schfs_vol_commit(&VOL); + if (r != 0) die("committing the snapshot", r); + } + if (fflush(f) != 0 || fclose(f) != 0) { perror(out); return 1; } uint64_t used = nblocks - VOL.free_blocks - SCHFS_FIRST_BLOCK; diff --git a/tools/schfs_fsck.c b/tools/schfs_fsck.c index 72151f4..7663137 100644 --- a/tools/schfs_fsck.c +++ b/tools/schfs_fsck.c @@ -676,6 +676,60 @@ do_extract(const char *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 @@ -690,7 +744,8 @@ main(int argc, char **argv) " --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"); + " --extract DIR write the whole tree onto the host\n" + " --snapshots list the retained tree roots\n"); return 2; } @@ -751,6 +806,8 @@ main(int argc, char **argv) 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;