Files
SplitBit-Emulator/Tests/disk.sh
T
Anachronaut ce0f18f4ef Refuse a directory whose last entries cannot be named as a parent
A parent is an entry index PLUS ONE in two bytes, so entry 65535 has no
parent number: adding one wraps to zero, and zero is the root. Eight
entries to a block, so 8192 directory blocks reaches it and SplitDisk
formatted that happily.

It does not fail by refusing, which is why it was worth chasing rather than
reasoning about. Reproduced on a disk built for it: mkdir /deep/child, with
/deep at entry 65535, printed 'Made "/deep/child" as entry 0' and put child
in the ROOT. Listing /deep then showed nothing, because the search is for a
parent of 65536 and the entry carries zero - so the same mkdir succeeded
again, and again, and five entries called /child piled up in the root.
Duplicate names in one directory are the one thing rename refuses outright,
on the grounds that a search answers with whichever it meets first and the
rest can never be reached; this manufactured them one per attempt.

8191 blocks is the most, giving 65528 entries. Refused when formatting and
again when reading, in both implementations, because a disk claiming more
was made by something that never checked. On the machine only the high byte
of the count has to be looked at: anything from 0x20 up is too many.

Three checks, all of which fail with their guard removed. The machine's
disk claims the size rather than having it, so the test image is 64 blocks
that lie rather than sixteen megabytes that do not - mounting is refused at
the geometry, which is read out of block 0.
2026-08-25 23:47:02 -04:00

