osFileStart, osFileWrite and osFileDone are the mirror of osFileInfo and osFileBlock. A program can now write a file it never holds: Pour writes twelve blocks and a tail while keeping 256 bytes of it at a time, and the host tool reads all 3,112 bytes back with every block where it was put. ONE WRITE IS OPEN AT A TIME AND COSMOS HOLDS IT. Reading needs no state - a name and an index are the whole question - but writing safely does, because the new file has to exist before the old one is thrown away and something has to remember which temporary belongs to which name. Keeping that here means the careful order is written once instead of in every program that streams. Nothing already on the disk is touched until osFileDone, so a disk without room says so while the old file is still there. That is stronger than osFileSave can manage, where the size is only known once the caller has every byte in hand. osFileSave stays: Edit and Files hand over whole documents and have no reason to learn any of this. osFileWrite refuses an index past the end of the file, and that refusal is not politeness. Files are contiguous, so block nine of a three block file is a real block belonging to something else, and writing it would put one file's bytes inside another with nothing anywhere saying so. Checked both ways: the tail block is allowed and the one past it is not. Three bugs, all of them the same shape - a register or pointer used for two things at once: DP3 carried the block count in and was popped high byte first, which is the wrong way round from every reader in the system and made the count two hundred and fifty six times too big. sbfsStreamStart took the name in DP0 and then wanted DP0 for something else before it had read it, so it walked whatever it last pointed at and reported that it could find no room. sbfsStreamWrite kept the caller's block in DP3 across a find - DP3 being the pointer a return does not put back, which is exactly why the find uses it too. What went to the disk was whatever the scan last looked at. It goes in memory now, and the file is correct because every block says which block it is; a check on the length alone would have passed all three of these. Writing no longer finds the file for each block either. Nothing moves a file once it is made, so where it starts is settled when the temporary is created. That was not even slow - a scan stops the moment it matches - but it was a walk of the directory per block for an answer that cannot change, and it is 28 per cent of the cost of writing forty blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
174 lines
7.3 KiB
Bash
Executable File
174 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Checks the two implementations of SBFS against each other, on the same disk.
|
|
#
|
|
# SplitDisk and sbfs.asm are two programs written from one specification and sharing no
|
|
# code at all - one is C on the host, the other is SplitBit assembly running on the
|
|
# machine. disk.sh checks the host half against the format and run.sh checks the machine
|
|
# half against recorded output, but neither of those can catch the two of them agreeing
|
|
# with themselves and disagreeing with each other.
|
|
#
|
|
# SO THIS BUILDS THE SAME DISK TWICE, once with each, and compares the images byte for
|
|
# byte. Every field either one writes and the other only reads is checked here and nowhere
|
|
# else: which entry a thing lands in, which block, what a directory's unused fields hold,
|
|
# the version in the superblock, the free count. A disagreement in any of those is a disk
|
|
# one of them can read and the other cannot, and the way that is usually discovered is
|
|
# somebody's file coming back wrong months later.
|
|
#
|
|
# Written by Anachronaut
|
|
|
|
set -u
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
WORK="$ROOT/Tests/build/agree"
|
|
ASM="$ROOT/Assembler"
|
|
TOOL="$ROOT/SplitDisk"
|
|
EMU="$ROOT/SplitBit"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
FAILED_NAMES=()
|
|
|
|
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
|
|
[ -t 1 ] || { GREEN=""; RESET=""; RED=""; }
|
|
|
|
report() {
|
|
local mark="$1" name="$2" note="${3:-}"
|
|
if [ "$mark" = "ok" ]; then
|
|
PASS=$((PASS + 1)); printf " [%sok %s] %-24s %s\n" "$GREEN" "$RESET" "$name" "$note"
|
|
else
|
|
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
|
printf " [%sFAIL%s] %-24s %s\n" "$RED" "$RESET" "$name" "$note"
|
|
fi
|
|
}
|
|
|
|
for tool in "$ASM" "$TOOL" "$EMU"; do
|
|
[ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; }
|
|
done
|
|
|
|
rm -rf "$WORK"; mkdir -p "$WORK"
|
|
cd "$WORK" || exit 1
|
|
|
|
"$ASM" -I "$ROOT/Programs/CosmOS/Source" "$ROOT/Programs/CosmOS/Source/cosmos.asm" \
|
|
-o cosmos.bin >/dev/null 2>&1 || { echo "CosmOS would not assemble."; exit 1; }
|
|
|
|
echo "Checking the two SBFS implementations against each other."
|
|
|
|
# ---- The same tree, made both ways ----
|
|
#
|
|
# The order matters and is the same on both sides, because both allocate first fit and both
|
|
# take the first free entry: given the same operations in the same order they should reach
|
|
# the same bytes, and any difference is a real one rather than an artefact of the script.
|
|
"$TOOL" format host.img 128 2 >/dev/null
|
|
"$TOOL" mkdir host.img /Apps >/dev/null
|
|
"$TOOL" mkdir host.img /Apps/Deep >/dev/null
|
|
"$TOOL" mkdir host.img /Notes >/dev/null
|
|
|
|
"$TOOL" format machine.img 128 2 >/dev/null
|
|
printf 'mkdir /Apps\nmkdir /Apps/Deep\nmkdir /Notes\nexit\n' \
|
|
| "$EMU" cosmos.bin --fast --disk machine.img >/dev/null 2>&1
|
|
|
|
if cmp -s host.img machine.img; then
|
|
report ok "three directories" "byte for byte"
|
|
else
|
|
report FAIL "three directories" "$(cmp host.img machine.img 2>&1 | head -1)"
|
|
fi
|
|
|
|
# ---- Removing one puts the disk back exactly ----
|
|
#
|
|
# A wiped entry has to be indistinguishable from one that was never used, or a disk that
|
|
# has had something deleted stops matching a fresh one that never did. Both sides zero all
|
|
# thirty two bytes, and this is what says so.
|
|
"$TOOL" rmdir host.img /Apps/Deep >/dev/null
|
|
printf 'rmdir /Apps/Deep\nexit\n' | "$EMU" cosmos.bin --fast --disk machine.img >/dev/null 2>&1
|
|
if cmp -s host.img machine.img; then
|
|
report ok "and removing one" "byte for byte"
|
|
else
|
|
report FAIL "and removing one" "$(cmp host.img machine.img 2>&1 | head -1)"
|
|
fi
|
|
|
|
# ---- A file put down a path ----
|
|
#
|
|
# The machine writes files through the careful order a save has to use - make a temporary,
|
|
# write it, delete the original, rename the temporary - and the host writes the entry once.
|
|
# Two quite different routes to what has to be the same disk.
|
|
# The editor goes on both disks FIRST and by the same route, so that the only thing left
|
|
# differing is how the payload got written. Putting it on one disk before the payload and
|
|
# the other after was enough to move every entry and block after it, and the comparison
|
|
# duly failed on a difference the script had introduced.
|
|
"$ASM" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Edit.asm" -o Edit.sbx >/dev/null 2>&1
|
|
"$TOOL" put host.img Edit.sbx >/dev/null
|
|
"$TOOL" put machine.img Edit.sbx >/dev/null
|
|
|
|
printf 'a file that lives in a directory\n' > payload.txt
|
|
"$TOOL" put host.img payload.txt /Notes/payload.txt >/dev/null
|
|
printf 'load Edit.sbx\nrun /Notes/payload.txt\na\na file that lives in a directory\n.\nw\nq\nexit\n' \
|
|
| "$EMU" cosmos.bin --fast --disk machine.img >/dev/null 2>&1
|
|
|
|
if cmp -s host.img machine.img; then
|
|
report ok "a file down a path" "byte for byte"
|
|
else
|
|
report FAIL "a file down a path" "$(cmp host.img machine.img 2>&1 | head -1)"
|
|
fi
|
|
|
|
# ---- A file written a block at a time ----
|
|
#
|
|
# The machine never holds more than 256 bytes of this file, and the host reads it back
|
|
# whole. Every block is filled with a byte that says which block it is, so a block written
|
|
# into the wrong place of the file is visible rather than merely being the right length -
|
|
# which is the failure this actually had while it was being built, and the one a check on
|
|
# the size alone would have passed.
|
|
#
|
|
# The last block is a part block on purpose. A tail is where every off-by-one in a
|
|
# filesystem hides.
|
|
"$ASM" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Pour.asm" -o Pour.sbx >/dev/null 2>&1
|
|
"$TOOL" format poured.img 256 2 >/dev/null
|
|
"$TOOL" put poured.img Pour.sbx >/dev/null
|
|
printf 'Pour 12\nexit\n' | "$EMU" cosmos.bin --fast --disk poured.img >/dev/null 2>&1
|
|
"$TOOL" get poured.img poured.dat gotPoured.dat >/dev/null 2>&1
|
|
|
|
if [ -f gotPoured.dat ] && [ "$(wc -c < gotPoured.dat | tr -d ' ')" = "3112" ]; then
|
|
report ok "streamed out block by block" "3112 bytes, twelve blocks and a tail"
|
|
else
|
|
report FAIL "streamed out block by block" "wanted 3112 bytes, got $(wc -c < gotPoured.dat 2>/dev/null || echo nothing)"
|
|
fi
|
|
|
|
# Every byte has to name the block it came from, or a block went somewhere else.
|
|
if od -An -v -tu1 gotPoured.dat 2>/dev/null | awk '
|
|
{ for (i = 1; i <= NF; i++) { if ($i != 65 + int(n / 256)) bad = 1; n++ } }
|
|
END { exit (bad ? 1 : 0) }'; then
|
|
report ok "and every block landed" "each byte names its own block"
|
|
else
|
|
report FAIL "and every block landed" "a block is not where it was written"
|
|
fi
|
|
|
|
# ---- And each can read what the other wrote ----
|
|
#
|
|
# Matching bytes and being readable are not the same claim. A field both of them write
|
|
# wrongly in the same way would pass every comparison above.
|
|
"$TOOL" get machine.img /Notes/payload.txt fromMachine.txt >/dev/null 2>&1
|
|
if cmp -s payload.txt fromMachine.txt; then
|
|
report ok "the host reads it back" "$(wc -c < fromMachine.txt | tr -d ' ') bytes"
|
|
else
|
|
report FAIL "the host reads it back" "the file came back different"
|
|
fi
|
|
|
|
"$TOOL" mkdir host.img /Notes/Inner >/dev/null
|
|
printf 'x' > deep.txt
|
|
"$TOOL" put host.img deep.txt /Notes/Inner/deep.txt >/dev/null
|
|
seen=$(printf 'cd /Notes/Inner\ndir\nexit\n' \
|
|
| "$EMU" cosmos.bin --fast --disk host.img 2>&1 | grep -c "deep.txt")
|
|
if [ "$seen" -ge 1 ]; then
|
|
report ok "the machine reads it back" "found it three deep"
|
|
else
|
|
report FAIL "the machine reads it back" "the machine could not see it"
|
|
fi
|
|
|
|
echo
|
|
if [ "$FAIL" -eq 0 ]; then
|
|
echo "All $PASS agreement checks passed."
|
|
exit 0
|
|
fi
|
|
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
|
|
exit 1
|