D2: the machine walks a path

sbfsFind takes a path where it used to take a name: names with '/' between
them, walked from the root, with '.' and '..'. Each name is looked for among
the entries whose parent is where the walk has got to. A bare name is a path of
one name, so everything written before directories existed still works and
still costs one walk of the directory.

sbfsMount takes either version. On a version one disk every entry has zeroes
where a parent goes and the walk starts at zero, so the comparison always
agrees - which is how a flat disk reads correctly here with nothing done to it.

PROGRAMS DID NOT HAVE TO BE TAUGHT ANY OF THIS. Resolution sits inside
sbfsFind, below the services, so every osFile call keeps its signature and a
path is simply a longer name. Type, More, Edit and the assembler gained
subdirectories without a line changing in any of them.

Four things this turned up, none of which was the path walk:

load copied the path into a buffer sized for a NAME, so anything over 22
characters was cut short - and cut short into a path that often still resolved.
"/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/" and reported
that the program was a directory. That is the whole of what looked like a bug
in '..', and it cost most of the time here.

load on a directory SUCCEEDED. A directory has no blocks, so reading it reads
nothing and leaves the staging area holding whatever was staged last - which,
if that was a program, still says SBEX and still has a working entry address.
It handed back the program before it. Refused outright now.

delete and rename on a directory are refused, and save refuses one up front
rather than failing at the rename and leaving a temporary behind. Deleting a
directory frees an entry index, and a parent IS an index, so the next file
created would take it and inherit the children.

create writes the parent rather than leaving it zero by luck. It would be zero
- delete wipes all thirty two bytes and a fresh entry never had any - but that
is a fact about two other routines, and a file appearing inside a directory it
was never put in is not a failure anybody would think to look for.

dir marks directories and counts them apart from files, because at this point
it was calling them files of no bytes.

Two hazards written down in the design note turned out not to be real, and both
were checked rather than argued about:

The lookup cache holding 22 bytes of a longer path cannot hand back the wrong
file - textSame wants both strings to end in the same place, so a cut down
entry misses. It can never HIT either, though, so every path longer than a name
went to the disk every time; it holds a whole path now.

The allocator stepping over directories changes nothing any test can see. A
directory has no start as well as no blocks, so its bounds are nought to nought
and no candidate begins before it ends. The four instructions stay, with a
comment saying they are not load bearing today and why they are there anyway.

makedisks.sh resolves its build path before it cds. Given a relative one it
carried on and quietly built disks missing some of their files, which is how
the tree fixture lost a file and sent me looking for a bug in '..'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-24 19:08:26 -04:00
co-authored by Claude Opus 5
parent 78e9eef472
commit 36a1b07b5b
10 changed files with 891 additions and 76 deletions
+2 -2
View File
@@ -3,11 +3,11 @@ CosmOS
> two stops, and what the registers were at each
break at 200E
A 11 B 22 Q 00 status 00
DP0 1030 DP1 06C2 DP2 0000 DP3 2000 SP FFFF
DP0 1030 DP1 0730 DP2 0000 DP3 2000 SP FFFF
press a key
break at 2023
A 44 B 55 Q 00 status 00
DP0 1000 DP1 06C2 DP2 0000 DP3 2000 SP FFF5
DP0 1000 DP1 0730 DP2 0000 DP3 2000 SP FFF5
press a key
carried on to the end
finished
+35
View File
@@ -0,0 +1,35 @@
CosmOS
> Apps <dir>
Deep <dir>
Say.sbx 155
Say.sbx 52
rooted.txt 21
3 files, 2 directories
> loaded, starting at 2000
> it says: the shallow one
finished
> loaded, starting at 2000
> Hello, World!
finished
> loaded, starting at 2000
> it says: the shallow one again
finished
> loaded, starting at 2000
> Hello, World!
finished
> no such file
> no such file
> that is a directory
> no such file
> no such file
> no such file
> that is a directory
> gone
> Apps <dir>
Deep <dir>
Say.sbx 155
rooted.txt 21
2 files, 2 directories
> halted
Execution halted.
[exit 0]
+17
View File
@@ -0,0 +1,17 @@
CosmOS
> Folder <dir>
Inner <dir>
Edit.sbx 1996
1 file, 2 directories
> loaded, starting at 2000
> note.txt, 0 lines
> : : : > written, 67 bytes
> finished
> Folder <dir>
Inner <dir>
Edit.sbx 1996
note.txt 67
2 files, 2 directories
> halted
Execution halted.
[exit 0]
+19
View File
@@ -0,0 +1,19 @@
dir
load /Apps/Say.sbx
run the shallow one
load /Apps/Deep/Say.sbx
run the deep one
load /Apps/./Deep/../Say.sbx
run the shallow one again
load /Apps/Deep/../../Apps/./Deep/Say.sbx
run the deep one the long way round
load /rooted.txt/Say.sbx
load /Apps/Missing/Say.sbx
load /Apps
load /Apps/abcdefghijklmnopqrstuvw
load /
load /Apps/Deep/../..
delete /Apps
delete /Apps/Deep/Say.sbx
dir
exit
+11
View File
@@ -0,0 +1,11 @@
dir
load Edit.sbx
run note.txt
a
a line written onto a disk with directories on it
and a second one
.
w
q
dir
exit
+43 -1
View File
@@ -9,7 +9,13 @@
# Written by Anachronaut
set -eu
BUILD="$1"
# Made absolute before anything else. This script cds into its own working directory part
# of the way down, so a relative build path would be measured from the wrong place from
# there on - and the way that failed was not an error but a disk quietly missing some of
# the files it was supposed to have, which is a much worse thing to debug.
mkdir -p "$1"
BUILD="$(cd "$1" && pwd)"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TOOL="$ROOT/SplitDisk"
DISKS="$BUILD/disks"
@@ -135,6 +141,17 @@ printf 'the second one' > two.txt
"$ROOT/Programs/CosmOS/Apps/Edit.asm" -o "$WORK/Edit.sbx" >/dev/null
"$TOOL" put "$DISKS/editor.img" "$WORK/Edit.sbx" >/dev/null
# A disk that has directories on it AND gets written to, which nothing else covers: every
# other version two fixture is read only, and every fixture that is written to is flat.
# Making a file runs the allocator over a directory entry, and a directory has no blocks,
# so it must be in nobody's way while a run of free ones is looked for. Its own disk, for
# the same reason the editor has one: what it writes would otherwise turn up in the
# listing of every test that came after it.
"$TOOL" format "$DISKS/treewrite.img" 256 2 >/dev/null
"$TOOL" mkdir "$DISKS/treewrite.img" /Folder >/dev/null
"$TOOL" mkdir "$DISKS/treewrite.img" /Folder/Inner >/dev/null
"$TOOL" put "$DISKS/treewrite.img" "$WORK/Edit.sbx" >/dev/null
# A disk for the file services, holding nothing but the program that exercises them. Its
# own, because that program writes: it tidies up after itself, but a run that stopped part
# way would leave a document behind on a fixture every later test reads.
@@ -177,6 +194,31 @@ awk 'BEGIN { for (i = 0; i < 30; i++) printf "line %02d: ABCDEFGHIJKLMNOPQRSTUVW
"$ROOT/Programs/CosmOS/Apps/More.asm" -o "$WORK/More.sbx" >/dev/null
"$TOOL" put "$DISKS/type.img" "$WORK/More.sbx" >/dev/null
# A disk with directories on it, which is the whole of what version two adds. Built by the
# host tool, because at this rung the machine can read a tree and not yet make one - and
# that split is the point: the two implementations are checked against each other rather
# than each against itself.
#
# ONE NAME IS PUT DOWN TWICE, in two directories, because a name meaning something
# different in each place is what directories are for and what a flat filesystem cannot do.
#
# The two are DIFFERENT PROGRAMS on purpose. Say echoes whatever it is given and hello
# prints one fixed line, so which of them ran is written in the output. Two copies of one
# program would have been the easier fixture and a useless one: resolving to the wrong
# Say.sbx would have printed exactly what resolving to the right one prints, and the test
# would have passed while the path walk was ignoring directories entirely.
"$TOOL" format "$DISKS/tree.img" 256 4 >/dev/null
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/hello.asm" -o "$WORK/treeHello.sbx" >/dev/null
"$TOOL" mkdir "$DISKS/tree.img" /Apps >/dev/null
"$TOOL" mkdir "$DISKS/tree.img" /Apps/Deep >/dev/null
"$TOOL" put "$DISKS/tree.img" "$WORK/Say.sbx" /Apps/Say.sbx >/dev/null
"$TOOL" put "$DISKS/tree.img" "$WORK/treeHello.sbx" /Apps/Deep/Say.sbx >/dev/null
printf 'this is not a program' > rooted.txt
"$TOOL" put "$DISKS/tree.img" rooted.txt >/dev/null
# A disk for invoking a program by typing its name. Four files, each there to say one
# thing about how a typed word turns into a file name.
#
+19
View File
@@ -324,6 +324,25 @@ cosmosType | CosmOS/Source/cosmos.asm | run | cosmosTyp
# with Space, and one line with Return. The input is intentionally packed so that the one
# byte the pager consumes leaves the next shell command immediately behind it.
cosmosMore | CosmOS/Source/cosmos.asm | run | cosmosMore.in | - | disks/type.img
# Reading a disk that has directories on it. The machine can walk a path at this point but
# cannot make a directory, so the disk is built by the host tool and read here - which is
# the two implementations checking each other rather than either checking itself.
#
# The two Say.sbx are DIFFERENT PROGRAMS under one name - Say echoes its argument, hello
# prints one fixed line - so which one a path reached is written in the output. Two copies
# of the same program would have passed this test with the parent comparison removed
# altogether, which is the one thing it exists to check.
# Then the ways a path does not resolve: through a file, into a directory that is not
# there, onto a directory rather than a program, a component too long to be a name, and
# the root itself. Last, that delete refuses a directory - the refusal that keeps a freed
# entry index from being handed out with children still pointing at it.
cosmosTree | CosmOS/Source/cosmos.asm | run | cosmosTree.in | - | disks/tree.img
# Writing a file onto a disk that has directories on it. The document goes in the root,
# because nothing on the machine can put one anywhere else yet, and the point is that it
# gets there at all: making a file walks the allocator over two directory entries, which
# 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
# Typing a program's name starts it. The disk is built so that each line of the input asks
# a different question of the one rule: "Say hello there" is a bare name with an argument,
# "Say.sbx" is the same file spelled out in full, and "run once more" says the program that