SchOS/tools/schfs_roundtrip.sh
Nexoniarz d0f7acf9eb Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
2026-08-09 13:47:01 +02:00

120 lines
4.6 KiB
Shell

#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
#
# SchFS round-trip test: build an image from a directory tree, read it back
# with a completely separate program, and diff the result against the original.
#
# The point is the oracle. schfs_test checks the B-tree against a model written
# for the purpose; this checks the *filesystem* against a tree produced by
# something with no connection to SchFS at all -- the host's own filesystem.
# If mkschfs and schfs_fsck ever agree on something wrong, the source tree still
# disagrees, and diff says so.
#
# tools/schfs_roundtrip.sh <mkschfs> <schfs_fsck> [tree]
#
# With no tree, a synthetic one is generated covering the cases that have
# historically broken image builders: empty files, empty directories, files
# spanning several blocks, files ending exactly on a block boundary, duplicate
# content that must be hard-linked, symlinks, deep nesting and awkward names.
set -e
# Every tool here is piped into sed for indenting, and a shell only checks the
# *last* command of a pipeline. Without this, schfs_fsck could exit non-zero --
# or crash outright -- and the script would sail past it, which is exactly what
# happened the first time this ran.
if (set -o pipefail) 2>/dev/null; then set -o pipefail; fi
MKSCHFS=${1:?usage: schfs_roundtrip.sh <mkschfs> <schfs_fsck> [tree]}
FSCK=${2:?usage: schfs_roundtrip.sh <mkschfs> <schfs_fsck> [tree]}
TREE=$3
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
if [ -z "$TREE" ]; then
# Generated by a separate script so the same tree can be rebuilt by hand
# when debugging a failure, without running the whole round-trip.
TREE="$WORK/tree"
sh "$(dirname "$0")/mktree.sh" "$TREE"
fi
IMG="$WORK/out.img"
OUT="$WORK/extracted"
echo "SchFS round-trip"
echo " source tree: $TREE"
"$MKSCHFS" "$IMG" --size 4096 --label roundtrip --sysroot "$TREE" | sed 's/^/ /'
echo
"$FSCK" "$IMG" --check | sed 's/^/ /'
mkdir -p "$OUT"
"$FSCK" "$IMG" --extract "$OUT" | sed 's/^/ /'
echo
# --no-dereference so a symlink is compared as a symlink rather than followed;
# without it a broken link that was stored correctly still fails the diff.
if diff -r --no-dereference "$TREE" "$OUT"; then
echo " [ok] extracted tree is identical to the source"
else
echo " FAIL: extracted tree differs from the source"
exit 1
fi
# Hard links are the one thing diff cannot see: it compares contents, and two
# separate copies with identical contents pass. Check the link count directly,
# or dedup could silently degrade to duplication and nothing would notice.
LINKS=$("$FSCK" "$IMG" --stat /bin/multiblock | sed -n 's/^nlink: *//p')
if [ "$LINKS" != "2" ]; then
echo " FAIL: /bin/multiblock has nlink=$LINKS, expected 2 (dedup did not hard-link)"
exit 1
fi
echo " [ok] duplicate content stored once, with nlink=2"
# ...and the converse: same size, different bytes must stay separate.
DLINKS=$("$FSCK" "$IMG" --stat /bin/decoy | sed -n 's/^nlink: *//p')
if [ "$DLINKS" != "1" ]; then
echo " FAIL: /bin/decoy has nlink=$DLINKS, expected 1 (dedup merged unequal files)"
exit 1
fi
echo " [ok] same-size different-content files kept separate"
# Building the same tree twice must produce the same bytes. A difference means
# something non-deterministic crept in, which makes every future image diff
# useless as a signal.
IMG2="$WORK/out2.img"
"$MKSCHFS" "$IMG2" --size 4096 --label roundtrip --sysroot "$TREE" > /dev/null
if cmp -s "$IMG" "$IMG2"; then
echo " [ok] rebuilding the same tree produces an identical image"
else
echo " FAIL: image build is not reproducible"
exit 1
fi
# Second pass with a deliberately crippled name hash.
#
# With the real 64-bit hash, two names in one directory never collide, so the
# packed-dirent list -- the fiddliest code in the object layer -- never runs at
# all. Six bits means 64 buckets for ~330 names, so every bucket holds several
# entries and lookup, readdir and the append-on-collision path all get properly
# exercised. Both tools must agree on the mask or nothing resolves.
echo
echo " forcing hash collisions (6-bit name hash)"
IMG3="$WORK/collide.img"
OUT3="$WORK/collide-out"
"$MKSCHFS" "$IMG3" --size 4096 --hash-bits 6 --sysroot "$TREE" | sed 's/^/ /'
"$FSCK" "$IMG3" --check --hash-bits 6 | sed 's/^/ /'
mkdir -p "$OUT3"
"$FSCK" "$IMG3" --extract "$OUT3" --hash-bits 6 | sed 's/^/ /'
if diff -r --no-dereference "$TREE" "$OUT3" > /dev/null; then
echo " [ok] identical tree survives a directory hash that collides constantly"
else
echo " FAIL: colliding-hash image does not round-trip"
diff -r --no-dereference "$TREE" "$OUT3" | head -10
exit 1
fi
echo
echo "round-trip passed"