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
+136 -14
View File
@@ -269,6 +269,8 @@ doDir:
RSTA
SETD.0 DirSeen
STA.0
SETD.0 DirFolders
STA.0
CALL sbfsFirst
BRI dirCheck
@@ -290,6 +292,15 @@ dirCheck:
MVQA
CALL printSpaces
; A directory says so instead of saying a size. It has no blocks, so the arithmetic
; below would call it a file of no bytes - which is a different thing that happens to
; look the same from here.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ dirIsDirectory
; A file's length is its block count times 256 plus its tail, which is the block count
; in the high byte and the tail in the low one. Nothing has to multiply anything.
SETD.0 SbfsFileBlocks
@@ -308,7 +319,35 @@ dirCheck:
CALL newLine
BRI dirStep
dirIsDirectory:
SETD.0 DirFolders
LDA.0
INCA
STA.0
SETD.0 DirectoryText
CALL printString
CALL newLine
BRI dirStep
dirDone:
; Directories were counted alongside the files and now come back out of the total, so
; that "three files" means three files. Saying it any other way makes the number
; disagree with the listing right above it, which is the sort of thing that teaches
; somebody not to trust the listing.
SETD.0 DirFolders
LDA.0
SETD.1 DirTaken
STA.1
SETD.0 DirSeen
LDA.0
SETD.1 DirTaken
LDB.1
CCF
SUB
MVQA
SETD.0 DirSeen
STA.0
SETD.0 DirSeen
LDA.0
CALL printByteDecimal
@@ -323,6 +362,28 @@ dirOne:
SETD.0 FileText
dirCount:
CALL printString
; And how many of them were directories, but only when there were any. A disk with none
; on it should read exactly the way it always did.
SETD.0 DirFolders
LDA.0
BRA dirNoFolders
SETD.0 AndText
CALL printString
SETD.0 DirFolders
LDA.0
CALL printByteDecimal
SETD.0 DirFolders
LDA.0
DECA
BRA dirOneFolder
SETD.0 FoldersText
BRI dirFolderCount
dirOneFolder:
SETD.0 FolderText
dirFolderCount:
CALL printString
dirNoFolders:
CALL newLine
BRI prompt
@@ -370,10 +431,11 @@ widthDone:
; typing what it is called, whatever is inside it. Only load reaches a file by its whole
; name, which is why the monitor can still put any file at all in front of itself.
;
; NameOk is one if there is a name in ProgramName and zero if the word could not be made
; into one. A directory entry holds twenty two characters and four of those are spoken for
; by the extension, so eighteen is as long as a bare name can be. Being refused here reads
; as an unknown command, which is the truth: no file of that name can exist.
; NameOk is one if there is a path in ProgramName and zero if the word could not be made
; into one. What limits it is the buffer, not the format: each NAME along a path is still
; twenty two characters, and the path walker refuses a longer one rather than cutting it
; down. Being refused here reads as an unknown command, which is the truth: nothing this
; shell can reach is called that.
nameProgram:
RSTA
SETD.0 NameOk
@@ -381,7 +443,7 @@ nameProgram:
SETD.0 CommandLine
SETD.1 ProgramName
INIB 0d22 ; How much of the twenty two is left.
INIB 0d59 ; What is left of the buffer, less the four for the extension.
nameCopy:
LDA.0
BRA nameCopied
@@ -390,7 +452,7 @@ nameCopy:
INCD.1
DECB
BNB nameCopy
RET ; Twenty two characters and still going. Not a name.
RET ; Longer than the buffer holds, so it is not a path either.
nameCopied:
; DP1 is on the byte after the word, which is where an extension would go, and B is what
@@ -402,7 +464,7 @@ nameCopied:
; Only a word of four characters or more can already end in ".sbx". Stepping back four to
; look at a shorter one would read whatever happens to sit in front of the buffer.
INIA 0d18
INIA 0d55
CCF
SUB
BRC nameAppend
@@ -468,10 +530,10 @@ doLoad:
LDA.0
BRA loadNothingNamed
; The name exactly as typed. load is how a file is reached by its whole name, so nothing
; The path exactly as typed. load is how a file is reached by its whole name, so nothing
; is added to it and nothing is assumed about what it ends in.
SETD.1 ProgramName
INIB 0d23
INIB 0d64
CALL copyText
CALL loadProgram
@@ -512,6 +574,7 @@ loadNothingNamed:
; 4 it is not a program
; 5 a version of the format this loader does not know
; 6 more vectors than there is room to keep
; 7 it is a directory
;
; Two is the one worth telling apart from the others. It is the only outcome where nothing
; was wrong with the disk or with a file, and so the only one a caller can fairly report as
@@ -530,6 +593,17 @@ loadProgram:
CALL sbfsFind
BNQ loadMissing
; A DIRECTORY IS REFUSED HERE AND NOT LEFT TO THE MAGIC CHECK BELOW. It 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 in it. Loading a directory would quietly hand back the program before it, and
; running it would look like the directory had run.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ loadIsDirectory
SETD.1 0x80 0x00
CALL sbfsRead
BNQ loadUnreadable
@@ -739,6 +813,10 @@ loadWrongVersion:
loadTooManyVectors:
INIA 0d6
SETD.0 TooManyVectors
BRI loadRefuse
loadIsDirectory:
INIA 0d7
SETD.0 IsDirectory
loadRefuse:
SETD.1 LoadMessage
STD.0.1
@@ -774,9 +852,26 @@ doDelete:
deleteWhat:
SETD.0 DeleteWhat
BRI fileComplain
deleteFailed:
; Two refusals arrive here as one. Asking again costs a walk of the directory, which is
; nothing on a path nobody takes twice, and it is the difference between "you typed a
; name that is not there" and "that is a directory, and delete does not take those".
SETD.1 TextRest
LDD.0.1
CALL sbfsFind
BNQ deleteNoSuch
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ deleteIsDirectory
deleteNoSuch:
SETD.0 NoSuchFile
BRI fileComplain
deleteIsDirectory:
SETD.0 IsDirectory
BRI fileComplain
doRename:
SETD.0 DiskReady
@@ -1230,8 +1325,15 @@ fileLookupSearch:
; Remember it. DP0 still names the file: a CALL puts the pointers back, which is the one
; place that convention is a convenience rather than an obstacle.
;
; A WHOLE PATH IS KEPT, not the twenty two bytes a name has. Keeping twenty two 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 rather than matching something else - but
; it can never match either, so every path longer than a name would go to the disk every
; single time and the cache would quietly stop being one.
SETD.1 FileCacheName
CALL sbfsKeepName
INIB 0d64
CALL copyText
SETD.0 FileCacheStart
SETD.2 SbfsFileStart
CALL sbfsSetWord
@@ -2713,6 +2815,16 @@ Unknown:
"I do not know: "
Farewell:
"halted"
DirectoryText:
"<dir>"
IsDirectory:
"that is a directory"
AndText:
", "
FoldersText:
" directories"
FolderText:
" directory"
FilesText:
" files"
FileText:
@@ -2861,6 +2973,10 @@ HelpName:
ExitName:
"exit"
DirFolders:
0x00
DirTaken:
0x00
DiskReady:
0x00
LoadedOk:
@@ -2872,10 +2988,16 @@ LoadStatus:
LoadMessage:
0x00 0x00
; The name of the file to load, which load copies out of the line as typed and a typed
; program name is built into. Twenty two characters and the zero that ends them.
; THE PATH of the file to load, which load copies out of the line as typed and a typed
; program name is built into.
;
; Sixty four rather than the twenty three a NAME needs. It held a name when a disk was
; flat and there was nothing else to hold, and leaving it that size once paths existed cut
; every path longer than twenty two characters down to twenty two - which is not a failure
; that looks like one. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/",
; resolved perfectly well, and reported that the program was a directory.
ProgramName:
#Reserve 0d23
#Reserve 0d64
NameOk:
0x00
NameLeft:
@@ -2924,7 +3046,7 @@ PrintNumber:
FileCacheValid:
0x00
FileCacheName:
#Reserve 0d23
#Reserve 0d64
FileCacheStart:
0x00 0x00
FileCacheBlocks: