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:
Anachronaut
2026-08-25 09:18:29 -04:00
co-authored by Claude Opus 5
parent 36ce9f6ccf
commit da91a36d92
13 changed files with 948 additions and 43 deletions
+84 -1
View File
@@ -142,6 +142,16 @@ promptSay:
CALL textSame
BRQ doCd
SETD.0 CommandLine
SETD.1 MkdirName
CALL textSame
BRQ doMkdir
SETD.0 CommandLine
SETD.1 RmdirName
CALL textSame
BRQ doRmdir
SETD.0 CommandLine
SETD.1 DeleteName
CALL textSame
@@ -1006,6 +1016,61 @@ cdNotDirectory:
SETD.0 NotDirectory
BRI fileComplain
; ---- mkdir and rmdir ----
;
; Two commands rather than one that works out what you meant, and delete stays for files
; only. Each of the four says exactly what it will take, so none of them can be the one
; that took away more than was asked for.
doMkdir:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA mkdirWhat
CALL sbfsMakeDir
BNQ mkdirFailed
SETD.0 MadeText
CALL printString
CALL newLine
BRI prompt
mkdirWhat:
SETD.0 MkdirWhat
BRI fileComplain
mkdirFailed:
SETD.0 MkdirNo
BRI fileComplain
doRmdir:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA rmdirWhat
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsRemoveDir
BNQ rmdirFailed
SETD.0 RemovedText
CALL printString
CALL newLine
BRI prompt
rmdirWhat:
SETD.0 RmdirWhat
BRI fileComplain
rmdirFailed:
SETD.0 RmdirNo
BRI fileComplain
; ---- Writing out where the machine is ----
;
; Builds the working directory's path into CwdText and leaves CwdAt pointing at where it
@@ -3122,6 +3187,18 @@ DirectoryText:
"<dir>"
NotDirectory:
"that is not a directory"
MadeText:
"made"
RemovedText:
"removed"
MkdirWhat:
"mkdir what?"
RmdirWhat:
"rmdir what?"
MkdirNo:
"cannot make that: check the path, the name, and whether it is taken"
RmdirNo:
"cannot remove that: it must be a directory, and empty"
Separator:
"/"
@@ -3151,7 +3228,9 @@ 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"
HelpCdText:
"cd [path] go to a directory, or to the root with nothing after it"
"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"
HelpMoreText:
"delete <file> take it off the disk
rename <file> <to> call it something else
@@ -3285,6 +3364,10 @@ RenameName:
"rename"
CdName:
"cd"
MkdirName:
"mkdir"
RmdirName:
"rmdir"
HelpName:
"help"
ExitName:
+597 -34
View File
@@ -320,6 +320,173 @@ sbfsFindMissing:
ADD
RET
; ---- Splitting a path into where and what ----
;
; DP0 points at a path. Everything but the last name is walked, so what comes back is the
; directory a thing should be MADE in and the name to make it under: SbfsAt is the
; directory and SbfsWanted holds the name, padded to twenty two the way an entry holds one.
; Q is zero if that worked.
;
; The head is copied rather than the path being cut in place. The path belongs to whoever
; called, and a routine that writes a zero byte into somebody else's string is one that has
; to put it back on every way out, including the ways out that failed.
;
; THE SEPARATOR STAYS ON THE END OF THE HEAD, and that is what makes one rule cover both
; kinds of path. "/x" leaves a head of "/", which is the root; "x" leaves an empty head,
; which is where the machine already is; and "A/x" leaves "A/", which is neither of those
; and needs no special case to say so.
sbfsWalkParent:
RSTA
SETD.1 SbfsHeadLen
STA.1
SETD.1 SbfsSpanAt
STA.1
PSHD.0
POPD.2 ; DP2 walks the path, DP0 stays on the front of it.
sbfsSplitScan:
LDA.2
BRA sbfsSplitEnd
INIB 0x2F
CCF
SUB
BNQ sbfsSplitStep
; A separator. The head runs to just past it, so the last one to be seen wins.
SETD.1 SbfsSpanAt
LDA.1
INCA
SETD.1 SbfsHeadLen
STA.1
sbfsSplitStep:
SETD.1 SbfsSpanAt
LDA.1
INCA
STA.1
BRA sbfsSplitTooLong ; Round past two hundred and fifty five: not a path.
INCD.2
BRI sbfsSplitScan
sbfsSplitEnd:
; The head, up to and including the separator that ended it.
PSHD.0
POPD.2
SETD.1 SbfsHead
SETD.0 SbfsHeadLen
LDA.0
INIB 0d95
CCF
SUB
BNC sbfsSplitTooLong ; Longer than there is room to copy it into.
SETD.0 SbfsHeadLen
LDA.0
SETD.0 SbfsSpanAt
STA.0 ; Counting it back down again.
sbfsHeadCopy:
SETD.0 SbfsSpanAt
LDA.0
BRA sbfsHeadDone
DECA
STA.0
LDA.2
STA.1
INCD.2
INCD.1
BRI sbfsHeadCopy
sbfsHeadDone:
RSTA
STA.1 ; The zero that ends the head.
; And the last name, which is whatever DP2 is now on. Measured before it is copied,
; because twenty two is all an entry holds and a longer name is refused rather than cut
; down - a name cut to twenty two characters is a different name and might well be
; taken. Measuring is the only way to know: looking at the twenty third character of a
; shorter name reads past the end of somebody else's string.
PSHD.2
POPD.0
LDA.0
BRA sbfsSplitNoName ; The path ended in a separator, so it names nothing.
RSTA
SETD.1 SbfsSpanAt
STA.1
sbfsLeafMeasure:
LDA.2
BRA sbfsLeafMeasured
SETD.1 SbfsSpanAt
LDA.1
INCA
STA.1
INIB 0d23
CCF
SUB
BRQ sbfsSplitTooLong
INCD.2
BRI sbfsLeafMeasure
sbfsLeafMeasured:
; KEPT SOMEWHERE OF ITS OWN, and this is not tidiness. Walking the head goes through
; sbfsPathNext, which puts every name it meets into SbfsWanted on its way past - so the
; last name of the head would land exactly where the leaf was and the thing would be
; created under the name of the directory it was going into. "mkdir Apps/Deep" made
; /Apps/Apps.
SETD.1 SbfsLeaf
CALL sbfsKeepName
; Now where it goes. The head is walked exactly the way any other path is.
SETD.0 SbfsHead
CALL sbfsWalk
BNQ sbfsSplitNoName
; And the leaf comes back out, now that nothing else is going to write there.
SETD.0 SbfsLeaf
SETD.1 SbfsWanted
INIA 0d22
SETD.2 SbfsCount
STA.2
sbfsLeafBack:
LDA.0
STA.1
INCD.0
INCD.1
LDA.2
DECA
STA.2
BNA sbfsLeafBack
; And it has to be somewhere things can be put. The root always is.
SETD.0 SbfsAt
LDA.0
INCD.0
LDB.0
OR
BRQ sbfsSplitGood
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BRQ sbfsSplitNoName
sbfsSplitGood:
RSTA
RSTB
CCF
ADD
RET
sbfsSplitTooLong:
sbfsSplitNoName:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Taking a path apart ----
;
; Copies the next name out of the path into SbfsWanted, padded with zeroes to twenty two
@@ -502,6 +669,21 @@ sbfsScanFound:
ADD ; Q is zero: found.
RET
; DP2 is on an entry. Q is zero if it lives in the directory SbfsHoldAt names. The third
; of these, and the reason all three are subroutines: a RET puts DP2 back on the entry.
sbfsMatchHold:
PSHD.2
POPD.0
DPUP.0 0d28
SETD.2 SbfsHoldAt
CALL sbfsSameByte
BNQ sbfsMatchHoldDone
INCD.0
INCD.2
CALL sbfsSameByte
sbfsMatchHoldDone:
RET
; DP2 is on an entry. Q is zero if it lives in the directory the machine is in. The same
; comparison sbfsMatchParent makes, against the other of the two places a walk can be.
sbfsWalkHere:
@@ -910,6 +1092,24 @@ sbfsMatchYes:
ADD
RET
; Twenty two bytes from DP0 to DP1, which is one name exactly as an entry holds it. Not
; sbfsCopyWord's job and not sbfsKeepName's either: this one is for a name that is already
; padded and is only being put somewhere safe.
sbfsCopyName:
INIA 0d22
SETD.2 SbfsCount
STA.2
sbfsCopyNameLoop:
LDA.0
STA.1
INCD.0
INCD.1
LDA.2
DECA
STA.2
BNA sbfsCopyNameLoop
RET
; Copies the name at DP0 into DP1, twenty two bytes of it, padding with zeroes the way an
; entry is padded so that the two can be compared as they stand.
sbfsKeepName:
@@ -1261,9 +1461,13 @@ sbfsExtentDone:
; which has to be settled before it is made because nothing here can grow one afterwards.
; Q is zero if it was made, and then SbfsFileStart says where its blocks are.
sbfsCreate:
SETD.1 SbfsWanted
CALL sbfsKeepName
; Where it goes and what it is called come out of the path together. Everything below
; works from SbfsAt and SbfsWanted, so anything that already knows those two can start
; at sbfsCreateAt and skip the walking.
CALL sbfsWalkParent
BNQ sbfsCreateFailed
sbfsCreateAt:
CALL sbfsFileExtent
CALL sbfsAllocate
BNQ sbfsCreateFailed
@@ -1341,19 +1545,16 @@ 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.
; WHICH DIRECTORY IT IS IN, written rather than left to be zero by luck. A free entry
; has been wiped by delete or has never been used, so those two bytes would say the root
; on their own - 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 turning up 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
SETD.0 SbfsAt
CALL sbfsCopyWord
PSHD.3
POPD.1
@@ -1597,6 +1798,272 @@ sbfsSameByte:
XOR
RET
; ---- Making a directory ----
;
; DP0 names one. Q is zero if it was made.
;
; A directory costs ONE ENTRY AND NO BLOCKS AT ALL: its start, its block count and its tail
; all stay zero. That is what keeps the flat array of entries the whole allocation map,
; which is the thing this filesystem is built on - with files laid down contiguously, every
; block is inside some entry's range or it is not, and an entry with no range is in
; nobody's way.
sbfsMakeDir:
CALL sbfsWalkParent
BNQ sbfsMakeFailed
; Nothing of that name in there already. Two entries with one name in one directory is a
; directory that cannot be searched sensibly: a search answers with whichever it meets
; first, and the other becomes unreachable without ever having been deleted.
CALL sbfsScanFor
BRQ sbfsMakeFailed
; A scan that found nothing leaves the walk where it was, so SbfsAt still says where
; this is going. Walking the head again to be sure would put the head's last name back
; into SbfsWanted and undo the leaf.
; A free entry, found the same way creating a file finds one.
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1
sbfsMakeBlock:
CALL sbfsReadBlock
BNQ sbfsMakeFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsMakeEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsMakeFill ; This one is free.
DPUP.2 0d32
LDA.1
DECA
STA.1
BNA sbfsMakeEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BRA sbfsMakeFailed ; The directory is full.
BRI sbfsMakeBlock
sbfsMakeFill:
PSHD.2
POPD.3
INIA 0x03
STA.3 ; In use, and a directory.
; No blocks, no start and no tail. Written out rather than left alone, because a free
; entry is not the only thing that lands here.
PSHD.3
POPD.1
INCD.1
INIB 0d5
sbfsMakeZero:
RSTA
STA.1
INCD.1
DECB
BNB sbfsMakeZero
PSHD.3
POPD.1
DPUP.1 0d28
SETD.0 SbfsAt
CALL sbfsCopyWord
PSHD.3
POPD.1
DPUP.1 0d06
SETD.0 SbfsWanted
INIA 0d22
SETD.2 SbfsCount
STA.2
sbfsMakeName:
LDA.0
STA.1
INCD.0
INCD.1
LDA.2
DECA
STA.2
BNA sbfsMakeName
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
BNQ sbfsMakeFailed
CALL sbfsRaiseVersion
RET
sbfsMakeFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; The disk now has a directory on it, so it is a version two disk and has to say so.
;
; THIS IS THE ONLY THING THAT RAISES THE NUMBER, because it is the only thing that makes
; the difference between the two versions real. A disk stays readable by anything that has
; never heard of a directory right up until it actually has one.
sbfsRaiseVersion:
RSTA
SETD.0 SbfsBlock
STA.0
INCD.0
STA.0
CALL sbfsReadBlock
BNQ sbfsRaiseDone
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.0 SbfsBuffer
DPUP.0 0d04
INIA 0d2
STA.0
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
sbfsRaiseDone:
RSTA
RSTB
CCF
ADD
RET
; ---- Removing a directory ----
;
; DP0 names one. Q is zero if it went.
;
; ANYTHING STILL INSIDE IT IS A REFUSAL, and that 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 removed from under them would turn up inside whatever took its
; place. Nothing points downward, so there would be no way to find them afterwards and no
; way to notice. Emptying it first is the only safe order there is.
sbfsRemoveDir:
CALL sbfsFind
BNQ sbfsRemoveFailed
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BRQ sbfsRemoveFailed ; A file. Deleting is for those.
; Where it is, kept while the disk is asked whether anything lives in it - the asking
; reads other blocks and leaves the walk somewhere else entirely.
SETD.0 SbfsAt
SETD.1 SbfsHoldAt
CALL sbfsCopyWord
CALL sbfsHasChildren
BRQ sbfsRemoveFailed
; Found again, because looking for children read over the block the entry was in.
SETD.0 SbfsHoldAt
SETD.1 SbfsTarget
CALL sbfsCopyWord
SETD.0 SbfsTarget
CALL sbfsBackWord
CALL sbfsAtIndex
BNQ sbfsRemoveFailed
CALL sbfsWipeFound
RET
sbfsRemoveFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; Q is zero if anything at all says its parent is SbfsHoldAt. Nothing points downward, so
; the only way to know what is in a directory is to ask everything whether it is in it.
sbfsHasChildren:
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1
sbfsChildBlock:
CALL sbfsReadBlock
BRQ sbfsChildLoaded
RET ; The read failed, and Q says so - which reads as "yes", and
; refusing to remove something on a disk that will not read is the
; right way round to be wrong.
sbfsChildLoaded:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsChildEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsChildNext
; Through a CALL, so that DP2 comes back on the entry without anything having to work
; out where that was. Doing the comparison in line here meant clobbering DP2 and then
; rebuilding it from the buffer and the count - which had the subtraction the wrong way
; round, walked the pointer off the end of the block, and let rmdir take a directory
; with something still in it. Exactly the failure this routine exists to prevent.
CALL sbfsMatchHold
BRQ sbfsChildYes
sbfsChildNext:
DPUP.2 0d32
SETD.1 SbfsCount
LDA.1
DECA
STA.1
BNA sbfsChildEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BRA sbfsChildNone
BRI sbfsChildBlock
sbfsChildYes:
RSTA
RSTB
CCF
ADD ; Q is zero: something lives there.
RET
sbfsChildNone:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Deleting ----
;
; Frees a file. DP0 names it, and Q is zero if it went.
@@ -1627,6 +2094,16 @@ sbfsDelete:
AND
BNQ sbfsDeleteFailed
CALL sbfsWipeFound
RET
; Throws away whatever the last find landed on, wherever it landed. DP3 is on the entry and
; SbfsBlock is the directory block it came out of, which is everything needed to change it
; and put it back.
;
; Removing a directory arrives here too. A directory has no blocks, so the count that goes
; back to the free total is nought and the sum is right without being a special case.
sbfsWipeFound:
; How much room it was taking, worked out before the entry that says so is thrown away.
CALL sbfsFileExtent
@@ -1693,16 +2170,24 @@ sbfsRename:
SETD.2 SbfsSavedName
STD.0.2
; The new name is a path like any other, so only its last name is the name. Renaming
; something to "/A/notes.txt" used to call it that, all thirteen characters of it.
PSHD.1
POPD.0
CALL sbfsWalkParent
BNQ sbfsRenameFailed
SETD.0 SbfsAt
SETD.1 SbfsHoldAt
CALL sbfsCopyWord
SETD.0 SbfsWanted
SETD.1 SbfsNewName
CALL sbfsKeepName ; Twenty two bytes, padded, the way an entry holds one.
CALL sbfsCopyName
; Refused if something already answers to the new name. Two entries with one name is a
; disk that cannot be searched sensibly: a search answers with whichever it meets first,
; so the other becomes unreachable without ever having been deleted.
SETD.0 SbfsNewName
CALL sbfsFind
; Refused if something already answers to that name in that directory. Two entries with
; one name in one place is a directory that cannot be searched sensibly: a search answers
; with whichever it meets first, and the other becomes unreachable without ever having
; been deleted.
CALL sbfsScanFor
BRQ sbfsRenameFailed
SETD.2 SbfsSavedName
@@ -1710,6 +2195,14 @@ sbfsRename:
CALL sbfsFind
BNQ sbfsRenameFailed
; RENAMING DOES NOT MOVE ANYTHING. Only the twenty two bytes of the name change, and the
; parent is not among them - so a new path naming a different directory would be a lie
; the disk went along with. Refused instead.
SETD.0 SbfsUpParent
SETD.2 SbfsHoldAt
CALL sbfsCompareWord
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.
@@ -1782,12 +2275,26 @@ 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.
; WHERE IT GOES, WORKED OUT ONCE. Everything below is done in terms of a directory and
; a name rather than a path, and that is what makes the careful order below work in a
; subdirectory: the temporary has to be made in the SAME directory as the file, because
; the rename at the end changes a name and does not move anything.
SETD.2 SbfsSaveName
LDD.0.2
CALL sbfsFind
CALL sbfsWalkParent
BNQ sbfsSaveFailed
SETD.0 SbfsAt
SETD.1 SbfsSaveParent
CALL sbfsCopyWord
SETD.0 SbfsWanted
SETD.1 SbfsSaveLeaf
CALL sbfsCopyName
; Refused up front if that 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
; then instead, having already written a temporary nothing would ever come back for.
CALL sbfsSaveWhere
CALL sbfsScanFor
BNQ sbfsSaveNotThere
SETD.0 SbfsFoundFlags
LDA.0
@@ -1798,8 +2305,11 @@ 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
CALL sbfsDelete
CALL sbfsSaveTemp
CALL sbfsScanFor
BNQ sbfsSaveNoTemp
CALL sbfsWipeFound
sbfsSaveNoTemp:
SETD.0 SbfsFileBlocks
SETD.2 SbfsSaveBlocks
@@ -1809,8 +2319,8 @@ sbfsSaveNotThere:
SETD.1 SbfsFileTail
STA.1
SETD.0 SbfsTempName
CALL sbfsCreate
CALL sbfsSaveTemp
CALL sbfsCreateAt
BNQ sbfsSaveFailed
SETD.2 SbfsSaveData
@@ -1818,16 +2328,51 @@ sbfsSaveNotThere:
CALL sbfsWriteFile
BNQ sbfsSaveFailed
; Now, and not before, the old one goes. It may not exist, which is what saving something
; for the first time looks like from here.
SETD.2 SbfsSaveName
LDD.0.2
CALL sbfsDelete
; Now, and not before, the old one goes. It may not be there at all, which is what
; saving something for the first time looks like from here.
CALL sbfsSaveWhere
CALL sbfsScanFor
BNQ sbfsSaveNoOld
CALL sbfsWipeFound
sbfsSaveNoOld:
; And the temporary takes its name. Found again first, because everything above has been
; reading other blocks over the one it lives in.
CALL sbfsSaveTemp
CALL sbfsScanFor
BNQ sbfsSaveFailed
PSHD.3
POPD.1
DPUP.1 0d06
SETD.0 SbfsSaveLeaf
CALL sbfsCopyName
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
RET
; The directory the file is going in, and the name it is going under. Said again before
; each step, because every step in between goes to the disk and leaves the walk somewhere
; else entirely.
sbfsSaveWhere:
SETD.0 SbfsSaveParent
SETD.1 SbfsAt
CALL sbfsCopyWord
SETD.0 SbfsSaveLeaf
SETD.1 SbfsWanted
CALL sbfsCopyName
RET
; The same directory, under the name the half finished file is written as.
sbfsSaveTemp:
SETD.0 SbfsSaveParent
SETD.1 SbfsAt
CALL sbfsCopyWord
SETD.0 SbfsTempName
SETD.2 SbfsSaveName
LDD.1.2
CALL sbfsRename
SETD.1 SbfsWanted
CALL sbfsKeepName
RET
sbfsSaveFailed:
@@ -1895,6 +2440,20 @@ SbfsCwd:
; plus one, so that zero is the root and a version one disk's zeroes already say it.
SbfsPathAt:
0x00 0x00
; ---- What splitting a path off its last name keeps ----
SbfsHead:
#Reserve 0d96
SbfsHeadLen:
0x00
SbfsHoldAt:
0x00 0x00
; The last name of a path, kept out of the way while the rest of the path is walked.
SbfsLeaf:
#Reserve 0d23
SbfsSpanAt:
0x00
SbfsPathState:
0x00
SbfsPathLeft:
@@ -1958,6 +2517,10 @@ SbfsSaveBlocks:
0x00 0x00
SbfsSaveTail:
0x00
SbfsSaveParent:
0x00 0x00
SbfsSaveLeaf:
#Reserve 0d23
; What a document is called while it is being written and is not yet the real thing. A
; name nothing else is likely to want, and short enough to leave room for a long one.