182 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Checks SplitDisk against the SBFS format.
#
# The tool and the SplitBit side are two implementations of one written specification,
# and nothing but that document keeps them the same. This checks the host half on its
# own: that a file put onto a disk comes back off it byte for byte, that the sizes which
# exercise the block and tail arithmetic all survive, and that the things the format
# says cannot happen are refused rather than half done.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TOOL="$ROOT/SplitDisk"
WORK="$ROOT/Tests/build/disk"
PASS=0
FAIL=0
FAILED_NAMES=()
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
check() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
else
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "$name"
fi
}
# The opposite: the command is supposed to fail, and passing would be the bug.
refuses() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %s (it was allowed)\n" "$RED" "$RESET" "$name"
else
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
fi
}
if [ ! -x "$TOOL" ]; then
echo "SplitDisk is not built."
exit 1
fi
rm -rf "$WORK"; mkdir -p "$WORK"
cd "$WORK" || exit 1
echo "Checking SplitDisk against the SBFS format."
check "format a disk" "$TOOL" format work.img 64 2
refuses "refuse a disk with no room" "$TOOL" format tiny.img 2 4
refuses "refuse an unformatted disk" "$TOOL" list /dev/null
# The sizes that exercise every corner of blocks-plus-tail: nothing at all, less than a
# block, exactly a block, a part block, and an exact multiple.
: > empty.bin
printf 'x' > one.bin
head -c 256 /dev/urandom > exact.bin
head -c 700 /dev/urandom > part.bin
head -c 768 /dev/urandom > whole.bin
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
check "put $f" "$TOOL" put work.img "$f"
done
roundTrip() {
"$TOOL" get work.img "$1" "got_$1" >/dev/null 2>&1 || return 1
cmp -s "$1" "got_$1"
}
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
check "$f comes back byte for byte" roundTrip "$f"
done
refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin 16bitSegmentedSieveModern.asm
refuses "refuse a duplicate name" "$TOOL" put work.img one.bin
refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin
check "delete" "$TOOL" delete work.img one.bin
refuses "the deleted file is gone" "$TOOL" get work.img one.bin out.bin
check "the name can be used again" "$TOOL" put work.img one.bin
# Contiguous files mean a disk can have room without having room in one piece. That is a
# consequence of the format rather than a bug, so it is checked rather than worked around.
"$TOOL" format frag.img 16 1 >/dev/null 2>&1
head -c 1024 /dev/urandom > a.bin; cp a.bin b.bin; cp a.bin c.bin
"$TOOL" put frag.img a.bin >/dev/null 2>&1
"$TOOL" put frag.img b.bin >/dev/null 2>&1
"$TOOL" put frag.img c.bin >/dev/null 2>&1
"$TOOL" delete frag.img a.bin >/dev/null 2>&1
"$TOOL" delete frag.img c.bin >/dev/null 2>&1
head -c 2048 /dev/urandom > big.bin
refuses "refuse a file with no run long enough" "$TOOL" put frag.img big.bin
head -c 512 /dev/urandom > fits.bin
check "but one that fits the gap goes on" "$TOOL" put frag.img fits.bin
# ---- Directories ----
#
# Version two, which adds a parent to each entry and a flag bit saying an entry is a
# directory. Both come out of bytes the entry had already set aside, so nothing moved and
# a version one disk needs no converting: zero in those bytes means the root, which is
# exactly where every file on a flat disk is.
#
# The version is therefore a statement about what is ON a disk rather than about what made
# it, and these check that it is only raised when it becomes true.
"$TOOL" format tree.img 64 2 >/dev/null 2>&1
printf 'a file in the root' > root.txt
check "a fresh disk is flat" "$TOOL" put tree.img root.txt
version() { "$TOOL" list "$1" 2>/dev/null | head -1 | grep -q "version $2"; }
check "and says it is version 1" version tree.img 1
check "make a directory" "$TOOL" mkdir tree.img /Apps
check "which raises it to version 2" version tree.img 2
check "make one inside it" "$TOOL" mkdir tree.img /Apps/Source
check "put a file down a path" "$TOOL" put tree.img root.txt /Apps/Source/deep.txt
# The point of the whole exercise: a name means something different in each place, so the
# same one can be used twice without either being in the other's way.
check "the same name in two places" "$TOOL" put tree.img root.txt /Apps/root.txt
roundTripAt() {
"$TOOL" get tree.img "$1" got_deep.txt >/dev/null 2>&1 || return 1
cmp -s root.txt got_deep.txt
}
check "it comes back byte for byte" roundTripAt /Apps/Source/deep.txt
check ". and .. walk the path" roundTripAt /Apps/./Source/../root.txt
check ".. from the root is the root" roundTripAt /Apps/../../root.txt
# Each of these is a way the tree could be made to contradict itself, and each is refused
# rather than half done.
refuses "no file where a directory goes" "$TOOL" put tree.img root.txt /root.txt/x.txt
refuses "no putting into thin air" "$TOOL" put tree.img root.txt /Nowhere/x.txt
refuses "no duplicate in one directory" "$TOOL" mkdir tree.img /Apps
refuses "no getting a directory" "$TOOL" get tree.img /Apps out.bin
refuses "delete will not take a directory" "$TOOL" delete tree.img /Apps
refuses "rmdir will not take a file" "$TOOL" rmdir tree.img /root.txt
refuses "nor the root" "$TOOL" rmdir tree.img /
# THE REFUSAL THAT MATTERS MOST. Parents are entry indices and a freed index is handed out
# again, so removing a directory with things still in it would let the next file created
# adopt them. Emptying it first is the only safe order.
refuses "no removing an occupied one" "$TOOL" rmdir tree.img /Apps/Source
check "empty it first" "$TOOL" delete tree.img /Apps/Source/deep.txt
check "then it goes" "$TOOL" rmdir tree.img /Apps/Source
# A path is names with separators between them, and a name is still twenty two characters.
refuses "refuse a 23 character component" "$TOOL" mkdir tree.img /Apps/abcdefghijklmnopqrstuvw
refuses "refuse a path naming nothing" "$TOOL" mkdir tree.img /Apps/
# A directory costs an entry and no blocks at all, which is what keeps the flat array of
# entries the whole allocation map. If a directory ever took a block, this would drop.
blocksFree() { "$TOOL" list "$1" 2>/dev/null | tail -1 | sed 's/.*used, //; s/ blocks free.*//'; }
before=$(blocksFree tree.img)
"$TOOL" mkdir tree.img /Empty >/dev/null 2>&1
check "a directory costs no blocks" [ "$before" = "$(blocksFree tree.img)" ]
# ---- A directory no bigger than the parent field can name ----
#
# Eight entries to a block and the parent is an index plus one in two bytes, so entry
# 65535 has no parent number: adding one wraps to zero, and zero is the root. Such an
# entry does not refuse what is put inside it. It writes the thing into the ROOT while
# reporting the path that was asked for, and then cannot find it again - so the same
# create succeeds over and over, piling up entries of one name in one directory, which is
# the exact corruption rename exists to refuse.
refuses "no directory past the wrap" "$TOOL" format huge.img 65535 8192
check "the largest that fits" "$TOOL" format huge.img 65535 8191
# And a disk claiming one, which is what something that never checked would have written.
# The claim is in the superblock, so it does not need a disk that size to be made.
"$TOOL" format lying.img 64 2 >/dev/null
printf '\x20\x00' | dd of=lying.img bs=1 seek=10 conv=notrunc status=none
refuses "nor reading one that claims it" "$TOOL" list lying.img
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS disk tool checks passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1