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
+38 -4
View File
@@ -17,6 +17,9 @@ for itself.
- Interactive Shell: Read commands from the SplitBit console and continue until `exit` or
the end of input.
- SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk.
- Paths: Anywhere a filename is taken, a path may be given instead - names with `/`
between them, walked from the root, with `.` and `..`. Directories are read but not yet
made; the host tool makes them.
- Loadable Applications: Validate SBEX files, copy their Program and Data segments into
the addresses for which they were assembled, and start them at their declared entry
point.
@@ -83,7 +86,7 @@ CosmOS currently provides these built-in commands:
| Command | Description |
| -- | -- |
| `dir` | List the files on the mounted disk and their sizes. |
| `load <file>` | Read and validate an SBEX application, then place its code and data where its header requests. |
| `load <path>` | Read and validate an SBEX application, then place its code and data where its header requests. |
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
| `<name> [words]` | Any word the shell does not recognise is looked for on the disk as `<name>.sbx`, and loaded and started if it is there. |
| `delete <file>` | Remove a file from the filesystem and release its blocks. |
@@ -130,6 +133,34 @@ facility and as a test that CosmOS correctly restores its Stack and vector table
every run; and `load` is how the monitor puts an arbitrary file in front of itself, which
is a thing typing a name deliberately cannot do.
### Paths:
Everywhere CosmOS takes a filename it will take a path: names with `/` between them,
walked from the root, with `.` meaning where you are and `..` meaning the directory above.
`..` from the root is the root. A bare name is a path of one name, so nothing written
before directories existed had to change.
```text
> load /Apps/Snake.sbx
> Type /Notes/today.txt
```
**Programs did not have to be taught any of this.** Path resolution lives inside
`sbfsFind`, below the services, so `osFileInfo`, `osFileBlock`, `osFileSave`,
`osFileDelete` and `osFileRename` all still take a pointer to a name - and a path is
simply a longer name. `Type`, `More`, `Edit` and the assembler gained subdirectories
without a line being changed in any of them.
Each *name* along a path is still the 22 characters a directory entry holds, and a longer
one is refused rather than cut short, because a name cut to 22 characters is a different
name that might well be some other file's.
At this stage CosmOS **reads** directories and does not make them. There is no `cd`, no
`mkdir`, and no working directory: every path is from the root. Files a program writes go
in the root. Deleting or renaming a directory is refused - deleting one would free its
entry index, and since a parent is written as an index, the next file created would take
that index and inherit its children.
### Starting An Application By Name:
A word the shell has no command for is not immediately an error. Before saying so, the
@@ -150,15 +181,18 @@ trustworthy.
cannot be started by typing what it is called, whatever happens to be inside it. Only
`load` reaches a file by its literal name.
A path works here too, so `/Apps/Say hello` starts `/Apps/Say.sbx` and gives it `hello`.
**A file that is found but is broken says so.** If `notes.sbx` exists and is not an SBEX
program, typing `notes` reports `not a program` rather than `I do not know: notes`.
Reporting an unknown command about a file that is sitting on the disk would send somebody
looking in the wrong place.
Names are matched exactly, including case, because every other name on the filesystem is.
A directory entry holds twenty two characters and four are spoken for by the extension, so
eighteen is the longest a bare name can be; a longer word is reported as unknown, which is
the truth, since no file of that name can exist.
What limits the typed word is the buffer it is built in rather than 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. A word that will not fit is reported as unknown, which is the
truth, since nothing the shell can reach is called that.
CosmOS also boots without a disk. It reports that no filesystem was found, leaves the
shell and memory monitor available, and refuses commands that require a mounted disk
+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:
+571 -55
View File
@@ -60,13 +60,25 @@ sbfsMagicSame:
BNA sbfsMagicLoop
sbfsMagicDone:
; The version has to be one we understand.
; The version has to be one we understand, and there are two of them.
;
; Version one is flat: every file is in the root because there is nowhere else. Version
; two gives each entry a parent, and writes it as the entry's index PLUS ONE - so the
; zeroes a version one disk has in those bytes read as "in the root", which is exactly
; where all of its files are. A version one disk needs nothing done to it to be read
; here, and that is why both numbers are taken rather than one being converted.
SETD.0 SbfsBuffer
DPUP.0 0d04
LDA.0
INIB 0d1
XOR
BRQ sbfsGeometry
SETD.0 SbfsBuffer
DPUP.0 0d04
LDA.0
INIB 0d2
XOR
BRQ sbfsGeometry
RET ; A version we do not know.
sbfsGeometry:
@@ -90,60 +102,155 @@ sbfsGeometry:
ADD ; Q is zero: mounted.
RET
; Finds a file by name. DP0 points at a name, ending in a zero byte. Q is zero if it was
; found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it.
sbfsFind:
SETD.1 SbfsWanted
CALL sbfsKeepName
; ---- Finding something by path ----
;
; DP0 points at a path ending in a zero byte: names with '/' between them, walked from the
; root. Q is zero if it was found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail
; describe it, SbfsFoundFlags says what kind of thing it is, and DP3 is left on the entry
; with SbfsBlock on the directory block it came out of - which is what deleting and
; renaming need in order to change it and put it back.
;
; A BARE NAME IS A PATH OF ONE NAME, so everything written before there were directories
; still works and still costs one walk of the directory.
;
; Each name is looked for among the entries whose parent is where the walk has got to.
; There is no list of children anywhere: being a child is a fact written in the child, so
; finding them means looking at all of them. That is the same walk the flat version did
; with one more thing compared, which is why a flat disk costs what it always did - and on
; a version one disk every entry says "the root", so the comparison is free and true.
; Start at the first directory block and work through them all.
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
sbfsFind:
SETD.1 SbfsPathAt
STD.0.1
; The walk starts at the root, which is zero: a parent is an entry index PLUS ONE, and
; the root is not an entry.
SETD.0 SbfsAt
RSTA
STA.0
INCD.0
STA.0
; Nothing described yet. An empty path comes back as missing, which is right: the root
; is not a file.
RSTA
SETD.0 SbfsFieldsValid
STA.0
sbfsFindName:
CALL sbfsPathNext
SETD.0 SbfsPathState
LDA.0
BRA sbfsFindGotName
INIB 0d1
CCF
SUB
BRQ sbfsFindEnd ; The path ran out.
BRI sbfsFindMissing ; A name longer than a name can be, so nothing is called it.
sbfsFindGotName:
; "." and ".." belong to the filesystem and are answered here rather than looked for.
; Nothing on a disk is ever called either of them, so this takes nothing away.
SETD.0 SbfsWanted
LDA.0
INIB 0x2E
CCF
SUB
BNQ sbfsFindLook
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1 ; Only the low byte: a directory of 256 blocks is 2048 files.
BRA sbfsFindName ; "." on its own is where the walk already is.
INIB 0x2E
CCF
SUB
BNQ sbfsFindLook
INCD.0
LDA.0
BRA sbfsFindUp ; ".." on its own goes back the way it came.
sbfsFindBlock:
CALL sbfsReadBlock
BRQ sbfsFindLoaded
RET ; The read failed.
sbfsFindLoaded:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
; Eight entries to a block, thirty two bytes each.
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsFindEntry:
LDA.2
INIB 0x01
sbfsFindLook:
; Only a directory can be looked inside. Where the walk is now was put there by the last
; scan, so what kind of thing it is has already been read out of its entry.
SETD.0 SbfsAt
LDA.0
INCD.0
LDB.0
OR
BRQ sbfsFindScan ; At the root, which can always be looked inside.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BRQ sbfsFindNext ; The in use bit is down, so this entry is free.
CALL sbfsMatchEntry
BRQ sbfsFindFound
BRQ sbfsFindMissing ; Not a directory, so there is nothing inside it.
sbfsFindNext:
DPUP.2 0d32
LDA.1
DECA
STA.1
BNA sbfsFindEntry
sbfsFindScan:
CALL sbfsScanFor
BNQ sbfsFindMissing
BRI sbfsFindName
sbfsFindNextBlock:
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BRA sbfsFindMissing
BRI sbfsFindBlock
sbfsFindUp:
; ".." from the root is the root. Every filesystem answers this way, and it is the only
; answer that cannot walk off the top of the disk.
SETD.0 SbfsAt
LDA.0
INCD.0
LDB.0
OR
BRQ sbfsFindName
; Where the walk is, as an index rather than as a parent.
SETD.0 SbfsAt
SETD.1 SbfsTarget
CALL sbfsCopyWord
SETD.0 SbfsTarget
CALL sbfsBackWord
CALL sbfsAtIndex
BNQ sbfsFindMissing ; The disk would not read.
SETD.0 SbfsUpParent
SETD.1 SbfsAt
CALL sbfsCopyWord
; What it landed on is a directory, because a parent always is. What describes it, on
; the other hand, is the entry it came FROM - so that is marked as not to be believed,
; and loaded properly if the path stops here.
INIA 0x02
SETD.0 SbfsFoundFlags
STA.0
RSTA
SETD.0 SbfsFieldsValid
STA.0
BRI sbfsFindName
sbfsFindEnd:
; Did the walk get anywhere at all? The root is not a file.
SETD.0 SbfsAt
LDA.0
INCD.0
LDB.0
OR
BRQ sbfsFindMissing
SETD.0 SbfsFieldsValid
LDA.0
BNA sbfsFindGood
; The path ended on a "..", so what describes the entry is one place out of date.
SETD.0 SbfsAt
SETD.1 SbfsTarget
CALL sbfsCopyWord
SETD.0 SbfsTarget
CALL sbfsBackWord
CALL sbfsAtIndex
BNQ sbfsFindMissing
sbfsFindGood:
RSTA
RSTB
CCF
ADD ; Q is zero: found.
RET
sbfsFindMissing:
; Nothing of that name. Q has to be something other than zero to say so.
@@ -153,13 +260,295 @@ sbfsFindMissing:
ADD
RET
sbfsFindFound:
; ---- Taking a path apart ----
;
; Copies the next name out of the path into SbfsWanted, padded with zeroes to twenty two
; the way an entry holds one, and moves SbfsPathAt past it.
;
; SbfsPathState says what happened: zero for a name, one for the end of the path, two for
; a name longer than a name can be. THE LAST IS REFUSED RATHER THAN CUT SHORT, because a
; name cut to twenty two characters is a different name, and might well be some other
; file's.
;
; Separators are skipped rather than counted, so "/Apps/", "Apps" and "//Apps" all walk
; the same way and a trailing one simply ends the path.
sbfsPathNext:
RSTA
SETD.0 SbfsPathState
STA.0
SETD.0 SbfsPathAt
LDD.1.0
sbfsPathSkip:
LDA.1
BRA sbfsPathEnd
INIB 0x2F
CCF
SUB
BNQ sbfsPathName
INCD.1
BRI sbfsPathSkip
sbfsPathEnd:
INIA 0d1
SETD.0 SbfsPathState
STA.0
BRI sbfsPathSave
sbfsPathName:
SETD.2 SbfsWanted
INIA 0d22
SETD.0 SbfsPathLeft
STA.0
sbfsPathCopy:
LDA.1
BRA sbfsPathPad ; The path ended, so the name ended with it.
INIB 0x2F
CCF
SUB
BRQ sbfsPathPad ; A separator ends the name.
SETD.0 SbfsPathLeft
LDA.0
BRA sbfsPathTooLong ; Twenty two already, and there is still more of it.
DECA
STA.0
LDA.1
STA.2
INCD.1
INCD.2
BRI sbfsPathCopy
sbfsPathPad:
; Zeroes out to the twenty two, so that comparing the whole field compares the name.
SETD.0 SbfsPathLeft
LDA.0
BRA sbfsPathSave
sbfsPathPadLoop:
RSTA
STA.2
INCD.2
SETD.0 SbfsPathLeft
LDA.0
DECA
STA.0
BNA sbfsPathPadLoop
sbfsPathSave:
SETD.0 SbfsPathAt
STD.1.0
RET
sbfsPathTooLong:
INIA 0d2
SETD.0 SbfsPathState
STA.0
RET
; ---- Looking in one directory ----
;
; Looks for the name in SbfsWanted among the entries whose parent is SbfsAt. Q is zero if
; it is there, and then the walk moves onto it and everything that describes it is read
; out of its entry. DP3 is left on that entry.
sbfsScanFor:
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1 ; Only the low byte: a directory of 256 blocks is 2048 entries.
; Entries are COUNTED rather than worked out from where they sit. An index becomes a
; block and an offset by dividing by eight, and this machine has no divide; counting
; costs one step per entry through a walk that was happening anyway.
SETD.0 SbfsScanIndex
RSTA
STA.0
INCD.0
STA.0
sbfsScanBlock:
CALL sbfsReadBlock
BRQ sbfsScanLoaded
RET ; The read failed.
sbfsScanLoaded:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
; Eight entries to a block, thirty two bytes each.
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsScanEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsScanNext ; The in use bit is down, so this entry is free.
CALL sbfsMatchParent
BNQ sbfsScanNext ; It lives somewhere else.
CALL sbfsMatchEntry
BRQ sbfsScanFound
sbfsScanNext:
SETD.0 SbfsScanIndex
CALL sbfsStepWord
DPUP.2 0d32
LDA.1
DECA
STA.1
BNA sbfsScanEntry
sbfsScanNextBlock:
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BRA sbfsScanMissing
BRI sbfsScanBlock
sbfsScanMissing:
RSTA
INIB 0d1
CCF
ADD
RET
sbfsScanFound:
; DP2 is on the entry. Keep it in DP3 while the pieces are taken out of it: DP3 is the
; pointer a CALL does not put back, so it is the only one that survives a subroutine.
; Copying it into DP0 has to be done here rather than in a routine of its own, for the
; same reason: a routine that set DP0 would have the assignment undone by its own RET.
PSHD.2
POPD.3
CALL sbfsTakeEntry
; The walk moves onto what it found, written the way a parent is written.
SETD.0 SbfsScanIndex
SETD.1 SbfsAt
CALL sbfsCopyWord
SETD.0 SbfsAt
CALL sbfsStepWord
RSTA
RSTB
CCF
ADD ; Q is zero: found.
RET
; DP2 is on an entry. Q is zero if it lives in the directory the walk is looking in.
;
; On a version one disk these two bytes are zero in every entry, and the walk starts at
; zero, so this always agrees - which is exactly how a flat disk reads correctly here
; without anything having been done to it.
sbfsMatchParent:
PSHD.2
POPD.0
DPUP.0 0d28
SETD.2 SbfsAt
CALL sbfsSameByte
BNQ sbfsMatchParentDone
INCD.0
INCD.2
CALL sbfsSameByte
sbfsMatchParentDone:
RET
; ---- Going back up ----
;
; Walks the directory to the entry SbfsTarget names and takes it apart, SbfsUpParent
; included. Q is zero if it got there.
;
; WALKING TO AN INDEX RATHER THAN WORKING OUT WHERE IT SITS. An index becomes a block and
; an offset by dividing by eight, and this machine has no divide. The walk costs a few
; block reads and no arithmetic at all, and only ".." ever needs it.
sbfsAtIndex:
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1
SETD.0 SbfsScanIndex
RSTA
STA.0
INCD.0
STA.0
sbfsAtBlock:
CALL sbfsReadBlock
BRQ sbfsAtLoaded
RET ; The read failed.
sbfsAtLoaded:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsAtEntry:
; Where the entry is has to be put down for the length of the comparison, because
; comparing two numbers wants DP2 for one of them.
PSHD.2
SETD.0 SbfsScanIndex
SETD.2 SbfsTarget
CALL sbfsCompareWord
POPD.2
BRQ sbfsAtFound
SETD.0 SbfsScanIndex
CALL sbfsStepWord
DPUP.2 0d32
LDA.1
DECA
STA.1
BNA sbfsAtEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BRA sbfsAtMissing
BRI sbfsAtBlock
sbfsAtMissing:
RSTA
INIB 0d1
CCF
ADD
RET
sbfsAtFound:
PSHD.2
POPD.3
CALL sbfsTakeEntry
RSTA
RSTB
CCF
ADD
RET
; DP3 is on an entry. Takes it apart into everything that describes what was found.
;
; Written once and called from both the scan and the walk to an index, because two copies
; of "which byte means what" is the kind of thing that stays right until one of them is
; changed.
sbfsTakeEntry:
LDA.3
SETD.0 SbfsFoundFlags
STA.0
PSHD.3
POPD.0
@@ -180,10 +569,29 @@ sbfsFindFound:
SETD.1 SbfsFileTail
STA.1
RSTA
RSTB
CCF
ADD ; Q is zero: found.
PSHD.3
POPD.0
DPUP.0 0d28
SETD.1 SbfsUpParent
CALL sbfsCopyWord
INIA 0x01
SETD.0 SbfsFieldsValid
STA.0
RET
; Takes one from the two byte number at DP0, the other way round from sbfsStepWord.
sbfsBackWord:
INCD.0
LDA.0
DECA
STA.0
BNC sbfsBackDone ; It did not borrow, so the high byte is untouched.
DECD.0
LDA.0
DECA
STA.0
sbfsBackDone:
RET
; ---- Walking the directory ----
@@ -306,6 +714,15 @@ sbfsWalkEnd:
; Takes the pieces out of the entry the walk stopped on. Everything lands in memory,
; because that is the only place a subroutine can leave anything.
sbfsWalkTake:
; What kind of thing it is, so that a listing can say so. A directory has no blocks, and
; without this it would be handed out as a file of no bytes - which is a different thing
; that happens to look the same from here.
SETD.1 SbfsWalkAt
LDD.0.1
LDA.0
SETD.1 SbfsFoundFlags
STA.1
SETD.1 SbfsWalkAt
LDD.0.1
INCD.0
@@ -606,6 +1023,26 @@ sbfsAllocEntry:
INIB 0x01
AND
BRQ sbfsAllocNext ; A free entry holds nothing, so it is in nobody's way.
; NOR DOES A DIRECTORY, and this says so rather than leaving it to be true by accident.
;
; It would be true by accident. A directory has no blocks AND no start, so the bounds
; below come out as nought to nought, and the first half of the overlap test - does the
; candidate begin before this entry ends - is false for every candidate there is, since
; they all begin past the directory. Taking these four instructions out changes nothing
; that any test can see, and that was checked rather than assumed.
;
; They stay because what keeps them right is a value written somewhere else. The day
; something gives a directory entry a start - a note about where its children begin, a
; use for a field that is spare today - the overlap test starts answering yes to runs
; that straddle it, and blocks stop being usable for no reason anybody could see from
; here. Four instructions to say "a directory is in nobody's way" where it is meant is
; cheaper than finding that out.
LDA.2
INIB 0x02
AND
BNQ sbfsAllocNext
CALL sbfsEntryBounds
; The comparisons need DP2 for their own purposes, so where this entry is goes on the
@@ -798,6 +1235,20 @@ sbfsCreateFill:
LDA.0
STA.1
; THE ROOT, SAID OUT LOUD. A new file goes in the root because nothing here can put one
; anywhere else yet, and the two bytes that say so are written rather than assumed to be
; zero already. They would be - a free entry has been wiped by delete or has never been
; used - but that is a fact about two other routines, and a fact kept somewhere else is
; one that can be changed without this noticing. A file appearing inside a directory it
; was never put in is not a failure anybody would think to look for.
PSHD.3
POPD.1
DPUP.1 0d28
RSTA
STA.1
INCD.1
STA.1
PSHD.3
POPD.1
DPUP.1 0d06
@@ -1059,6 +1510,17 @@ sbfsDelete:
CALL sbfsFind
BNQ sbfsDeleteFailed
; A DIRECTORY IS NOT DELETED HERE, AND THAT REFUSAL IS NOT POLITENESS. A parent is an
; entry index, and a freed index is handed straight to the next thing put on the disk -
; so the children of a directory wiped from under them would reappear inside whatever
; took its place. Nothing points downward, so there is no way to find them afterwards
; and no way to notice. Emptying it first is the only safe order there is.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ sbfsDeleteFailed
; How much room it was taking, worked out before the entry that says so is thrown away.
CALL sbfsFileExtent
@@ -1110,6 +1572,10 @@ sbfsDeleteFailed:
;
; DP0 is the name a file has, DP1 is the name it should have. Q is zero if it was renamed.
;
; A file keeps the directory it is in: only the twenty two bytes of the name change, and
; the parent is not among them. Moving something between directories is a different job
; and is not this one.
;
; Only the twenty two bytes of the name change, so no data moves and no block is touched
; but the one holding the entry. THAT IS WHAT MAKES A SAFE SAVE POSSIBLE. Writing a file
; that has grown means putting it somewhere else, and the obvious order - throw the old one
@@ -1138,6 +1604,15 @@ sbfsRename:
CALL sbfsFind
BNQ sbfsRenameFailed
; Not a directory. Renaming one would be safe enough, but the shell has no way to make
; one yet, so allowing it here would only be a way to reach something that cannot be
; got at any other way.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ sbfsRenameFailed
PSHD.3
POPD.1
DPUP.1 0d06 ; Past the flags, the start, the block count and the tail.
@@ -1201,6 +1676,20 @@ sbfsSaveFile:
SETD.1 SbfsSaveTail
STA.1
; Refused up front if the name belongs to a directory. Left to itself the delete below
; would refuse it, the rename at the end would refuse it too, and the save would fail
; having already written a temporary that nothing would ever come back for.
SETD.2 SbfsSaveName
LDD.0.2
CALL sbfsFind
BNQ sbfsSaveNotThere
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ sbfsSaveFailed
sbfsSaveNotThere:
; A temporary left behind by a save that did not finish would be in the way. Whether
; there was one is not worth asking about, since either answer leads here.
SETD.0 SbfsTempName
@@ -1281,6 +1770,33 @@ SbfsMatchLeft:
0x00
SbfsLeft:
0x00
; ---- What walking a path keeps ----
;
; SbfsAt is where the walk has got to, written the way a parent is written: an entry index
; plus one, so that zero is the root and a version one disk's zeroes already say it.
SbfsPathAt:
0x00 0x00
SbfsPathState:
0x00
SbfsPathLeft:
0x00
SbfsAt:
0x00 0x00
SbfsScanIndex:
0x00 0x00
SbfsTarget:
0x00 0x00
SbfsUpParent:
0x00 0x00
SbfsFoundFlags:
0x00
; Whether what is in SbfsFileStart and the rest really describes where the walk is now.
; Going up leaves them describing the entry it came from, and only the end of a path is
; close enough to care.
SbfsFieldsValid:
0x00
SbfsIndex:
0x00 0x00
+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