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.
This commit is contained in:
parent
531ef120ef
commit
754d96ebc5
26 changed files with 127 additions and 39 deletions
111
tools/posix/memory-sync.sh
Normal file
111
tools/posix/memory-sync.sh
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/bin/sh
|
||||
# Move Claude Code's project memory between this repo and the local machine.
|
||||
#
|
||||
# tools/posix/memory-sync.sh export memory on this machine -> .claude/memory/
|
||||
# tools/posix/memory-sync.sh import .claude/memory/ -> this machine
|
||||
# tools/posix/memory-sync.sh status show both sides, change nothing
|
||||
#
|
||||
# Why this exists
|
||||
# ---------------
|
||||
# Claude Code keeps per-project memory outside the repo, under
|
||||
#
|
||||
# <home>/.claude/projects/<key>/memory/
|
||||
#
|
||||
# where <key> is the project's absolute path with the separators replaced by
|
||||
# dashes: D:\DEV\SchOS becomes D--DEV-SchOS, /home/me/dev/SchOS becomes
|
||||
# -home-me-dev-SchOS. Two consequences follow, and both have already bitten
|
||||
# this project once:
|
||||
#
|
||||
# * Renaming the directory orphans the memory. D:\DEV\Schunix -> D:\DEV\SchOS
|
||||
# changed the key, and everything written under the old one became
|
||||
# invisible until it was moved across by hand.
|
||||
# * The key is machine-specific, so memory written on Windows is not where
|
||||
# Linux looks for it, even for the same checkout.
|
||||
#
|
||||
# Committing the memory to the repo fixes both: git carries it between hosts
|
||||
# and across renames, and `import` puts it wherever this particular machine
|
||||
# expects to find it. Run `export` after a session that learned something and
|
||||
# `import` after cloning or pulling on another machine.
|
||||
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 {export|import|status}" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ $# -eq 1 ] || usage
|
||||
|
||||
# Repo root: this script lives in tools/posix/, so two levels up.
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repo=$(CDPATH= cd -- "$script_dir/../.." && pwd)
|
||||
repo_mem="$repo/.claude/memory"
|
||||
|
||||
# The absolute path Claude Code itself sees. Under Git Bash `pwd` gives
|
||||
# /d/DEV/SchOS, but Claude Code is a Windows program and uses D:\DEV\SchOS, so
|
||||
# the key must be derived from the Windows form or it will not match.
|
||||
native_path="$repo"
|
||||
case "$(uname -s)" in
|
||||
MINGW* | MSYS* | CYGWIN*)
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
native_path=$(cygpath -w "$repo")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Encode: every separator becomes a dash, and so does the drive colon.
|
||||
#
|
||||
# D:\DEV\SchOS -> D--DEV-SchOS
|
||||
# /home/me/dev/SchOS -> -home-me-dev-SchOS
|
||||
#
|
||||
# The colon maps to a dash rather than being deleted -- that is where the
|
||||
# doubled dash in the Windows form comes from, and dropping it instead yields
|
||||
# D-DEV-SchOS, which matches nothing.
|
||||
key=$(printf '%s' "$native_path" | tr '/\\:' '---')
|
||||
|
||||
home_dir=${HOME:-$USERPROFILE}
|
||||
host_mem="$home_dir/.claude/projects/$key/memory"
|
||||
|
||||
count() { [ -d "$1" ] && find "$1" -name '*.md' 2>/dev/null | wc -l | tr -d ' ' || echo 0; }
|
||||
|
||||
case "$1" in
|
||||
status)
|
||||
echo "repo: $repo"
|
||||
echo "as Claude sees it: $native_path"
|
||||
echo "project key: $key"
|
||||
echo
|
||||
echo "in repo: $repo_mem"
|
||||
echo " $(count "$repo_mem") memories"
|
||||
echo "on this host: $host_mem"
|
||||
echo " $(count "$host_mem") memories"
|
||||
;;
|
||||
|
||||
export)
|
||||
if [ ! -d "$host_mem" ]; then
|
||||
echo "memory-sync: nothing to export -- no memory at $host_mem" >&2
|
||||
echo " (is the project key right? run: $0 status)" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$repo_mem"
|
||||
# Copy rather than mirror: deleting from the repo is a git operation and
|
||||
# should be visible in a diff, not a side effect of running a sync script.
|
||||
cp "$host_mem"/*.md "$repo_mem"/ 2>/dev/null || true
|
||||
echo "exported $(count "$repo_mem") memories to .claude/memory/"
|
||||
echo "commit them to carry them to your other machines."
|
||||
;;
|
||||
|
||||
import)
|
||||
if [ ! -d "$repo_mem" ]; then
|
||||
echo "memory-sync: nothing to import -- no .claude/memory/ in this repo" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$host_mem"
|
||||
cp "$repo_mem"/*.md "$host_mem"/ 2>/dev/null || true
|
||||
echo "imported $(count "$host_mem") memories to:"
|
||||
echo " $host_mem"
|
||||
;;
|
||||
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
91
tools/posix/mkiso.sh
Normal file
91
tools/posix/mkiso.sh
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/bin/sh
|
||||
# Build a bootable SchOS ISO.
|
||||
#
|
||||
# tools/posix/mkiso.sh [output.iso]
|
||||
#
|
||||
# The disc boots through El Torito's "no emulation" mode, which is the only
|
||||
# one of the three that fits how SchOS actually loads itself.
|
||||
#
|
||||
# floppy emulation caps the image at 2.88 MB; the ramdisk-root kernel
|
||||
# alone is bigger than that.
|
||||
# hard disk emulation presents the image through the BIOS's INT 13h, and
|
||||
# SchOS's loader does not use INT 13h -- it drives
|
||||
# the IDE controller's registers directly, which
|
||||
# cannot reach a BIOS-emulated drive.
|
||||
# no emulation the BIOS loads a chosen run of sectors at 0x7C00
|
||||
# and gets out of the way. That is this.
|
||||
#
|
||||
# So the layout below is deliberate: -boot-load-size is 1 + STAGE2_SECTORS,
|
||||
# which makes the BIOS place both boot/cdboot and the whole of stage2 in memory
|
||||
# before anything of ours runs. That puts stage2 at 0x7E00 -- byte for byte the
|
||||
# arrangement bootasm.S produces on a hard disk. From there the only difference
|
||||
# is that stage2main.c reads the kernel back with ATAPI instead of ATA.
|
||||
#
|
||||
# STAGE2_SECTORS comes from the environment, set by the Makefile, because this
|
||||
# same number also lives in boot/bootasm.S, boot/stage2main.c and the dd offsets
|
||||
# in schos.img's recipe. It was previously written out by hand in three
|
||||
# separate spots in this file alone, and raising it for the boot logo is what
|
||||
# turned that from a latent hazard into a broken image.
|
||||
#
|
||||
# NOT using -boot-info-table on purpose: it patches 56 bytes over offset 8 of
|
||||
# the boot image, which is the middle of cdboot's code. stage2main.c finds
|
||||
# where it was loaded from by reading the El Torito boot catalog instead.
|
||||
|
||||
set -e
|
||||
|
||||
BUILD=${BUILD:-build}
|
||||
OUT=${1:-schos.iso}
|
||||
STAGE2_SECTORS=${STAGE2_SECTORS:-48}
|
||||
STAGE2_BYTES=$((STAGE2_SECTORS * 512))
|
||||
LOAD_SIZE=$((1 + STAGE2_SECTORS))
|
||||
STAGING="$BUILD/iso"
|
||||
|
||||
CDBOOT="$BUILD/boot/cdboot"
|
||||
STAGE2="$BUILD/boot/stage2"
|
||||
KERNEL="$BUILD/kernel/kernel"
|
||||
|
||||
for f in "$CDBOOT" "$STAGE2" "$KERNEL"; do
|
||||
[ -f "$f" ] || { echo "mkiso: missing $f -- run make first" >&2; exit 1; }
|
||||
done
|
||||
|
||||
# The three pieces must be contiguous and at these exact sizes: stage2main.c's
|
||||
# readseg() finds the kernel at sector (1 + STAGE2_SECTORS) of the boot image,
|
||||
# so a short stage2 would silently shift the kernel out from under it.
|
||||
cdboot_sz=$(wc -c < "$CDBOOT")
|
||||
stage2_sz=$(wc -c < "$STAGE2")
|
||||
[ "$cdboot_sz" -eq 512 ] || { echo "mkiso: cdboot is $cdboot_sz bytes, expected 512" >&2; exit 1; }
|
||||
[ "$stage2_sz" -eq "$STAGE2_BYTES" ] || { echo "mkiso: stage2 is $stage2_sz bytes, expected $STAGE2_BYTES (STAGE2_SECTORS=$STAGE2_SECTORS)" >&2; exit 1; }
|
||||
|
||||
rm -rf "$STAGING"
|
||||
mkdir -p "$STAGING/boot"
|
||||
|
||||
cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin"
|
||||
|
||||
# xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags
|
||||
# below unchanged -- xorriso's -as mkisofs mode exists precisely to be
|
||||
# argument-compatible with it. The fallback is not academic: there is no xorriso
|
||||
# package for Windows in scoop, where this is developed, but cdrtools provides
|
||||
# mkisofs.exe.
|
||||
if command -v xorriso >/dev/null 2>&1; then
|
||||
MKISOFS="xorriso -as mkisofs"
|
||||
elif command -v mkisofs >/dev/null 2>&1; then
|
||||
MKISOFS="mkisofs"
|
||||
else
|
||||
echo "mkiso: need xorriso or mkisofs on PATH" >&2
|
||||
echo " Windows: scoop install cdrtools" >&2
|
||||
echo " Debian/Ubuntu: apt install xorriso" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$MKISOFS \
|
||||
-quiet \
|
||||
-V SCHOS \
|
||||
-A "SchOS" \
|
||||
-b boot/schos.bin \
|
||||
-no-emul-boot \
|
||||
-boot-load-size "$LOAD_SIZE" \
|
||||
-o "$OUT" \
|
||||
"$STAGING"
|
||||
|
||||
echo "==> $OUT ($(wc -c < "$OUT") bytes)"
|
||||
echo " qemu-system-x86_64 -cdrom $OUT -boot d"
|
||||
38
tools/posix/mktree.sh
Normal file
38
tools/posix/mktree.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/sh
|
||||
# Build the synthetic test tree used by schfs_roundtrip.sh into $1.
|
||||
# Kept separate so the tree can be recreated for debugging without running
|
||||
# the whole round-trip.
|
||||
set -e
|
||||
TREE=${1:?usage: mktree.sh <dir>}
|
||||
rm -rf "$TREE"
|
||||
mkdir -p "$TREE/bin" "$TREE/etc/conf.d" "$TREE/empty-dir" "$TREE/a/b/c/d/e/f/g"
|
||||
|
||||
: > "$TREE/etc/empty-file"
|
||||
echo "hello schfs" > "$TREE/etc/motd"
|
||||
printf 'x' > "$TREE/etc/one-byte"
|
||||
|
||||
dd if=/dev/zero bs=4096 count=1 2>/dev/null | tr '\0' 'A' > "$TREE/etc/exactly-4096"
|
||||
cp "$TREE/etc/exactly-4096" "$TREE/etc/block-plus-one"
|
||||
printf 'B' >> "$TREE/etc/block-plus-one"
|
||||
|
||||
dd if=/dev/zero bs=1000 count=37 2>/dev/null | tr '\0' 'C' > "$TREE/bin/multiblock"
|
||||
cp "$TREE/bin/multiblock" "$TREE/bin/multiblock-twin"
|
||||
dd if=/dev/zero bs=1000 count=37 2>/dev/null | tr '\0' 'D' > "$TREE/bin/decoy"
|
||||
|
||||
ln -s ../etc/motd "$TREE/bin/motd-link"
|
||||
ln -s /nowhere "$TREE/bin/dangling-link"
|
||||
|
||||
echo deep > "$TREE/a/b/c/d/e/f/g/deep.txt"
|
||||
echo one > "$TREE/etc/conf.d/one"
|
||||
|
||||
echo spaced > "$TREE/etc/a file with spaces"
|
||||
echo dotted > "$TREE/etc/.hidden"
|
||||
LONG=$(printf 'n%.0s' $(seq 1 255))
|
||||
echo long > "$TREE/etc/$LONG"
|
||||
|
||||
mkdir -p "$TREE/many"
|
||||
i=0
|
||||
while [ $i -lt 300 ]; do
|
||||
echo "file $i" > "$TREE/many/f$i"
|
||||
i=$((i + 1))
|
||||
done
|
||||
136
tools/posix/mutate.sh
Normal file
136
tools/posix/mutate.sh
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#!/bin/sh
|
||||
# Mutation-test the SchFS suites: inject a bug, confirm the tests report it,
|
||||
# restore. A test that cannot fail is worse than no test, because it is
|
||||
# believed. Run by hand; not part of the normal build.
|
||||
#
|
||||
# Every mutation is verified to have actually changed the file before the
|
||||
# suite runs. A sed pattern that silently stops matching -- after a refactor,
|
||||
# say -- would otherwise look exactly like a passing mutation test, which is
|
||||
# the same failure this script exists to catch.
|
||||
set -e
|
||||
cd /mnt/d/DEV/SchOS
|
||||
|
||||
OBJ=kernel/fs/schfs/obj.c
|
||||
OBJW=kernel/fs/schfs/objw.c
|
||||
SPACE=kernel/fs/schfs/space.c
|
||||
MK=tools/mkschfs.c
|
||||
cp $OBJ /tmp/obj.orig
|
||||
cp $OBJW /tmp/objw.orig
|
||||
cp $SPACE /tmp/space.orig
|
||||
cp $MK /tmp/mk.orig
|
||||
|
||||
restore() {
|
||||
cp /tmp/obj.orig $OBJ
|
||||
cp /tmp/objw.orig $OBJW
|
||||
cp /tmp/space.orig $SPACE
|
||||
cp /tmp/mk.orig $MK
|
||||
}
|
||||
trap restore EXIT
|
||||
|
||||
FAILED=0
|
||||
|
||||
# try <description> <file> <sed-expression> [suite]
|
||||
#
|
||||
# suite is "image" (the default: build an image and diff it against the source
|
||||
# tree) or "obj" (the in-memory object-layer test). They catch different things:
|
||||
# the image round-trip cannot see a bug that only shows up after a delete,
|
||||
# because it never deletes anything.
|
||||
try() {
|
||||
desc=$1; file=$2; expr=$3; suite=${4:-image}
|
||||
restore
|
||||
before=$(cksum < "$file")
|
||||
sed -i "$expr" "$file"
|
||||
after=$(cksum < "$file")
|
||||
|
||||
echo "mutation: $desc"
|
||||
if [ "$before" = "$after" ]; then
|
||||
echo " BROKEN MUTATION -- the pattern no longer matches anything"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$suite" = obj ]; then
|
||||
if ! make build/schfs_objtest > /tmp/mk.log 2>&1; then
|
||||
echo " rejected at compile time"
|
||||
return
|
||||
fi
|
||||
if ./build/schfs_objtest > /tmp/rt.log 2>&1; then
|
||||
echo " NOT CAUGHT -- the suite passed with this bug present"
|
||||
FAILED=1
|
||||
else
|
||||
echo " caught: $(grep -aE 'FAIL' /tmp/rt.log | head -1 | sed 's/^ *//')"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if ! make build/mkschfs build/schfs_fsck > /tmp/mk.log 2>&1; then
|
||||
echo " rejected at compile time"
|
||||
return
|
||||
fi
|
||||
if sh tools/schfs_roundtrip.sh ./build/mkschfs ./build/schfs_fsck \
|
||||
> /tmp/rt.log 2>&1; then
|
||||
echo " NOT CAUGHT -- the suite passed with this bug present"
|
||||
FAILED=1
|
||||
else
|
||||
echo " caught: $(grep -aE 'FAIL|Only in|differ|loop' /tmp/rt.log | head -1 | sed 's/^ *//')"
|
||||
fi
|
||||
}
|
||||
|
||||
# Reads run past end-of-file, so every file comes back padded.
|
||||
try "file_read ignores the end-of-file clamp" $OBJ \
|
||||
's|if (off + n > in.size)|if (0)|'
|
||||
|
||||
# The dirent walk skips forward too far, losing entries in any packed list.
|
||||
try "dirent_walk advances past the next entry" $OBJ \
|
||||
's| off += step;| off += step + 8;|'
|
||||
|
||||
# Directory lookup returns the neighbouring object.
|
||||
try "dir_lookup returns the wrong objectid" $OBJ \
|
||||
's| f->ino = d->objectid;| f->ino = d->objectid + 1;|'
|
||||
|
||||
# Every extent points one block past its data.
|
||||
#
|
||||
# Note this replaced an earlier mutation that shortened the extent by a block.
|
||||
# That one was NOT caught, and correctly so: an extent covering too little just
|
||||
# leaves the tail unmapped, and schfs_file_append then allocates it separately,
|
||||
# so the file still reads back byte for byte. The design absorbs it. Pointing
|
||||
# at the wrong block cannot be absorbed.
|
||||
try "extents point one block past their data" $OBJW \
|
||||
's| e.disk_block = first;| e.disk_block = first + 1;|'
|
||||
|
||||
# Copy-on-write splits an extent into head, replacement and tail. Losing the
|
||||
# tail leaves everything after the rewritten block unmapped, so it silently
|
||||
# reads back as zeros -- data loss that looks exactly like a sparse file.
|
||||
try "an overwrite drops the tail of the extent it split" $OBJW \
|
||||
's| if (fblk + 1 < start + e.nblocks) {| if (0) {|' obj
|
||||
|
||||
# Truncating down and growing again must read zeros. Skipping the zeroing of
|
||||
# the partial last block hands back whatever the file used to hold there --
|
||||
# which, across a delete and a reallocation, is somebody else's data.
|
||||
try "truncate leaves stale bytes in the last partial block" $OBJW \
|
||||
's| memset((char \*)dblk + tail, 0, SCHFS_BLKSIZE - tail);| /* mutated */;|' obj
|
||||
|
||||
# Deferred frees never rejoin the free map, so every delete leaks. Nothing that
|
||||
# only reads or only writes can see this; it takes a test that deletes
|
||||
# everything and then checks the free count.
|
||||
try "committed frees are never returned to the free map" $SPACE \
|
||||
's| inuse\[w\] &= ~pend\[w\];| /* mutated */;|' obj
|
||||
|
||||
# Hard links are not counted, so dedup is indistinguishable from duplication
|
||||
# and only the explicit nlink assertion can tell the difference.
|
||||
try "hard links do not bump nlink" $MK \
|
||||
's|^ in.nlink++;| /* mutated */;|'
|
||||
|
||||
# Data checksums cover only the head of a block, so tail corruption is missed.
|
||||
try "data checksum covers only the first 16 bytes" $OBJW \
|
||||
's|uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);|uint32_t c = schfs_crc32c(0, data, 16);|'
|
||||
|
||||
restore
|
||||
make build/mkschfs build/schfs_fsck > /dev/null 2>&1
|
||||
echo
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo "all mutations were caught"
|
||||
else
|
||||
echo "SOME MUTATIONS WERE NOT CAUGHT -- the suite has a blind spot"
|
||||
exit 1
|
||||
fi
|
||||
141
tools/posix/schfs_roundtrip.sh
Normal file
141
tools/posix/schfs_roundtrip.sh
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
#!/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
|
||||
|
||||
# Symlinks are load-bearing here, so a host that cannot create them cannot run
|
||||
# this test at all.
|
||||
#
|
||||
# Windows is that host. Git Bash needs developer mode or MSYS=winsymlinks to
|
||||
# make one, and even where it can, mkschfs's mingw build cannot *see* one --
|
||||
# lstat degrades to stat and S_ISLNK is always false, by design and with a
|
||||
# comment saying so. The round-trip would therefore be comparing a tree with no
|
||||
# symlinks against an image with no symlinks and calling that a pass, which is
|
||||
# worse than not running: it is coverage that reports itself as present and is
|
||||
# not.
|
||||
#
|
||||
# Skipping with exit 0 so `make schfs-check` still means something on Windows
|
||||
# -- the B-tree, object and space suites are all host-independent and do run.
|
||||
if ! ln -s target "$WORK/.symlink-probe" 2>/dev/null; then
|
||||
echo "SchFS round-trip: SKIPPED -- this host cannot create symlinks."
|
||||
echo " The generated tree needs them, and mkschfs cannot see them here"
|
||||
echo " anyway. Run this on Linux, macOS or BSD for real coverage."
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$WORK/.symlink-probe"
|
||||
|
||||
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"
|
||||
62
tools/posix/sha256check.sh
Normal file
62
tools/posix/sha256check.sh
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#!/bin/sh
|
||||
# Verify a file's SHA-256 against an expected hex digest, on any host.
|
||||
#
|
||||
# tools/sha256check.sh <expected-hex> <file>
|
||||
#
|
||||
# There is no one checksum command across the supported hosts. Linux and Git
|
||||
# Bash have coreutils' sha256sum; macOS ships shasum(1) and no sha256sum;
|
||||
# FreeBSD and NetBSD ship sha256(1) with a different output format again;
|
||||
# OpenBSD has sha256 too but usually reached through cksum -a sha256. A recipe
|
||||
# that hardcodes any one of them builds on some of the hosts this project
|
||||
# supports and fails on the rest, and the failure arrives only when someone
|
||||
# first runs `make ports` on a Mac.
|
||||
#
|
||||
# Compares digests rather than trusting a -c flag, because the -c input format
|
||||
# differs between the implementations as well.
|
||||
|
||||
set -eu
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "usage: $0 <expected-sha256> <file>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
expected="$1"
|
||||
file="$2"
|
||||
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "sha256check: no such file: $file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actual=
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
actual=$(shasum -a 256 "$file" | cut -d' ' -f1)
|
||||
elif command -v sha256 >/dev/null 2>&1; then
|
||||
# FreeBSD/NetBSD: -q prints the digest alone.
|
||||
actual=$(sha256 -q "$file")
|
||||
elif command -v cksum >/dev/null 2>&1 && cksum -a sha256 /dev/null >/dev/null 2>&1; then
|
||||
# OpenBSD.
|
||||
actual=$(cksum -a sha256 "$file" | sed 's/.*= *//')
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
actual=$(openssl dgst -sha256 "$file" | sed 's/.*= *//')
|
||||
else
|
||||
echo "sha256check: no usable checksum tool found" >&2
|
||||
echo " looked for: sha256sum, shasum, sha256, cksum, openssl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Case-insensitive: openssl and sha256 have been known to differ here.
|
||||
expected_lc=$(printf '%s' "$expected" | tr 'A-Z' 'a-z')
|
||||
actual_lc=$(printf '%s' "$actual" | tr 'A-Z' 'a-z')
|
||||
|
||||
if [ "$expected_lc" != "$actual_lc" ]; then
|
||||
echo "sha256check: checksum mismatch for $file" >&2
|
||||
echo " expected: $expected_lc" >&2
|
||||
echo " actual: $actual_lc" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$file: OK"
|
||||
Loading…
Reference in a new issue