D4: the machine makes directories too
mkdir and rmdir are the machine's own now, and a file goes where its path says rather than always in the root. A disk can be organised without the host tool touching it. Everything below the surface works in terms of a directory and a name rather than a path. sbfsWalkParent splits the last name off, walks the rest, and hands back the two - and the separator stays on the end of the head, which is what makes one rule cover every kind of path: "/x" leaves "/", which is the root; "x" leaves nothing, which is where the machine already is; and "A/x" leaves "A/", which is neither and needs no special case to say so. Saving works in those two as well, and had to. The careful order a save uses - make a temporary, write it, delete the original, rename the temporary - only works if the temporary is made in the SAME directory as the file, because the rename at the end changes a name and does not move anything. Renaming to a path naming a different directory is refused for that reason, rather than quietly being a lie the disk goes along with. Three things this cost, all found by running it: mkdir Apps/Deep made /Apps/Apps. The leaf was worked out into SbfsWanted and then the head was walked - and walking goes through sbfsPathNext, which puts every name it meets into SbfsWanted on the way past. The head's last name landed exactly where the leaf was. It has somewhere of its own now. rmdir took a directory with something still in it, which is the one failure the whole design is arranged to prevent. Looking for children clobbered DP2 and rebuilt it from the buffer and the entry count with the subtraction the wrong way round, so the pointer walked off the end of the block and found nothing. The comparison goes through a CALL now, like the two beside it, and DP2 comes back on the entry because a RET puts it there. SplitDisk's "in use but not reachable from the root" line is what caught it. Refusing a name longer than twenty two used to read the twenty third character of a shorter one, which is somebody else's string. It is measured now. Tests/agree.sh is new and is the gate this rung was for: the same disk built twice, once with SplitDisk and once with CosmOS, compared byte for byte. The two share no code and only a written specification, and every field one writes and the other only reads is checked there and nowhere else - which entry a thing lands in, which block, what a directory's unused fields hold, the version, the free count. It caught a wrong parent immediately when that was broken on purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
36ce9f6ccf
commit
da91a36d92
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/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
|
||||
|
||||
# ---- 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
|
||||
@@ -4,6 +4,8 @@ load <file> read a program off the disk
|
||||
run [words] start what was loaded, and tell it those words
|
||||
<name> [words] look where you are and then in /Apps, and start that
|
||||
cd [path] go to a directory, or to the root with nothing after it
|
||||
mkdir <path> make a directory
|
||||
rmdir <path> remove an empty one
|
||||
delete <file> take it off the disk
|
||||
rename <file> <to> call it something else
|
||||
monitor look at memory, change it, and jump into it
|
||||
|
||||
@@ -3,11 +3,11 @@ CosmOS
|
||||
> two stops, and what the registers were at each
|
||||
break at 400E
|
||||
A 11 B 22 Q 00 status 00
|
||||
DP0 2030 DP1 082E DP2 0000 DP3 4000 SP FFFF
|
||||
DP0 2030 DP1 09BC DP2 0000 DP3 4000 SP FFFF
|
||||
press a key
|
||||
break at 4023
|
||||
A 44 B 55 Q 00 status 00
|
||||
DP0 2000 DP1 082E DP2 0000 DP3 4000 SP FFF5
|
||||
DP0 2000 DP1 09BC DP2 0000 DP3 4000 SP FFF5
|
||||
press a key
|
||||
carried on to the end
|
||||
finished
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
CosmOS
|
||||
> 0 files
|
||||
> made
|
||||
> made
|
||||
> made
|
||||
> Apps <dir>
|
||||
Notes <dir>
|
||||
0 files, 2 directories
|
||||
> /Apps> Deep <dir>
|
||||
0 files, 1 directory
|
||||
/Apps> cannot make that: check the path, the name, and whether it is taken
|
||||
/Apps> made
|
||||
/Apps> /Notes> Deep <dir>
|
||||
0 files, 1 directory
|
||||
/Notes> cannot remove that: it must be a directory, and empty
|
||||
/Notes> removed
|
||||
/Notes> that is a directory
|
||||
/Notes> removed
|
||||
/Notes> > cannot make that: check the path, the name, and whether it is taken
|
||||
> cannot remove that: it must be a directory, and empty
|
||||
> cannot remove that: it must be a directory, and empty
|
||||
> removed
|
||||
> removed
|
||||
> 0 files
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -21,6 +21,8 @@ load <file> read a program off the disk
|
||||
run [words] start what was loaded, and tell it those words
|
||||
<name> [words] look where you are and then in /Apps, and start that
|
||||
cd [path] go to a directory, or to the root with nothing after it
|
||||
mkdir <path> make a directory
|
||||
rmdir <path> remove an empty one
|
||||
delete <file> take it off the disk
|
||||
rename <file> <to> call it something else
|
||||
monitor look at memory, change it, and jump into it
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
dir
|
||||
mkdir Apps
|
||||
mkdir Apps/Deep
|
||||
mkdir Notes
|
||||
dir
|
||||
cd Apps
|
||||
dir
|
||||
mkdir Deep
|
||||
mkdir /Notes/Deep
|
||||
cd /Notes
|
||||
dir
|
||||
rmdir /Apps
|
||||
rmdir /Apps/Deep
|
||||
delete /Apps
|
||||
rmdir /Notes/Deep
|
||||
cd /
|
||||
mkdir Apps/Deep/Inner
|
||||
rmdir Apps/Deep/Inner
|
||||
rmdir Apps/Deep
|
||||
rmdir Apps
|
||||
rmdir Notes
|
||||
dir
|
||||
exit
|
||||
@@ -219,6 +219,12 @@ awk 'BEGIN { for (i = 0; i < 30; i++) printf "line %02d: ABCDEFGHIJKLMNOPQRSTUVW
|
||||
printf 'this is not a program' > rooted.txt
|
||||
"$TOOL" put "$DISKS/tree.img" rooted.txt >/dev/null
|
||||
|
||||
# A blank disk for the machine to build a tree on itself. It starts as a version ONE disk
|
||||
# with nothing at all on it, because half of what this checks is that making the first
|
||||
# directory raises the version - the number says what is on a disk rather than what made
|
||||
# it, so a disk with no directories is flat whoever formatted it.
|
||||
"$TOOL" format "$DISKS/build.img" 128 2 >/dev/null
|
||||
|
||||
# A disk for moving about on. Two directories hold a file of THE SAME NAME with different
|
||||
# text in it, which is the fixture the working directory needs: "notes.txt" has to mean a
|
||||
# different file from each of them, and the only way to see that it does is for the two to
|
||||
|
||||
@@ -343,6 +343,21 @@ cosmosTree | CosmOS/Source/cosmos.asm | run | cosmosTre
|
||||
# hold no blocks and must therefore be in nobody's way when a run of free ones is wanted.
|
||||
# The listing afterwards says where it landed and how big it is.
|
||||
cosmosTreeWrite | CosmOS/Source/cosmos.asm | run | cosmosTreeWrite.in | - | disks/treewrite.img
|
||||
# The machine building its own tree. It starts with a blank version one disk and makes
|
||||
# every directory on it, which is the half of the filesystem the machine could only read
|
||||
# until now.
|
||||
#
|
||||
# The refusals are most of the test. rmdir will not take a file and delete will not take a
|
||||
# directory, so neither can be the one that removed more than was asked for; a directory
|
||||
# with anything in it is refused outright, because a parent is an entry INDEX and a freed
|
||||
# index goes to the next thing created - the children would turn up inside whatever took
|
||||
# its place, with nothing pointing downward to find them by. A name already taken in that
|
||||
# directory is refused, and the same name in a different directory is not, which is the
|
||||
# whole point of the exercise.
|
||||
#
|
||||
# The last dir is there to show the disk still adds up: every entry made and unmade, and
|
||||
# nothing left over.
|
||||
cosmosBuild | CosmOS/Source/cosmos.asm | run | cosmosBuild.in | - | disks/build.img
|
||||
# The working directory. cd moves the machine, the prompt says where it is once that is
|
||||
# not the root, and dir lists one directory rather than the whole disk.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user