SchOS/tools/mkschfs.c
Nexoniarz 020aa6113c Users and permissions: accounts, login, enforced file permissions, setuid, su, passwd, doas
The kernel now checks owner/group/other on open, create, unlink, rename, exec
and path search; honours set-user-id and set-group-id; restricts kill, mount,
reboot and mknod; keeps supplementary groups; and implements chown. devtmpfs
nodes carry owners and modes that chmod/chown keep. Terminals run login, the
image has root and schos, X starts as the ordinary user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 13:16:07 +01:00

814 lines
30 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// mkschfs — build a SchFS image from a host directory tree.
//
// This is the tool that makes the on-disk format real: everything the kernel
// mounts in phase 2 is produced here. It deliberately shares the actual
// filesystem code (btree.c, vol.c, obj.c, objw.c) rather than reimplementing
// the layout, so a disagreement between writer and reader is impossible by
// construction — there is only one implementation, exercised on the host where
// a mistake costs a second instead of a boot.
//
// mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME] [--owners FILE]
//
// Two behaviours worth knowing about:
//
// * Identical file contents are stored once and hard-linked. sysroot/bin
// holds a full copy of the toybox binary per applet, so this is the
// difference between a 190 MB image and a 6 MB one.
// * Output is reproducible. Directory entries are sorted, the UUID is
// derived rather than random, and timestamps come from the source files,
// so building the same tree twice gives byte-identical images and a diff
// means something actually changed.
#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 <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
/* major()/minor() for device nodes. glibc puts them here; mingw has no such
header and no device numbers at all, because Windows has no mknod. On that
host the macros are stubbed: a device node cannot exist in the source tree
being imaged, so the values are never anything but zero, and stubbing is
honest where pretending to read st_rdev would not be. */
#ifdef __MINGW32__
#define major(x) 0
#define minor(x) 0
/* Symlinks, likewise. Windows has no lstat/readlink and mingw exposes neither,
so on this host a symlink cannot be encountered while walking the tree --
lstat degrades to stat, nothing is ever S_ISLNK, and readlink can only be
reached through a branch that is now unreachable. Verified rather than
assumed: `find sysroot -type l` returns nothing.
If symlinks are ever staged (procfs wants /proc/<pid>/exe to be one), this
host stops being able to build a faithful image and the stub must become a
real Win32 reparse-point read. Failing loudly then is the point of the
abort() below. */
#define lstat(p, b) stat((p), (b))
#define S_ISLNK(m) 0
static long readlink(const char *p, char *b, unsigned long n)
{ (void)p; (void)b; (void)n; return -1; }
#elif defined(__linux__)
/* glibc and musl both put major()/minor() here, and glibc warns and then
errors if you take them from <sys/types.h> instead. Linux only: macOS and
the BSDs have no <sys/sysmacros.h> at all and define both in <sys/types.h>,
which is already included above -- including it unconditionally is a build
failure on every non-Linux Unix. */
#include <sys/sysmacros.h>
#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"
#include "../kernel/fs/schfs/objw.c"
#include "../kernel/fs/schfs/attr.c"
/* ------------------------------------------------------------------ */
/* File-backed block device */
/* ------------------------------------------------------------------ */
struct imgdev {
FILE *f;
struct schfs_vol *vol;
uint64_t nblocks;
long reads, writes;
};
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;
d->reads++;
return 0;
}
static int
img_write(void *ctx, uint64_t b, const 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 (fwrite(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
return -1;
d->writes++;
return 0;
}
// Make what has been written actually reach the file.
//
// The kernel's version of this hook drains the buffer cache and then tells the
// drive to commit its own cache; here the equivalent stages are stdio's buffer
// and the operating system's page cache. The image mkschfs is building is
// normally consumed by something else the moment it exits, so this is mostly
// belt and braces -- but the commit path calls it, and a hook that lies about
// having flushed is exactly the bug this whole change set is about.
static int
img_flush(void *ctx)
{
struct imgdev *d = ctx;
if (fflush(d->f) != 0)
return -1;
return 0;
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* */
/* Straight through to the volume's space manager. An earlier version */
/* kept a build-time free list of its own, because the bump allocator it */
/* replaced could only ever take back the block it handed out last and */
/* a build otherwise leaked one root-to-leaf path per insert. Two things */
/* made that unnecessary: the space manager reuses any freed block, and */
/* the tree now rewrites its own nodes in place within a transaction */
/* group, so a build barely frees anything in the first place. */
/* ------------------------------------------------------------------ */
static uint64_t
img_alloc(void *ctx)
{
return schfs_vol_alloc(((struct imgdev *)ctx)->vol);
}
static uint64_t
img_alloc_run(void *ctx, uint64_t n)
{
return schfs_vol_alloc_run(((struct imgdev *)ctx)->vol, n);
}
static void
img_free(void *ctx, uint64_t b, uint64_t gen)
{
schfs_vol_free(((struct imgdev *)ctx)->vol, b, gen);
}
// Pages for the allocation bitmap. calloc rather than malloc so a bug that
// forgets to initialise one shows up as "everything is free" consistently
// rather than as whatever the heap happened to contain.
static void *
img_palloc(void)
{
return calloc(1, SCHFS_BLKSIZE);
}
static void
img_pfree(void *p)
{
free(p);
}
/* ------------------------------------------------------------------ */
/* Content dedup */
/* */
/* sysroot/bin contains one independent ~800 KB copy of toybox per */
/* applet name. Storing each separately is the difference between an */
/* image that fits in RAM and one that does not, so identical content */
/* becomes one object with several names -- a plain hard link, which */
/* the format already supports through nlink. */
/* ------------------------------------------------------------------ */
struct dedup_ent {
char *path;
dev_t hdev; /* host device+inode: when these match, */
ino_t hino; /* it is literally the same file */
off_t size;
uint32_t crc;
uint64_t ino;
};
static struct dedup_ent *dedup_tab;
static int dedup_n, dedup_cap;
/* A file the host already told us is the same file.
*
* ports/toybox/Makefile hard-links its 236 applet names to one binary, so this
* settles all but the first of them without reading anything: the content path
* below reads the candidate to CRC it and then re-reads both files to confirm,
* which is half a gigabyte of I/O for an answer fstat already has. */
static uint64_t
dedup_lookup_ino(dev_t hdev, ino_t hino)
{
if (hino == 0)
return 0;
for (int i = 0; i < dedup_n; i++)
if (dedup_tab[i].hdev == hdev && dedup_tab[i].hino == hino)
return dedup_tab[i].ino;
return 0;
}
static int
files_identical(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb"), *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return 0; }
int same = 1;
for (;;) {
char ba[8192], bb[8192];
size_t na = fread(ba, 1, sizeof(ba), fa);
size_t nb = fread(bb, 1, sizeof(bb), fb);
if (na != nb || memcmp(ba, bb, na) != 0) { same = 0; break; }
if (na == 0) break;
}
fclose(fa); fclose(fb);
return same;
}
// Size and CRC agreeing is not proof, so a candidate is confirmed byte by byte.
// Silently hard-linking two files that merely collide would corrupt the image
// in a way nothing downstream could detect.
static uint64_t
dedup_lookup(const char *path, off_t size, uint32_t crc)
{
for (int i = 0; i < dedup_n; i++)
if (dedup_tab[i].size == size && dedup_tab[i].crc == crc &&
files_identical(dedup_tab[i].path, path))
return dedup_tab[i].ino;
return 0;
}
static void
dedup_remember(const char *path, const struct stat *st,
off_t size, uint32_t crc, uint64_t ino)
{
dev_t dedup_hdev = st->st_nlink > 1 ? st->st_dev : 0;
ino_t dedup_hino = st->st_nlink > 1 ? st->st_ino : 0;
if (dedup_n == dedup_cap) {
dedup_cap = dedup_cap ? dedup_cap * 2 : 256;
dedup_tab = realloc(dedup_tab, (size_t)dedup_cap * sizeof(*dedup_tab));
if (!dedup_tab) { perror("realloc"); exit(1); }
}
dedup_tab[dedup_n].path = strdup(path);
dedup_tab[dedup_n].hdev = dedup_hdev;
dedup_tab[dedup_n].hino = dedup_hino;
dedup_tab[dedup_n].size = size;
dedup_tab[dedup_n].crc = crc;
dedup_tab[dedup_n].ino = ino;
dedup_n++;
}
/* ------------------------------------------------------------------ */
static struct schfs_vol VOL;
static char DATABLK[SCHFS_BLKSIZE];
static char CURBLK[SCHFS_BLKSIZE];
// The volume's three scratch blocks. Statics rather than locals because the
// same code runs in the kernel, where a 4096-byte local is the whole stack —
// keeping the shapes identical is what makes the host the place bugs are found.
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 long nfiles, ndirs, nlinks, nsymlinks, ndevs;
static long long nbytes, ndeduped;
static void
die(const char *what, int r)
{
fprintf(stderr, "mkschfs: %s failed (%d)%s\n", what, r,
r == -3 ? " -- image is full, try a larger --size" : "");
exit(1);
}
/* Who owns what. The host's owners mean nothing inside the image -- every file
came from whoever ran the build -- so everything is root's unless a rule in
the --owners file says otherwise. One rule per line, `path uid gid [mode]`,
with the path relative to the sysroot ("/home/schos"). uid and gid apply to
the path and to everything below it; a mode, in octal, to that path alone.
Later rules win. */
struct owner_rule { char *path; uint32_t uid, gid; int has_mode; uint32_t mode; };
static struct owner_rule *owner_rules;
static int nowner_rules;
static const char *cur_rel; /* the entry being added, set by add_entry */
static void
load_owners(const char *file)
{
FILE *f = fopen(file, "r");
char line[1024];
if (!f) { perror(file); exit(1); }
while (fgets(line, sizeof line, f)) {
char path[900];
unsigned uid, gid, mode;
int n;
if (line[0] == '#' || line[0] == '\n')
continue;
n = sscanf(line, "%899s %u %u %o", path, &uid, &gid, &mode);
if (n < 3) { fprintf(stderr, "mkschfs: bad owners line: %s", line); exit(1); }
owner_rules = realloc(owner_rules, (size_t)(nowner_rules + 1) * sizeof *owner_rules);
owner_rules[nowner_rules].path = strdup(path);
owner_rules[nowner_rules].uid = uid;
owner_rules[nowner_rules].gid = gid;
owner_rules[nowner_rules].has_mode = n == 4;
owner_rules[nowner_rules].mode = mode;
nowner_rules++;
}
fclose(f);
}
static void
apply_owners(struct schfs_inode *in)
{
if (!cur_rel)
return;
for (int i = 0; i < nowner_rules; i++) {
const char *r = owner_rules[i].path;
size_t n;
if (*r == '/')
r++; /* cur_rel is relative: "home/schos" */
n = strlen(r);
int exact = strcmp(cur_rel, r) == 0;
if (!exact && !(strncmp(cur_rel, r, n) == 0 && cur_rel[n] == '/'))
continue;
in->uid = owner_rules[i].uid;
in->gid = owner_rules[i].gid;
if (exact && owner_rules[i].has_mode)
in->mode = owner_rules[i].mode;
}
}
static void
inode_init(struct schfs_inode *in, const struct stat *st, uint16_t type,
uint64_t parent)
{
memset(in, 0, sizeof(*in));
in->type = type;
in->parent = parent;
in->nlink = 1;
in->mode = st ? (uint32_t)(st->st_mode & 07777)
: (type == SCHFS_T_DIR ? 0755u : 0644u);
in->uid = 0;
in->gid = 0;
apply_owners(in);
if (st) {
// All three come from mtime, deliberately.
//
// Copying the host's atime would make the output depend on when the
// source files were last *read* -- and mkschfs reads every one of them,
// so building the same tree twice produced two different images. ctime
// is similar: it tracks host inode changes that mean nothing inside the
// image. mtime is the only one that describes the content, which is the
// only thing being copied.
in->atime = (uint64_t)st->st_mtime;
in->mtime = (uint64_t)st->st_mtime;
in->ctime = (uint64_t)st->st_mtime;
}
}
static void
bump_nlink(uint64_t ino)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
int r = schfs_inode_get(&VOL, &c, ino, &in);
if (r != 0)
die("re-reading an inode for a hard link", r);
in.nlink++;
// Under v1 this was the bug that shipped broken images: nlink was bumped
// but the inode's checksum was not recomputed, so every deduped file failed
// its own integrity check on first open. Here the checksum belongs to the
// tree node and is recomputed by the insert, so it cannot get out of step.
r = schfs_inode_put(&VOL, ino, &in);
if (r != 0)
die("writing back a hard link count", r);
}
static uint64_t
add_file(uint64_t parent, const char *host, const char *name,
const struct stat *st)
{
// A hard link is the same inode by definition, so ask the host before
// reading anything.
if (st->st_nlink > 1) {
uint64_t hit = dedup_lookup_ino(st->st_dev, st->st_ino);
if (hit) {
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
hit, SCHFS_T_FILE);
if (r != 0)
die("linking a deduplicated file", r);
bump_nlink(hit);
nlinks++;
ndeduped += st->st_size;
return hit;
}
}
// CRC the content: it is both the dedup key and a cheap way to avoid
// reading candidates that cannot possibly match.
FILE *f = fopen(host, "rb");
if (!f) { perror(host); exit(1); }
uint32_t crc = 0;
for (;;) {
char buf[8192];
size_t n = fread(buf, 1, sizeof(buf), f);
if (n == 0) break;
crc = schfs_crc32c(crc, buf, (unsigned)n);
}
uint64_t existing = dedup_lookup(host, st->st_size, crc);
if (existing) {
fclose(f);
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
existing, SCHFS_T_FILE);
if (r != 0)
die("linking a deduplicated file", r);
bump_nlink(existing);
nlinks++;
ndeduped += st->st_size;
return existing;
}
// The final size is known before a byte is written, so the block count goes
// into the inode on its first and only write, and the whole run of data
// blocks is claimed up front. That is what collapses a multi-megabyte file
// into a single extent record instead of one per 4 KB block.
uint64_t need = ((uint64_t)st->st_size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, st, SCHFS_T_FILE, parent);
in.blocks = need;
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a file inode", r);
r = schfs_file_reserve(&VOL, ino, need);
if (r != 0) die("reserving data blocks", r);
rewind(f);
for (;;) {
// Large chunks on purpose: each append call re-reads and rewrites the
// inode, and every tree write copies its path, so appending a big file
// 8 KB at a time spends more space on metadata churn than on data.
static char buf[256 * 1024];
size_t n = fread(buf, 1, sizeof(buf), f);
if (n == 0) break;
r = schfs_file_append(&VOL, ino, buf, (unsigned)n, DATABLK);
if (r != 0) die("writing file data", r);
nbytes += (long long)n;
}
fclose(f);
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
ino, SCHFS_T_FILE);
if (r != 0) die("adding a directory entry", r);
dedup_remember(host, st, st->st_size, crc, ino);
nfiles++;
return ino;
}
static void
add_symlink(uint64_t parent, const char *host, const char *name,
const struct stat *st)
{
char target[4096];
ssize_t n = readlink(host, target, sizeof(target) - 1);
if (n < 0) { perror(host); exit(1); }
target[n] = '\0';
uint64_t need = ((uint64_t)n + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, st, SCHFS_T_SYMLINK, parent);
in.blocks = need;
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a symlink inode", r);
// The target is stored as the object's data, exactly like a file's
// contents, so nothing special is needed to read it back.
r = schfs_file_reserve(&VOL, ino, need);
if (r != 0) die("reserving a symlink block", r);
r = schfs_file_append(&VOL, ino, target, (unsigned)n, DATABLK);
if (r != 0) die("writing a symlink target", r);
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
ino, SCHFS_T_SYMLINK);
if (r != 0) die("adding a symlink entry", r);
nsymlinks++;
}
static void
add_dev(uint64_t parent, const char *name, const struct stat *st)
{
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, st, SCHFS_T_DEV, parent);
in.rdev_major = (uint16_t)major(st->st_rdev);
in.rdev_minor = (uint16_t)minor(st->st_rdev);
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a device inode", r);
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
ino, SCHFS_T_DEV);
if (r != 0) die("adding a device entry", r);
ndevs++;
}
/* Sorted directory listing, so images are reproducible. */
struct namelist { char **v; int n, cap; };
static void
nl_add(struct namelist *l, const char *s)
{
if (l->n == l->cap) {
l->cap = l->cap ? l->cap * 2 : 64;
l->v = realloc(l->v, (size_t)l->cap * sizeof(*l->v));
if (!l->v) { perror("realloc"); exit(1); }
}
l->v[l->n++] = strdup(s);
}
static int
nl_cmp(const void *a, const void *b)
{
return strcmp(*(char *const *)a, *(char *const *)b);
}
static void add_tree(uint64_t dirino, const char *hostdir);
// Paths kept out of the image, sysroot-relative.
//
// Same rule and same spelling as mkfs.c, because the two tools build the same
// tree and a path excluded from one must be excluded from the other. This is a
// licensing mechanism before it is a size one: the ramdisk image is linked into
// the kernel binary, so anything staged in sysroot/ when it is built becomes
// part of a distributable artifact.
#define MAXEXCLUDE 32
static const char *excludes[MAXEXCLUDE];
static int nexcludes;
// strlen(sysroot) + 1, so a host path can be turned back into the
// sysroot-relative one the exclude patterns are written against.
static size_t sysroot_prefix;
// Matches exactly, or as a directory prefix so that "usr/share/misc" also drops
// "usr/share/misc/big.dat".
static int
is_excluded(const char *relpath)
{
for (int i = 0; i < nexcludes; i++) {
size_t n = strlen(excludes[i]);
if (strncmp(relpath, excludes[i], n) == 0 &&
(relpath[n] == '\0' || relpath[n] == '/'))
return 1;
}
return 0;
}
// Returns 1 if the entry was a directory, so the caller can total up the link
// count without walking the directory a second time.
static int
add_entry(uint64_t dirino, const char *hostdir, const char *name)
{
char path[8192];
snprintf(path, sizeof(path), "%s/%s", hostdir, name);
// Checked before the entry is dispatched, so an excluded directory is never
// opened and never gets an inode -- an empty directory left behind would
// still describe what was removed.
if (nexcludes && strlen(path) > sysroot_prefix &&
is_excluded(path + sysroot_prefix)) {
fprintf(stderr, "mkschfs: excluding %s\n", path + sysroot_prefix);
return 0;
}
struct stat st;
// lstat, not stat: a symlink must be stored as a symlink rather than
// silently turned into a second copy of whatever it points at.
if (lstat(path, &st) != 0) { perror(path); exit(1); }
cur_rel = strlen(path) >= sysroot_prefix ? path + sysroot_prefix : path;
if (strlen(name) > SCHFS_NAME_MAX) {
fprintf(stderr, "mkschfs: name too long (max %d): %s\n",
SCHFS_NAME_MAX, path);
exit(1);
}
if (S_ISDIR(st.st_mode)) {
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, &st, SCHFS_T_DIR, dirino);
in.nlink = 2; /* itself and its own "." */
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a directory inode", r);
r = schfs_dir_add(&VOL, dirino, name, (unsigned)strlen(name),
ino, SCHFS_T_DIR);
if (r != 0) die("adding a subdirectory entry", r);
ndirs++;
add_tree(ino, path);
return 1;
}
if (S_ISLNK(st.st_mode))
add_symlink(dirino, path, name, &st);
else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
add_dev(dirino, name, &st);
else if (S_ISREG(st.st_mode))
add_file(dirino, path, name, &st);
else
fprintf(stderr, "mkschfs: skipping %s (unsupported file type)\n", path);
return 0;
}
static void
add_tree(uint64_t dirino, const char *hostdir)
{
DIR *d = opendir(hostdir);
if (!d) { perror(hostdir); exit(1); }
struct namelist l = { 0, 0, 0 };
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
nl_add(&l, e->d_name);
}
closedir(d);
// qsort declares its first argument non-null, and an empty directory
// leaves l.v at NULL. Every implementation ignores the pointer when the
// count is zero; the standard does not say they have to.
if (l.n > 0)
qsort(l.v, l.n, sizeof(*l.v), nl_cmp);
long subdirs = 0;
for (int i = 0; i < l.n; i++) {
subdirs += add_entry(dirino, hostdir, l.v[i]);
free(l.v[i]);
}
free(l.v);
// A directory's link count is 2 (itself, and its own ".") plus one per
// subdirectory, because each subdirectory's ".." points back here. ".." is
// synthetic in SchFS, but the count still has to match what POSIX tools
// expect, or "find -type d" prunes real directories and du walks the tree
// wrongly. Written last because the count is not known until the children
// have been added.
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, dirino, &in) != 0)
die("re-reading a directory inode", -2);
in.nlink = (uint32_t)(2 + subdirs);
if (schfs_inode_put(&VOL, dirino, &in) != 0)
die("writing a directory link count", -2);
}
/* ------------------------------------------------------------------ */
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++) {
if (strcmp(argv[i], "--sysroot") == 0 && i + 1 < argc)
sysroot = argv[++i];
else if (strcmp(argv[i], "--size") == 0 && i + 1 < argc)
nblocks = strtoull(argv[++i], NULL, 0);
else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc)
label = argv[++i];
else if (strcmp(argv[i], "--owners") == 0 && i + 1 < argc)
load_owners(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",
MAXEXCLUDE);
return 1;
}
excludes[nexcludes++] = argv[++i];
}
else if (strcmp(argv[i], "--hash-bits") == 0 && i + 1 < argc) {
// Testing only: shrink the name hash so collisions are the norm
// rather than a once-in-a-lifetime event. schfs_fsck takes the
// same flag and must be given the same value to read the image.
int bits = atoi(argv[++i]);
schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1);
}
else if (argv[i][0] == '-') {
fprintf(stderr, "mkschfs: unknown option %s\n", argv[i]);
return 1;
} else if (!out)
out = argv[i];
else {
fprintf(stderr, "mkschfs: too many arguments\n");
return 1;
}
}
if (!out || !sysroot) {
fprintf(stderr,
"usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\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)));
return 1;
}
if (nblocks <= SCHFS_FIRST_BLOCK) {
fprintf(stderr, "mkschfs: --size must exceed %d blocks (boot block "
"plus the uberblock ring)\n", SCHFS_FIRST_BLOCK);
return 1;
}
FILE *f = fopen(out, "w+b");
if (!f) { perror(out); return 1; }
// Pre-extend so every block is readable. The bump allocator hands out
// blocks in order, but the tree reads a block back the moment it is
// written, and a short file would fail that read rather than the write.
if (ftruncate(fileno(f), (off_t)nblocks * SCHFS_BLKSIZE) != 0) {
perror("ftruncate");
return 1;
}
IMG.f = f;
IMG.vol = &VOL;
IMG.nblocks = nblocks;
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_free;
bd.balloc_run = img_alloc_run;
bd.bflush = img_flush;
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc, img_pfree);
if (r != 0) die("formatting", r);
// The root directory is an object like any other; nothing points at it
// except the uberblock's root_objectid.
struct schfs_inode root;
struct stat rst;
int have_rst = (lstat(sysroot, &rst) == 0);
inode_init(&root, have_rst ? &rst : NULL, SCHFS_T_DIR, 0);
root.mode = 0755;
root.nlink = 2;
r = schfs_inode_put(&VOL, VOL.root_objectid, &root);
if (r != 0) die("writing the root inode", r);
ndirs++;
// add_entry() builds host paths as "<sysroot>/<name>", so this is where the
// sysroot-relative part of such a path begins.
sysroot_prefix = strlen(sysroot) + 1;
add_tree(VOL.root_objectid, sysroot);
// Publish. Everything above this line is invisible to a reader; if the tool
// died half way through, the image would simply have no valid uberblock and
// be rejected as "not a SchFS volume" rather than mounted half-built.
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;
printf("mkschfs: %s\n", out);
printf(" %ld dirs, %ld files, %ld hard links, %ld symlinks, %ld devices\n",
ndirs, nfiles, nlinks, nsymlinks, ndevs);
printf(" %lld bytes of data written, %lld bytes saved by dedup\n",
nbytes, ndeduped);
printf(" %llu of %llu blocks used (%llu KB of %llu KB), generation %llu\n",
(unsigned long long)used, (unsigned long long)nblocks,
(unsigned long long)used * SCHFS_BLKSIZE / 1024,
(unsigned long long)nblocks * SCHFS_BLKSIZE / 1024,
(unsigned long long)VOL.generation);
return 0;
}