THE MACHINE SUPPLIES BLOCKS AND SAYS WHAT A DRIVE IS. It says nothing about filesystems, which is what leaves room for a system that would rather have its own - and is why the volatile bit is a fact about the hardware rather than a promise about SBFS. 0x26 what the selected drive is: bit 0, contents do not survive 0x27, 0x28 how many blocks it has --ram-disk N a drive of N blocks with memory behind it A drive of memory selects, reads, writes and has a size like any other, and a program cannot tell the difference except by how fast it was. The one thing it cannot work out for itself is that the contents are volatile, because an empty disk and a volatile disk look identical from outside. THAT BIT IS THE DIFFERENCE BETWEEN A DRIVE A SYSTEM MAY FORMAT ON SIGHT AND ONE IT MUST NOT. CosmOS formats a volatile drive it cannot read, because there was never anything on it to lose, and leaves every other unreadable drive alone - an unformatted floppy is not an invitation, it is a blank floppy. Removing that check formats somebody's blank disk, which is checked rather than asserted: cosmosBlankDisk boots with one and requires it to be refused. So CosmOS grew a format. The size comes from the drive rather than from a superblock, since a superblock states a size too and that is no use on a disk which has not got one yet. Sixteen directory blocks, 128 names, chosen rather than worked out: a scratch disk runs out of names long before room, and this machine cannot divide. The RAM disk is no faster on this emulator by default, and that is honest rather than disappointing: the emulated disk has no seek time unless asked for one. With --disk-cycles 10000 the same copy is 7.94M cycles against 8.70M, the difference being every write. run.sh takes "ram:2048" where an image name goes, which needs no removing between runs because there is nothing to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
3851 lines
99 KiB
NASM
3851 lines
99 KiB
NASM
; sbfs.asm
|
|
; Reading the SplitBit Filesystem.
|
|
;
|
|
; This belongs to CosmOS, which is the thing that needs it. Programs outside CosmOS may
|
|
; include it, and the test programs do, but when CosmOS becomes a repository of its own
|
|
; this file goes with it and anything left behind takes a copy.
|
|
;
|
|
; The other implementation of this format is SplitDisk on the host. Nothing is shared
|
|
; between them but the written specification, so anything that changes here has to change
|
|
; there in the same breath.
|
|
;
|
|
; ---- What an entry's flags mean ----
|
|
;
|
|
; 0x01 In use. A zero here is a free slot, whatever else the bytes hold.
|
|
; 0x02 A directory, which has no blocks at all.
|
|
; 0x04 A file being written and not yet committed. See sbfsSaveFile.
|
|
;
|
|
; The disk's buffer is registered as bank 3, and every block read lands there and is then
|
|
; blitted where it is wanted. The CPU never touches the buffer directly, because nothing
|
|
; can: memory a device brings is reachable only through the controller.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Program
|
|
|
|
; Registers the disk's buffer, reads the superblock, and checks that the disk really is
|
|
; one of ours. Q is zero if the disk is ready to be read.
|
|
sbfsMount:
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
INIA 0x20
|
|
OUTA 0xE2
|
|
INIA 0x03
|
|
OUTA 0xE8 ; RegisterBank: bank 3 is the disk's buffer.
|
|
|
|
SETD.0 SbfsBlock
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
STA.0 ; Block 0, the superblock.
|
|
CALL sbfsReadBlock
|
|
BRQ sbfsMountRead
|
|
RET ; The read failed, and Q says so.
|
|
|
|
sbfsMountRead:
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
|
|
; "SBFS", or this is not a disk of ours. A blank image is all zeroes, so without this
|
|
; an unformatted disk would look like a formatted one with no files on it.
|
|
SETD.0 SbfsBuffer
|
|
SETD.2 SbfsMagic
|
|
INIA 0d4
|
|
SETD.1 SbfsCount
|
|
STA.1
|
|
sbfsMagicLoop:
|
|
CALL sbfsSameByte
|
|
BRQ sbfsMagicSame
|
|
RET ; Q is the difference, which is not zero.
|
|
sbfsMagicSame:
|
|
INCD.0
|
|
INCD.2
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsMagicLoop
|
|
|
|
sbfsMagicDone:
|
|
; 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:
|
|
; Where the directory is and how big it is. The superblock carries both so that disks
|
|
; of different sizes can have different directories without a new format.
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d08
|
|
SETD.1 SbfsDirStart
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d10
|
|
SETD.1 SbfsDirBlocks
|
|
CALL sbfsCopyWord
|
|
|
|
; A DIRECTORY WHOSE LAST ENTRIES CANNOT BE NAMED AS A PARENT. Eight entries to a block,
|
|
; and the parent is an index plus one in two bytes, so entry 65535 has no parent number
|
|
; at all - adding one wraps to zero, and zero is the root.
|
|
;
|
|
; It does not fail by refusing. Anything created inside such a directory writes a parent
|
|
; of zero and lands in the ROOT, while whatever asked reports the path it wanted;
|
|
; looking there afterwards finds nothing, because the search is for a parent the entry
|
|
; does not carry, so the same create succeeds over and over and piles up entries of one
|
|
; name in the root. Two entries of one name in one place is the thing rename refuses
|
|
; outright, and this made them by the handful.
|
|
;
|
|
; 8191 blocks is the most, so anything from 0x2000 up is refused. Only the high byte has
|
|
; to be looked at to know.
|
|
SETD.0 SbfsDirBlocks
|
|
LDA.0
|
|
INIB 0x20
|
|
CCF
|
|
SUB
|
|
BNC sbfsMountTooBig ; The high byte is 0x20 or more, so more than 8191 blocks.
|
|
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d06
|
|
SETD.1 SbfsDiskBlocks
|
|
CALL sbfsCopyWord
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: mounted.
|
|
RET
|
|
|
|
sbfsMountTooBig:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD ; Q is not zero: not a disk this will mount.
|
|
RET
|
|
|
|
; ---- Making a disk into a filesystem ----
|
|
;
|
|
; The machine hands out blocks and says how many; what they mean is the system's business.
|
|
; That is the whole reason this exists here rather than only in the host tool: a machine with
|
|
; a drive made of memory comes up with a drive full of zeroes, and zeroes are not a
|
|
; filesystem. Somebody has to write the first one, and it should be whoever is going to read
|
|
; it - which is also what leaves room for a system that would rather have its own.
|
|
;
|
|
; Q is zero if it worked. Everything on the drive is lost, which is why nothing calls this
|
|
; except on a drive the machine has said is volatile.
|
|
sbfsFormat:
|
|
; How big it is, which only the drive can say. A superblock would say too, and a disk with
|
|
; no superblock is exactly the case this is for.
|
|
INA 0x27
|
|
SETD.1 SbfsScratch
|
|
STA.1
|
|
INA 0x28
|
|
INCD.1
|
|
STA.1
|
|
|
|
; The buffer, cleared, because everything not written below has to be nought and the
|
|
; controller's buffer holds whatever was last read.
|
|
SETD.0 SbfsBuffer
|
|
RSTB
|
|
sbfsFormatClear:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BNB sbfsFormatClear ; 256 of them: B wraps from nought to 255 and back to nought.
|
|
|
|
; "SBFS", and the version. Two, because a disk made now has directories.
|
|
SETD.0 SbfsMagic
|
|
SETD.1 SbfsBuffer
|
|
INIB 0d4
|
|
sbfsFormatMagic:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
DECB
|
|
BNB sbfsFormatMagic
|
|
INIA 0d2
|
|
STA.1 ; Offset 4: the version.
|
|
|
|
; Offset 6, how many blocks the disk has, as the drive reported it.
|
|
SETD.1 SbfsBuffer
|
|
DPUP.1 0d06
|
|
SETD.0 SbfsScratch
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.0
|
|
STA.1
|
|
|
|
; Offset 8, where the directory starts: block one, straight after this one.
|
|
SETD.1 SbfsBuffer
|
|
DPUP.1 0d08
|
|
RSTA
|
|
STA.1
|
|
INCD.1
|
|
INIA 0d1
|
|
STA.1
|
|
|
|
; Offset 10, how many blocks of directory. SIXTEEN, WHICH IS 128 NAMES, and chosen rather
|
|
; than worked out: a scratch disk runs out of names long before it runs out of room, and
|
|
; this machine cannot divide, so a number that fits every size this is used for is worth
|
|
; more than arithmetic to find a better one.
|
|
SETD.1 SbfsBuffer
|
|
DPUP.1 0d10
|
|
RSTA
|
|
STA.1
|
|
INCD.1
|
|
INIA 0d16
|
|
STA.1
|
|
|
|
; Block nought, written.
|
|
SETD.0 SbfsBlock
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsFormatFailed
|
|
|
|
; And the directory cleared, so that no entry is in use. The buffer is already nought
|
|
; everywhere the superblock did not reach, so it is cleared once more and written sixteen
|
|
; times rather than built again each time.
|
|
SETD.0 SbfsBuffer
|
|
RSTB
|
|
sbfsFormatBlank:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BNB sbfsFormatBlank
|
|
|
|
INIA 0d16
|
|
SETD.1 SbfsCount
|
|
STA.1
|
|
sbfsFormatDirectory:
|
|
SETD.0 SbfsBlock
|
|
INCD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0 ; Blocks one to sixteen. A directory never crosses 255 here.
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsFormatFailed
|
|
SETD.1 SbfsCount
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsFormatDirectory
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
sbfsFormatFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Every drive the machine has ----
|
|
;
|
|
; Asked for rather than assumed: the controller says how many are plugged in, and each is
|
|
; selected and mounted in turn. A drive with nothing in it, or a disk this cannot read, is
|
|
; left unmounted rather than stopping the others - a machine with a good disk in drive 0 and
|
|
; a blank in drive 1 should start.
|
|
;
|
|
; Q is zero if drive 0 mounted, because that is the one the system came off and the one the
|
|
; shell will be standing in when it gets a prompt.
|
|
sbfsMountAll:
|
|
RSTA
|
|
SETD.1 SbfsMounted
|
|
STA.1
|
|
SETD.1 SbfsDrive
|
|
STA.1
|
|
OUTA 0x24 ; Drive 0, whatever the controller was left on.
|
|
|
|
; The live record belongs to nobody yet, so every slot starts empty and a drive that fails
|
|
; to mount keeps an empty one.
|
|
SETD.0 SbfsMountTable
|
|
INIB 0d32
|
|
sbfsMountClear:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BNB sbfsMountClear
|
|
|
|
INA 0x25
|
|
SETD.1 SbfsDriveCount
|
|
STA.1
|
|
RSTA
|
|
SETD.1 SbfsDriveAt
|
|
STA.1
|
|
|
|
sbfsMountEach:
|
|
SETD.1 SbfsDriveAt
|
|
LDA.1
|
|
SETD.1 SbfsDriveCount
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ sbfsMountAllDone ; Past the last one.
|
|
|
|
; Selected directly rather than through sbfsUse: there is nothing in the live record worth
|
|
; putting back yet, and sbfsUse would copy eight bytes of nothing into a slot.
|
|
SETD.1 SbfsDriveAt
|
|
LDA.1
|
|
OUTA 0x24
|
|
SETD.1 SbfsDrive
|
|
STA.1
|
|
RSTA
|
|
SETD.1 SbfsBufferKnown
|
|
STA.1
|
|
|
|
CALL sbfsMount
|
|
BRQ sbfsMountGot
|
|
|
|
; ---- Nothing readable, and whether that is an invitation ----
|
|
;
|
|
; A drive the machine calls VOLATILE loses everything when the machine stops, so a volatile
|
|
; drive with no filesystem on it never had one to lose and bringing it up is the system's
|
|
; job. Anything else is somebody's disk: an unformatted floppy is not an invitation, it is
|
|
; a blank floppy, and a system that formatted it on sight would be a system you could not
|
|
; safely put a disk into.
|
|
;
|
|
; THE MACHINE SAYS WHAT THE DRIVE IS AND NOTHING ABOUT FILESYSTEMS. A system that would
|
|
; rather have its own reads the same bit and writes whatever it likes.
|
|
INA 0x26
|
|
INIB 0x01 ; VOLATILE
|
|
AND
|
|
BRQ sbfsMountNext ; Not ours to touch.
|
|
|
|
CALL sbfsFormat
|
|
BNQ sbfsMountNext
|
|
CALL sbfsMount
|
|
BNQ sbfsMountNext
|
|
|
|
sbfsMountGot:
|
|
|
|
; Mounted: remember which, and put the live record where it belongs.
|
|
SETD.1 SbfsDriveAt
|
|
LDA.1
|
|
CALL sbfsDriveBit
|
|
MVQA ; The bit is in Q, and RET put the old A back. OR reads A and B.
|
|
SETD.1 SbfsMounted
|
|
LDB.1
|
|
OR
|
|
STQ.1
|
|
|
|
SETD.1 SbfsDriveAt
|
|
LDA.1
|
|
CALL sbfsSlotAt
|
|
PSHD.3
|
|
POPD.1
|
|
SETD.0 SbfsMountLive
|
|
CALL sbfsCopyMount
|
|
|
|
sbfsMountNext:
|
|
SETD.1 SbfsDriveAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI sbfsMountEach
|
|
|
|
sbfsMountAllDone:
|
|
; Back to drive 0 and its record, which is where a shell starts. A stays nought throughout,
|
|
; which is the drive, the note on the buffer, and the slot to fetch.
|
|
RSTA
|
|
OUTA 0x24
|
|
SETD.1 SbfsDrive
|
|
STA.1
|
|
SETD.1 SbfsBufferKnown
|
|
STA.1
|
|
CALL sbfsSlotAt
|
|
PSHD.3
|
|
POPD.0
|
|
SETD.1 SbfsMountLive
|
|
CALL sbfsCopyMount
|
|
|
|
; Is drive 0 one of the ones that mounted?
|
|
INIA 0x01
|
|
SETD.1 SbfsMounted
|
|
LDB.1
|
|
AND
|
|
BRQ sbfsMountAllNone
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
sbfsMountAllNone:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; A = a drive number. Q is the bit that stands for it in SbfsMounted.
|
|
sbfsDriveBit:
|
|
INIB 0x01
|
|
sbfsDriveBitStep:
|
|
BRA sbfsDriveBitDone
|
|
PSHA
|
|
RSTA
|
|
SHL ; A and B are one register to SHL, so with A nought this is B
|
|
POPA ; doubled - which for four drives never reaches the top.
|
|
DECA
|
|
BRI sbfsDriveBitStep
|
|
sbfsDriveBitDone:
|
|
RSTA
|
|
CCF
|
|
ADD ; Q = B, which is the bit.
|
|
RET
|
|
|
|
; ---- Changing which disk is the disk ----
|
|
;
|
|
; A holds the drive. Q is zero if it is now the one in use.
|
|
;
|
|
; The eight live bytes go back to the drive they belong to and the wanted drive's come in.
|
|
; Everything below this line in the file goes on reading the same four names it always has and
|
|
; never learns that more than one disk exists - which is the whole of why this is affordable.
|
|
;
|
|
; The buffer is FORGOTTEN, and that is not tidiness. The controller has one buffer shared by
|
|
; every drive, so the note of which block is in it is wrong the moment the drive changes.
|
|
; Leaving it would mean the next read of that block number quietly skipping the disk and
|
|
; handing back the other drive's data.
|
|
sbfsUse:
|
|
SETD.1 SbfsDrive
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ sbfsUseAlready ; Already there, and swapping would be a long way round to nothing.
|
|
|
|
PSHA ; The drive that was asked for, kept across the copying.
|
|
|
|
; The live record back to the drive it belongs to. DP1 is still SbfsDrive, from the
|
|
; comparison above.
|
|
LDA.1
|
|
CALL sbfsSlotAt
|
|
PSHD.3
|
|
POPD.1
|
|
SETD.0 SbfsMountLive
|
|
CALL sbfsCopyMount
|
|
|
|
; And the wanted drive's record into the live eight.
|
|
POPA
|
|
PSHA
|
|
CALL sbfsSlotAt
|
|
PSHD.3
|
|
POPD.0
|
|
SETD.1 SbfsMountLive
|
|
CALL sbfsCopyMount
|
|
|
|
POPA
|
|
SETD.1 SbfsDrive
|
|
STA.1
|
|
OUTA 0x24
|
|
|
|
; ---- The note on the buffer belongs to the drive that is leaving ----
|
|
;
|
|
; One buffer serves every drive, so "block 31 is in the buffer" stops being true the moment
|
|
; the drive changes, and a read of block 31 that trusted it would hand back the other disk.
|
|
;
|
|
; IT CANNOT CURRENTLY BE REACHED, and that is worth writing down rather than leaving as an
|
|
; implied claim. Only the file read-ahead consults the note - a directory scan deliberately
|
|
; does not - and finding a file requires a scan, which overwrites the note on the way past.
|
|
; Two disks were built with the same file at the same block to try to catch it and the
|
|
; answer was right either way.
|
|
;
|
|
; Kept because it is three instructions and it holds an invariant rather than patching a
|
|
; symptom: the note describes the selected drive. The day something reads two files without
|
|
; a directory between them, this is already true instead of being a bug with a story.
|
|
RSTA
|
|
SETD.1 SbfsBufferKnown
|
|
STA.1
|
|
|
|
sbfsUseAlready:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; DP3 = the eight bytes belonging to the drive in A. Stepped rather than multiplied, because
|
|
; this machine cannot multiply and there are at most three steps.
|
|
sbfsSlotAt:
|
|
SETD.3 SbfsMountTable
|
|
BRA sbfsSlotThere
|
|
sbfsSlotStep:
|
|
DPUP.3 0d08
|
|
DECA
|
|
BNA sbfsSlotStep
|
|
sbfsSlotThere:
|
|
RET
|
|
|
|
; Eight bytes, DP0 to DP1.
|
|
sbfsCopyMount:
|
|
INIB 0d8
|
|
sbfsCopyMountByte:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
DECB
|
|
BNB sbfsCopyMountByte
|
|
RET
|
|
|
|
; ---- Finding something by path ----
|
|
;
|
|
; DP0 points at a path ending in a zero byte: names with '/' between them. A path that
|
|
; begins with a separator is walked from the root, and anything else from SbfsCwd, which
|
|
; is where the machine currently is.
|
|
;
|
|
; 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. It now means a name in the
|
|
; working directory rather than a name in the root, which is the whole of what a working
|
|
; directory is, and no caller had to be told.
|
|
;
|
|
; 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.
|
|
;
|
|
; sbfsFind is sbfsWalk with one thing added: the root is not a file. The two are separate
|
|
; because moving about wants the other answer - "cd /" and "cd .." both end at the root
|
|
; quite legitimately, and would have no way to say so through a routine that calls it a
|
|
; failure.
|
|
|
|
sbfsFind:
|
|
CALL sbfsWalk
|
|
BNQ sbfsFindNoWalk
|
|
|
|
; Did the walk get anywhere at all? The root is a place, but it is not a file.
|
|
SETD.0 SbfsAt
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0
|
|
OR
|
|
BRQ sbfsFindRoot
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: found.
|
|
sbfsFindNoWalk:
|
|
RET
|
|
|
|
sbfsFindRoot:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Is there a drive on the front of this path, and can we go there? ----
|
|
;
|
|
; A digit and a colon. Q is zero if the path is usable, whether or not one was there; Q is one
|
|
; if a drive was named and it is not one this machine can read, which makes the whole path
|
|
; unfindable - because it is.
|
|
;
|
|
; NAMING A DRIVE GOES THERE AND STAYS THERE. The alternative was to switch for the operation
|
|
; and switch back, which reads better in a listing and cannot work: what a path resolves to is
|
|
; a start block and a length, and those mean nothing without the drive they were read from. A
|
|
; load that resolved on drive 1 and then read on drive 0 would read the right blocks of the
|
|
; wrong disk.
|
|
sbfsPathDrive:
|
|
SETD.1 SbfsPathAt
|
|
LDD.0.1
|
|
LDA.0
|
|
INIB 0d48 ; '0'
|
|
CCF
|
|
SUB
|
|
BRC sbfsPathNoDrive ; Borrowed, so it is below '0' and not a digit.
|
|
MVQA
|
|
INIB 0d10
|
|
CCF
|
|
SUB
|
|
BNC sbfsPathNoDrive ; Ten or more, so not a digit either.
|
|
|
|
; The character after it has to be a colon, or this is a name that begins with a digit.
|
|
; DP1 is still SbfsPathAt, from the top of this routine.
|
|
LDD.0.1
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d58 ; ':'
|
|
CCF
|
|
SUB
|
|
BNQ sbfsPathNoDrive
|
|
|
|
; It is a drive. Is it one this machine has, with something readable in it?
|
|
LDD.0.1
|
|
LDA.0
|
|
INIB 0d48
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
SETD.1 SbfsPathWanted
|
|
STA.1
|
|
CALL sbfsDriveBit
|
|
MVQA
|
|
SETD.1 SbfsMounted
|
|
LDB.1
|
|
AND
|
|
BRQ sbfsPathBadDrive
|
|
|
|
SETD.1 SbfsPathWanted
|
|
LDA.1
|
|
CALL sbfsUse
|
|
|
|
; Past the digit and the colon. What follows is an ordinary path, and a bare "1:" is an
|
|
; empty one - which walks to where that drive already was.
|
|
SETD.1 SbfsPathAt
|
|
LDD.0.1
|
|
INCD.0
|
|
INCD.0
|
|
STD.0.1
|
|
|
|
sbfsPathNoDrive:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
sbfsPathBadDrive:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; The walk itself. Q is zero if the whole path was walked, and SbfsAt says where it ended -
|
|
; which may be the root, and that is an answer rather than a failure.
|
|
sbfsWalk:
|
|
SETD.1 SbfsPathAt
|
|
STD.0.1
|
|
|
|
; ---- A drive in front of the path ----
|
|
;
|
|
; Done here because this is where every path in the system arrives - eight callers between
|
|
; the shell, the config reader and this file - so naming a drive works everywhere at once
|
|
; rather than in whichever commands somebody remembered.
|
|
CALL sbfsPathDrive
|
|
BNQ sbfsWalkNoDrive
|
|
|
|
; ---- And DP0 has to be told ----
|
|
;
|
|
; sbfsPathDrive moved SbfsPathAt past the digit and the colon, but RET put DP0 back the way
|
|
; it found it - so the test below for a leading separator was reading the DIGIT and calling
|
|
; every prefixed path relative. It only showed when the drive being named was standing
|
|
; somewhere other than its root, because a relative walk from the root is an absolute one.
|
|
SETD.1 SbfsPathAt
|
|
LDD.0.1
|
|
BRI sbfsWalkPath
|
|
|
|
; A drive that this machine cannot read makes the path unfindable, because it is: there is
|
|
; nowhere for the rest of it to be.
|
|
sbfsWalkNoDrive:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
sbfsWalkPath:
|
|
|
|
; Where it starts. A path beginning with a separator is measured from the root, which is
|
|
; zero because a parent is an entry index PLUS ONE and the root is not an entry.
|
|
; Anything else is measured from wherever the machine already is.
|
|
LDA.0
|
|
INIB 0x2F
|
|
CCF
|
|
SUB
|
|
BRQ sbfsWalkFromRoot
|
|
|
|
SETD.0 SbfsCwd
|
|
SETD.1 SbfsAt
|
|
CALL sbfsCopyWord
|
|
|
|
; Where the machine is, is a directory - it could not have been got to otherwise - but
|
|
; nothing here has read its entry, so what describes it is whatever the last find left
|
|
; behind. Say the one thing about it that the walk needs, and mark the rest as not to be
|
|
; believed, exactly the way going up does.
|
|
INIA 0x02
|
|
SETD.0 SbfsFoundFlags
|
|
STA.0
|
|
RSTA
|
|
SETD.0 SbfsFieldsValid
|
|
STA.0
|
|
BRI sbfsFindName
|
|
|
|
sbfsWalkFromRoot:
|
|
SETD.0 SbfsAt
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
|
|
; Nothing described yet. An empty path comes back as the root, which is right.
|
|
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
|
|
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.
|
|
|
|
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 sbfsFindMissing ; Not a directory, so there is nothing inside it.
|
|
|
|
sbfsFindScan:
|
|
CALL sbfsScanFor
|
|
BNQ sbfsFindMissing
|
|
BRI sbfsFindName
|
|
|
|
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:
|
|
; Ending at the root is a perfectly good answer here, and there is nothing to describe.
|
|
SETD.0 SbfsAt
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0
|
|
OR
|
|
BRQ sbfsFindGood
|
|
|
|
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.
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
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
|
|
; 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.
|
|
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 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:
|
|
PSHD.2
|
|
POPD.0
|
|
DPUP.0 0d28
|
|
SETD.2 SbfsCwd
|
|
CALL sbfsSameByte
|
|
BNQ sbfsWalkHereDone
|
|
INCD.0
|
|
INCD.2
|
|
CALL sbfsSameByte
|
|
sbfsWalkHereDone:
|
|
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
|
|
|
|
; And the name, which only this way of arriving at an entry needs: walking up a chain of
|
|
; parents to write out where the machine is wants the name at every step. Copied here
|
|
; rather than in sbfsTakeEntry, because every find in the system goes through that one
|
|
; and none of them wants twenty two bytes moved on its behalf.
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d06
|
|
SETD.1 SbfsName
|
|
INIA 0d22
|
|
SETD.2 SbfsCount
|
|
STA.2
|
|
sbfsAtName:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BNA sbfsAtName
|
|
RSTA
|
|
STA.1 ; The twenty third byte, which makes it a string.
|
|
|
|
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
|
|
INCD.0
|
|
SETD.1 SbfsFileStart
|
|
CALL sbfsCopyWord
|
|
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d03
|
|
SETD.1 SbfsFileBlocks
|
|
CALL sbfsCopyWord
|
|
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d05
|
|
LDA.0
|
|
SETD.1 SbfsFileTail
|
|
STA.1
|
|
|
|
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 ----
|
|
;
|
|
; sbfsFind answers a question about one name. Listing what is on a disk is a different
|
|
; job: it needs the directory walked rather than searched. sbfsFirst starts a walk and
|
|
; sbfsNext steps it, and each of them stops on an entry that is in use, skipping the free
|
|
; ones on the way past.
|
|
;
|
|
; Q is zero while there is an entry to look at. When it is, SbfsName holds the name with
|
|
; a zero byte after it, and SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe the
|
|
; file exactly the way sbfsFind leaves them. Q is something else when the directory is
|
|
; finished, and also when a block could not be read, which are the same answer to the
|
|
; question "is there another one" and different answers to "why not".
|
|
;
|
|
; A walk keeps a directory block in SbfsBuffer between calls, so anything else that goes
|
|
; to the disk in the middle of one ends it. Take what is wanted out of an entry before
|
|
; asking the disk for anything else.
|
|
|
|
sbfsFirst:
|
|
SETD.0 SbfsDirStart
|
|
SETD.1 SbfsWalkBlock
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsDirBlocks
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 SbfsWalkLeft
|
|
STA.1 ; Only the low byte, the same as sbfsFind.
|
|
|
|
; Nothing is loaded yet. Saying the block in hand is empty makes the scan fetch the
|
|
; first one, so there is one way into a block rather than two.
|
|
RSTA
|
|
SETD.0 SbfsWalkCount
|
|
STA.0
|
|
BRI sbfsWalkScan
|
|
|
|
; The pointer and the count were stepped when the last entry was handed out, so there is
|
|
; nothing to do here but carry on looking - which is what the next label does, so this one
|
|
; falls into it rather than branching to the line below itself.
|
|
sbfsNext:
|
|
|
|
sbfsWalkScan:
|
|
; Anything left in the block in hand?
|
|
SETD.0 SbfsWalkCount
|
|
LDA.0
|
|
BNA sbfsWalkEntry
|
|
|
|
; No. Take the next directory block, if the directory has one.
|
|
SETD.0 SbfsWalkLeft
|
|
LDA.0
|
|
BRA sbfsWalkEnd
|
|
DECA
|
|
STA.0
|
|
|
|
SETD.0 SbfsWalkBlock
|
|
SETD.1 SbfsBlock
|
|
CALL sbfsCopyWord
|
|
CALL sbfsReadBlock
|
|
BRQ sbfsWalkLoaded
|
|
RET ; The read failed, and Q says so.
|
|
sbfsWalkLoaded:
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
|
|
; That one is in hand now, so the walk's block number moves on to the following one.
|
|
SETD.0 SbfsWalkBlock
|
|
CALL sbfsStepWord
|
|
|
|
; Eight entries to a block, starting at the top of the buffer.
|
|
SETD.0 SbfsBuffer
|
|
SETD.1 SbfsWalkPointer
|
|
STD.0.1
|
|
INIA 0d8
|
|
SETD.0 SbfsWalkCount
|
|
STA.0
|
|
BRI sbfsWalkScan
|
|
|
|
sbfsWalkEntry:
|
|
; Where this entry is. Kept in memory rather than in DP3, so that a walk does not
|
|
; quietly take the one pointer a caller can carry things across a call in.
|
|
SETD.1 SbfsWalkPointer
|
|
LDD.2.1
|
|
SETD.1 SbfsWalkAt
|
|
STD.2.1
|
|
|
|
; Step past it now, so that the walk has moved on whether this one is taken or skipped.
|
|
; Getting that wrong in only one of the two paths is how a walk repeats an entry
|
|
; forever, and it repeats the interesting ones rather than the boring ones.
|
|
SETD.0 SbfsWalkCount
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
PSHD.2
|
|
POPD.0
|
|
DPUP.0 0d32
|
|
SETD.1 SbfsWalkPointer
|
|
STD.0.1
|
|
|
|
; Is it a file? A free entry is not one, and there can be free entries in the middle of
|
|
; a directory, so this is a skip rather than a stop.
|
|
LDA.2
|
|
INIB 0x01
|
|
AND
|
|
BRQ sbfsWalkScan
|
|
|
|
; And is it in the directory the machine is in? The entries of every directory on the
|
|
; disk are mixed together in one array - being a child is a fact written in the child,
|
|
; not a list kept anywhere - so a walk that wants one directory has to say which, and
|
|
; everything else is stepped over on the way past.
|
|
CALL sbfsWalkHere
|
|
BNQ sbfsWalkScan
|
|
|
|
CALL sbfsWalkTake
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: here is an entry.
|
|
RET
|
|
|
|
sbfsWalkEnd:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD ; Q is one: the directory is finished.
|
|
RET
|
|
|
|
; 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
|
|
SETD.1 SbfsFileStart
|
|
CALL sbfsCopyWord
|
|
|
|
SETD.1 SbfsWalkAt
|
|
LDD.0.1
|
|
DPUP.0 0d03
|
|
SETD.1 SbfsFileBlocks
|
|
CALL sbfsCopyWord
|
|
|
|
SETD.1 SbfsWalkAt
|
|
LDD.0.1
|
|
DPUP.0 0d05
|
|
LDA.0
|
|
SETD.1 SbfsFileTail
|
|
STA.1
|
|
|
|
; The name. Twenty two bytes of it, padded with zeroes rather than terminated, so it is
|
|
; copied into a buffer with a twenty third byte that nothing ever writes. That byte is
|
|
; what makes a name that fills the field into a string that can be printed.
|
|
SETD.1 SbfsWalkAt
|
|
LDD.0.1
|
|
DPUP.0 0d06
|
|
SETD.1 SbfsName
|
|
INIA 0d22
|
|
SETD.2 SbfsCount
|
|
STA.2
|
|
sbfsWalkName:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
SETD.2 SbfsCount
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BNA sbfsWalkName
|
|
RET
|
|
|
|
; Compares the name in the entry at DP2 with the one kept in SbfsWanted. Q is zero if
|
|
; they are the same. Names are padded with zeroes rather than terminated, so a name that
|
|
; fills the field has no terminator to look for, which is why the count is what stops it.
|
|
; A COUNTER OF ITS OWN, and that matters more than it looks. sbfsFind counts the eight
|
|
; entries of a block in SbfsCount and calls this for each one; when this used SbfsCount too,
|
|
; a failed comparison left the ENTRY count holding however far the NAME comparison had got.
|
|
; A name differing at its first byte left 22 there, so the search walked twenty two entries
|
|
; through a buffer holding eight - off the end of it, into whatever data happened to follow,
|
|
; and reported a match against rubbish. What followed the buffer decided whether it looked
|
|
; like it worked, which is why it went unnoticed until unrelated data was added.
|
|
sbfsMatchEntry:
|
|
PSHD.2
|
|
POPD.0
|
|
DPUP.0 0d06 ; The name inside the entry.
|
|
SETD.2 SbfsWanted
|
|
INIA 0d22
|
|
SETD.1 SbfsMatchLeft
|
|
STA.1
|
|
|
|
sbfsMatchLoop:
|
|
CALL sbfsSameByte
|
|
BRQ sbfsMatchSame
|
|
RET ; They differ, and Q is the difference.
|
|
sbfsMatchSame:
|
|
LDA.0
|
|
BRA sbfsMatchYes ; Both ended in the same place.
|
|
INCD.0
|
|
INCD.2
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsMatchLoop ; Twenty two the same is the same name, so falling out is a match.
|
|
|
|
sbfsMatchYes:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
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:
|
|
INIA 0d22
|
|
SETD.2 SbfsCount
|
|
STA.2
|
|
RSTB ; B stays zero once the name has ended.
|
|
sbfsKeepLoop:
|
|
LDA.0
|
|
BRA sbfsKeepPad
|
|
STA.1
|
|
INCD.0
|
|
BRI sbfsKeepStep
|
|
sbfsKeepPad:
|
|
STB.1
|
|
sbfsKeepStep:
|
|
INCD.1
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BNA sbfsKeepLoop
|
|
sbfsKeepDone:
|
|
RET
|
|
|
|
; Reads the file that sbfsFind found into Data Memory at DP1. Q is zero if it worked.
|
|
;
|
|
; Whole blocks are copied whole, including the last one, so the bytes after the end of a
|
|
; file are whatever else was in that block on the disk. SbfsFileTail says where the file
|
|
; actually stops, and it is the caller's business to respect it.
|
|
;
|
|
; A file of more than 255 blocks is not handled here. That is 64K, which is the whole of
|
|
; Data Memory, so it could not be read into it anyway.
|
|
sbfsRead:
|
|
PSHD.1
|
|
POPD.3 ; Where it goes. DP3 survives a CALL, and nothing called here
|
|
; touches it.
|
|
SETD.0 SbfsFileStart
|
|
SETD.1 SbfsBlock
|
|
CALL sbfsCopyWord
|
|
|
|
SETD.0 SbfsFileBlocks
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 SbfsLeft
|
|
STA.1
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
BRA sbfsReadCounted ; No part block on the end.
|
|
SETD.0 SbfsLeft
|
|
LDA.0
|
|
INCA
|
|
STA.0 ; One more block for the tail.
|
|
|
|
sbfsReadCounted:
|
|
SETD.0 SbfsLeft
|
|
LDA.0
|
|
BRA sbfsReadDone ; An empty file has nothing to read.
|
|
|
|
sbfsReadLoop:
|
|
CALL sbfsReadBlock
|
|
BRQ sbfsReadGot
|
|
RET ; The read failed, and Q says so.
|
|
sbfsReadGot:
|
|
PSHD.3
|
|
POPD.1
|
|
CALL sbfsBufferOut
|
|
|
|
; On by a whole block. DPUP carries one byte at a time, so 256 takes two of them.
|
|
DPUP.3 0xFF
|
|
INCD.3
|
|
SETD.0 SbfsBlock
|
|
CALL sbfsStepWord
|
|
|
|
SETD.0 SbfsLeft
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA sbfsReadLoop
|
|
|
|
sbfsReadDone:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: read.
|
|
RET
|
|
|
|
; Reads one block of the file sbfsFind found, the one SbfsIndex names counting from zero,
|
|
; into Data Memory at DP1. Q is zero if it worked.
|
|
;
|
|
; This is the whole of streaming: a file too big to hold is read a block at a time by
|
|
; asking for each in turn, and nothing has to be kept between the calls but the number.
|
|
; sbfsRead is what this would be if it were called in a loop, which is why the two look
|
|
; alike; it stays as it is because reading a whole small file is what most callers want
|
|
; and doing it in one call is both shorter and faster.
|
|
;
|
|
; The whole block comes across, including a last one that the file only partly fills, so
|
|
; the bytes past the end of it are whatever else was on the disk there. SbfsFileTail says
|
|
; where the file stops and it is the caller's business to respect it, the same bargain
|
|
; sbfsRead offers.
|
|
;
|
|
; An index past the end of the file is not caught here. It reads whatever block that
|
|
; works out to, which is somebody else's file or free space; the caller knows how many
|
|
; blocks there are, because sbfsFileExtent tells it.
|
|
sbfsReadOne:
|
|
PSHD.1
|
|
POPD.3 ; Where it goes. DP3 is the one pointer a CALL does not put back.
|
|
|
|
SETD.0 SbfsBlock
|
|
SETD.2 SbfsFileStart
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsBlock
|
|
SETD.2 SbfsIndex
|
|
CALL sbfsAddWord
|
|
|
|
; ---- READ AHEAD ----
|
|
;
|
|
; Is this block already coming? It will be if the last time through asked for the one
|
|
; before it, which is what reading a file front to back does every time. Then there is
|
|
; nothing to start: the disk has been fetching it while the caller was busy with the last
|
|
; one, and all that is left is to wait for it.
|
|
;
|
|
; This is the path a search is NOT. A directory scan stops the moment it matches, so
|
|
; asking for the next block there is asking for one nobody will look at - and it was
|
|
; nineteen per cent slower for exactly that reason. A file is read to its end, so every
|
|
; block asked for early is one that would have been asked for anyway.
|
|
SETD.0 SbfsBufferKnown
|
|
LDA.0
|
|
BRA sbfsReadOneFresh
|
|
SETD.0 SbfsBufferBlock
|
|
SETD.2 SbfsBlock
|
|
CALL sbfsCompareWord
|
|
BNQ sbfsReadOneFresh
|
|
|
|
RCAL sbfsWaitDisk ; Already on its way, so only the waiting is left.
|
|
INIB 0x02
|
|
AND
|
|
BNQ sbfsReadOneDone
|
|
BRI sbfsReadOneHere
|
|
|
|
sbfsReadOneFresh:
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsReadOneDone ; The read failed, and Q says so.
|
|
|
|
sbfsReadOneHere:
|
|
PSHD.3
|
|
POPD.1
|
|
CALL sbfsBufferOut
|
|
|
|
; And the one after it, started now, if the file has one. Bounded by the file's own
|
|
; length so that reading the last block does not fetch whatever follows the file on the
|
|
; disk - which belongs to somebody else and would be paid for twice: once to fetch and
|
|
; once to throw away.
|
|
; IN A PLACE OF ITS OWN, not in SbfsIndex. What block was asked for is the caller's, and
|
|
; it is still wanted after this returns - handleFileBlock compares it against the file's
|
|
; length to work out whether this was the short last block. Stepping it here made every
|
|
; block report the wrong number of bytes.
|
|
CALL sbfsFileExtent
|
|
SETD.0 SbfsAheadIndex
|
|
SETD.2 SbfsIndex
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsAheadIndex
|
|
CALL sbfsStepWord
|
|
SETD.0 SbfsAheadIndex
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsCompareWord
|
|
BNC sbfsReadOneLast ; The next one is past the end of the file.
|
|
SETD.0 SbfsBlock
|
|
CALL sbfsStepWord
|
|
RCAL sbfsStartRead
|
|
sbfsReadOneLast:
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: read.
|
|
sbfsReadOneDone:
|
|
RET
|
|
|
|
; ---- Writing one block of a file ----
|
|
;
|
|
; The other half of sbfsReadOne, and deliberately the same shape. DP1 is where the block
|
|
; comes FROM and SbfsIndex says which block of the file it is, counting from zero. The file
|
|
; is whichever one the last find landed on, exactly as it is for reading.
|
|
;
|
|
; THE INDEX IS CHECKED AGAINST THE FILE'S LENGTH, and that check is not politeness. Files
|
|
; are laid down contiguously, so block N of a five block file is a real block belonging to
|
|
; whatever happens to sit five blocks along - and writing it would put one file's bytes
|
|
; inside another with nothing anywhere saying so. Reading past the end is a wrong answer;
|
|
; writing past it is somebody else's file.
|
|
sbfsWriteOne:
|
|
PSHD.1
|
|
POPD.3 ; Where it comes from. DP3 survives the calls below.
|
|
|
|
CALL sbfsFileExtent ; How many blocks the file really occupies.
|
|
SETD.0 SbfsIndex
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsCompareWord
|
|
BNC sbfsWriteOneNo ; The index is not below the count, so it is past the end.
|
|
|
|
SETD.0 SbfsBlock
|
|
SETD.2 SbfsFileStart
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsBlock
|
|
SETD.2 SbfsIndex
|
|
CALL sbfsAddWord
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
RET ; Q is whatever the write said.
|
|
|
|
sbfsWriteOneNo:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Writing ----
|
|
|
|
; Where the first block that can hold a file is: past the superblock and the directory.
|
|
sbfsFirstData:
|
|
SETD.0 SbfsCandidate
|
|
SETD.2 SbfsDirStart
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsCandidate
|
|
SETD.2 SbfsDirBlocks
|
|
CALL sbfsAddWord
|
|
RET
|
|
|
|
; SbfsEntryStart and SbfsEntryEnd describe the file in the entry at DP2. The end is one
|
|
; past the last block it holds, and a file of nothing holds none, so its end is its start.
|
|
sbfsEntryBounds:
|
|
PSHD.2
|
|
POPD.3
|
|
PSHD.3
|
|
POPD.0
|
|
INCD.0
|
|
SETD.1 SbfsEntryStart
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsEntryEnd
|
|
SETD.2 SbfsEntryStart
|
|
CALL sbfsSetWord
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d03
|
|
SETD.1 SbfsScratch
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsEntryEnd
|
|
SETD.2 SbfsScratch
|
|
CALL sbfsAddWord
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d05
|
|
LDA.0
|
|
BRA sbfsBoundsDone ; No part block on the end.
|
|
SETD.0 SbfsEntryEnd
|
|
SETD.2 SbfsOne
|
|
CALL sbfsAddWord
|
|
sbfsBoundsDone:
|
|
RET
|
|
|
|
; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart.
|
|
; Q is zero if there was room.
|
|
;
|
|
; First fit, walking the directory, because with files laid down contiguously the
|
|
; directory already says which blocks are spoken for. There is no allocation table to
|
|
; consult and none to keep right.
|
|
;
|
|
; ---- Moving the candidate along without starting again ----
|
|
;
|
|
; This used to give up the moment it found something in the way: it moved the candidate past
|
|
; that one entry and STARTED THE DIRECTORY AGAIN FROM THE FIRST BLOCK. With files laid down
|
|
; one after another that is a restart per file, and every restart reads directory blocks off
|
|
; the disk until it reaches the next one in the way - which is further in each time. A disk
|
|
; of 183 files cost thousands of block reads to place one file, and assembling onto a disk
|
|
; with the whole source tree on it took eleven minutes with nearly all of it spent here.
|
|
;
|
|
; The candidate moves along DURING the pass now, and the pass carries on from where it is.
|
|
; Entries later in the directory are then tested against where the candidate has got to, so
|
|
; on a disk that has been appended to - which is what a disk mostly is - one pass walks it
|
|
; past everything and the next confirms there is nothing left in the way. Two passes rather
|
|
; than one per file.
|
|
;
|
|
; IT IS STILL FIRST FIT. The candidate only ever moves past something that genuinely
|
|
; overlaps it, and when it does there is nowhere below that could have held the run: the
|
|
; entry in the way covers everything up to its end, and it begins before the candidate ends.
|
|
; So nothing is skipped that first fit would have found - and because entries earlier in the
|
|
; directory were tested against an earlier candidate, the pass repeats until one goes by with
|
|
; the candidate standing still.
|
|
sbfsAllocate:
|
|
CALL sbfsFirstData
|
|
|
|
sbfsAllocTry:
|
|
; The candidate has not moved yet this time round.
|
|
SETD.0 SbfsAllocMoved
|
|
RSTA
|
|
STA.0
|
|
|
|
SETD.0 SbfsCandEnd
|
|
SETD.2 SbfsCandidate
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsCandEnd
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsAddWord
|
|
|
|
; Past the end of the disk means there is nowhere left to look.
|
|
SETD.0 SbfsDiskBlocks
|
|
SETD.2 SbfsCandEnd
|
|
CALL sbfsCompareWord
|
|
BRC sbfsAllocNoRoom ; The disk is smaller than where this run would end.
|
|
|
|
SETD.0 SbfsDirStart
|
|
SETD.1 SbfsBlock
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsDirBlocks
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 SbfsLeft
|
|
STA.1
|
|
|
|
sbfsAllocBlock:
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsAllocFailed
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.2 SbfsBuffer
|
|
INIA 0d8
|
|
SETD.1 SbfsCount
|
|
STA.1
|
|
|
|
sbfsAllocEntry:
|
|
LDA.2
|
|
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
|
|
; Stack for the duration. Working it out again afterwards was the first way this was
|
|
; written, and it was both longer and wrong.
|
|
PSHD.2
|
|
|
|
; Two runs overlap when each begins before the other ends.
|
|
SETD.0 SbfsCandidate
|
|
SETD.2 SbfsEntryEnd
|
|
CALL sbfsCompareWord
|
|
BNC sbfsAllocClear
|
|
SETD.0 SbfsEntryStart
|
|
SETD.2 SbfsCandEnd
|
|
CALL sbfsCompareWord
|
|
BNC sbfsAllocClear
|
|
|
|
; They do overlap, so the candidate moves to the far end of what is in the way - and the
|
|
; scan carries on from here rather than beginning again, so whatever comes next is measured
|
|
; against where the candidate has got to.
|
|
;
|
|
; WHERE THIS ENTRY IS STAYS ON THE STACK THROUGHOUT. The work below wants DP2 for its own
|
|
; purposes and the scan needs it back on the entry to step to the next one, which is what
|
|
; sbfsAllocClear is for.
|
|
SETD.0 SbfsCandidate
|
|
SETD.2 SbfsEntryEnd
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsCandEnd
|
|
SETD.2 SbfsCandidate
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsCandEnd
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsAddWord
|
|
INIA 0x01
|
|
SETD.0 SbfsAllocMoved
|
|
STA.0
|
|
; And on into sbfsAllocClear, which puts the entry back in DP2 and steps to the next one.
|
|
|
|
sbfsAllocClear:
|
|
POPD.2
|
|
|
|
sbfsAllocNext:
|
|
DPUP.2 0d32
|
|
SETD.1 SbfsCount
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsAllocEntry
|
|
|
|
SETD.0 SbfsBlock
|
|
CALL sbfsStepWord
|
|
SETD.1 SbfsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsAllocBlock
|
|
|
|
; ---- The pass is over ----
|
|
;
|
|
; Something was in the way, so start again from the far end of the furthest of them. That
|
|
; is one jump for however many files the candidate ran into, rather than one jump each.
|
|
SETD.0 SbfsAllocMoved
|
|
LDA.0
|
|
BRA sbfsAllocRoom ; It never moved, so nothing is in the way of where it is.
|
|
BRI sbfsAllocTry
|
|
|
|
sbfsAllocRoom:
|
|
; Nothing was in the way, so this is where it goes.
|
|
SETD.0 SbfsFileStart
|
|
SETD.2 SbfsCandidate
|
|
CALL sbfsSetWord
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
sbfsAllocNoRoom:
|
|
sbfsAllocFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; The two byte number at DP0 becomes the one at DP2. Written destination first, so that a
|
|
; call reads the way an assignment does. sbfsCopyWord goes the other way, source first,
|
|
; which is a difference worth keeping in mind: getting it backwards here quietly destroyed
|
|
; a value that had just been worked out.
|
|
sbfsSetWord:
|
|
LDA.2
|
|
STA.0
|
|
INCD.2
|
|
INCD.0
|
|
LDA.2
|
|
STA.0
|
|
RET
|
|
|
|
; The two byte number at DP0 becomes itself less the one at DP2.
|
|
sbfsSubWord:
|
|
INCD.0
|
|
INCD.2
|
|
LDA.0
|
|
LDB.2
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
STA.0
|
|
DECD.0
|
|
DECD.2
|
|
LDA.0
|
|
LDB.2
|
|
SUB ; Borrows in from the low half.
|
|
MVQA
|
|
STA.0
|
|
RET
|
|
|
|
; How many blocks a file of SbfsFileBlocks and SbfsFileTail actually occupies, into
|
|
; SbfsWantBlocks. A file of nothing occupies none.
|
|
sbfsFileExtent:
|
|
SETD.0 SbfsWantBlocks
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
BRA sbfsExtentDone
|
|
SETD.0 SbfsWantBlocks
|
|
SETD.2 SbfsOne
|
|
CALL sbfsAddWord
|
|
sbfsExtentDone:
|
|
RET
|
|
|
|
; Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is,
|
|
; 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:
|
|
; 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:
|
|
; A plain file, unless somebody came in at the other door. Set here rather than left
|
|
; over from last time, so that one temporary does not make the next ordinary file one.
|
|
INIA 0x01
|
|
SETD.0 SbfsMakeFlags
|
|
STA.0
|
|
BRI sbfsCreateGo
|
|
|
|
; The same, for the half written file a save puts down before it dares touch the original.
|
|
; It is an ordinary entry in every other way - it holds blocks and answers to a name - and
|
|
; the flag is the whole of what says it is not finished. See sbfsSaveFile.
|
|
sbfsCreateTempAt:
|
|
INIA 0x05
|
|
SETD.0 SbfsMakeFlags
|
|
STA.0
|
|
|
|
sbfsCreateGo:
|
|
CALL sbfsFileExtent
|
|
CALL sbfsAllocate
|
|
BNQ sbfsCreateFailed
|
|
|
|
; Walk the directory for an entry nobody is using.
|
|
SETD.0 SbfsDirStart
|
|
SETD.1 SbfsBlock
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsDirBlocks
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 SbfsLeft
|
|
STA.1
|
|
|
|
sbfsCreateBlock:
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsCreateFailed
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.2 SbfsBuffer
|
|
INIA 0d8
|
|
SETD.1 SbfsCount
|
|
STA.1
|
|
|
|
sbfsCreateEntry:
|
|
LDA.2
|
|
INIB 0x01
|
|
AND
|
|
BRQ sbfsCreateFill ; This one is free.
|
|
DPUP.2 0d32
|
|
SETD.1 SbfsCount
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsCreateEntry
|
|
|
|
SETD.0 SbfsBlock
|
|
CALL sbfsStepWord
|
|
SETD.1 SbfsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsCreateBlock
|
|
|
|
sbfsCreateFull:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD ; Every entry is taken.
|
|
RET
|
|
|
|
sbfsCreateFill:
|
|
; DP2 is on the entry. Fill it in, then put the whole block back on the disk.
|
|
PSHD.2
|
|
POPD.3
|
|
SETD.0 SbfsMakeFlags
|
|
LDA.0
|
|
STA.3 ; In use, and possibly not finished being written.
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
INCD.1
|
|
SETD.0 SbfsFileStart
|
|
CALL sbfsCopyWord
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d03
|
|
SETD.0 SbfsFileBlocks
|
|
CALL sbfsCopyWord
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d05
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
STA.1
|
|
|
|
; 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
|
|
SETD.0 SbfsAt
|
|
CALL sbfsCopyWord
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d06
|
|
SETD.0 SbfsWanted
|
|
INIA 0d22
|
|
SETD.2 SbfsCount
|
|
STA.2
|
|
sbfsCreateName:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BNA sbfsCreateName
|
|
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsCreateFailed
|
|
|
|
; The free count is a note rather than the truth, but it should still be kept right.
|
|
RSTA
|
|
SETD.0 SbfsBlock
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsCreateFailed
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d12
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsSubWord
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
RET
|
|
|
|
sbfsCreateFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Writes a file that sbfsCreate made, from Data Memory at DP1. Q is zero if it worked.
|
|
;
|
|
; Whole blocks go out whole, so whatever follows the file in Data Memory ends up in the
|
|
; unused tail of its last block. That is harmless, since the tail in the entry says where
|
|
; the file stops, but it does mean a block can hold a little of whatever was next to it.
|
|
sbfsWriteFile:
|
|
PSHD.1
|
|
POPD.3
|
|
SETD.0 SbfsBlock
|
|
SETD.2 SbfsFileStart
|
|
CALL sbfsSetWord
|
|
CALL sbfsFileExtent
|
|
SETD.0 SbfsWantBlocks
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 SbfsLeft
|
|
STA.1
|
|
BRA sbfsWriteDone ; A file of nothing has nothing to write.
|
|
|
|
sbfsWriteLoop:
|
|
PSHD.3
|
|
POPD.1
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsWriteFailed
|
|
DPUP.3 0xFF
|
|
INCD.3
|
|
SETD.0 SbfsBlock
|
|
CALL sbfsStepWord
|
|
SETD.0 SbfsLeft
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA sbfsWriteLoop
|
|
|
|
sbfsWriteDone:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
sbfsWriteFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Copies a whole block from Data Memory at DP1 into the disk's buffer, which is the other
|
|
; way round from sbfsBufferOut.
|
|
sbfsBufferIn:
|
|
INIA 0d1
|
|
OUTA 0xE0
|
|
PSHD.1
|
|
POPA
|
|
POPB
|
|
OUTB 0xE1
|
|
OUTA 0xE2
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
RSTA
|
|
OUTA 0xE4
|
|
OUTA 0xE5
|
|
INIA 0x01
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7
|
|
INIA 0x01
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; Puts the disk's buffer down as the block named by SbfsBlock. Q is zero if it worked.
|
|
sbfsWriteBlock:
|
|
RCAL sbfsWaitDisk ; Anything still going finishes before this asks for more.
|
|
RCAL sbfsForgetBuffer
|
|
SETD.0 SbfsBlock
|
|
LDA.0
|
|
OUTA 0x20
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x21
|
|
INIA 0x02
|
|
OUTA 0x22
|
|
RCAL sbfsWaitDisk
|
|
INIB 0x02
|
|
AND
|
|
RET
|
|
|
|
; Reads the block named by SbfsBlock into the disk's buffer. Q is zero if it worked.
|
|
sbfsReadBlock:
|
|
RCAL sbfsStartRead
|
|
RCAL sbfsWaitDisk
|
|
INIB 0x02
|
|
AND ; Q is the error bit, so zero means it worked.
|
|
RET
|
|
|
|
; ---- Asking for a block without waiting for it ----
|
|
;
|
|
; The half of a read that costs nothing, and the whole of what makes reading ahead
|
|
; possible. Whatever the disk was still doing is collected first, because a controller
|
|
; given a command while it is busy has no good answer - so this is the one place that
|
|
; guarantees it is not, and every read goes through it.
|
|
;
|
|
; WHICH BLOCK IS COMING IS WRITTEN DOWN, because the disk has one buffer and the only way
|
|
; to know what is in it is to remember what was last asked for. Anything that fills the
|
|
; buffer some other way must say so by clearing it, or a later read would blit whatever
|
|
; happened to be there and call it the block it wanted.
|
|
sbfsStartRead:
|
|
RCAL sbfsWaitDisk
|
|
SETD.0 SbfsBlock
|
|
SETD.1 SbfsBufferBlock
|
|
CALL sbfsCopyWord
|
|
INIA 0x01
|
|
SETD.0 SbfsBufferKnown
|
|
STA.0
|
|
|
|
SETD.0 SbfsBlock
|
|
LDA.0
|
|
OUTA 0x20
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x21
|
|
INIA 0x01
|
|
OUTA 0x22
|
|
RRET
|
|
|
|
; The buffer is about to hold something this cannot describe.
|
|
sbfsForgetBuffer:
|
|
RSTA
|
|
SETD.0 SbfsBufferKnown
|
|
STA.0
|
|
RRET
|
|
|
|
; ---- Waiting for the disk ----
|
|
;
|
|
; The status port has a bit that means the operation is still going, and until now nothing
|
|
; here looked at it: the answer was always there before the next instruction was, so asking
|
|
; would have been asking about something that could not happen. A disk that takes any time
|
|
; at all makes it real, and a program that does not wait reads the block BEFORE the one it
|
|
; asked for - which is not an error anywhere, just quietly the wrong bytes.
|
|
;
|
|
; A REACHES THE CALLER, which is why this is RCAL and not CALL. What comes back is the
|
|
; settled status, and CALL would put A back the way it found it - so the one thing this
|
|
; exists to hand over is the one thing an ordinary call cannot carry. Two bytes of Stack
|
|
; instead of ten, as well, in a routine that runs on every block the machine ever touches.
|
|
; ASKED FIRST, THEN WAITED ON, and that order is the whole of what makes this safe. The
|
|
; disk raises its line when it finishes, so if it finished between the test and the WAIT
|
|
; the line is already standing and the WAIT does nothing rather than sleeping through the
|
|
; answer. Testing after waiting would have exactly the opposite property.
|
|
;
|
|
; No handler and no vector: the shell keeps the Interrupt Flag down, and a WAIT wakes on a
|
|
; line whether or not anybody is going to answer it. It takes the line down on the way
|
|
; past, which is what stops the next wait finding this one's line and returning at once.
|
|
sbfsWaitDisk:
|
|
INA 0x23
|
|
INIB 0x01
|
|
AND
|
|
BRQ sbfsWaitDone ; The busy bit is down, so there is nothing to wait for.
|
|
WAIT
|
|
BRI sbfsWaitDisk
|
|
sbfsWaitDone:
|
|
RRET ; A is the status, with the busy bit down: AND leaves A alone.
|
|
|
|
; Copies the disk's buffer, a whole block of it, to Data Memory at DP1.
|
|
sbfsBufferOut:
|
|
INIA 0d3
|
|
OUTA 0xE0
|
|
RSTA
|
|
OUTA 0xE1
|
|
OUTA 0xE2
|
|
INIA 0d1
|
|
OUTA 0xE3
|
|
PSHD.1
|
|
POPA
|
|
POPB
|
|
OUTB 0xE4
|
|
OUTA 0xE5
|
|
INIA 0x01
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7 ; A whole block.
|
|
INIA 0x01
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; Two bytes from DP0 to DP1, most significant first, the way every number on a SplitBit
|
|
; disk is stored.
|
|
sbfsCopyWord:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.0
|
|
STA.1
|
|
RET
|
|
|
|
; Adds one to the two byte number at DP0.
|
|
sbfsStepWord:
|
|
INCD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
BNC sbfsStepDone ; It did not wrap, so the high byte is untouched.
|
|
DECD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
sbfsStepDone:
|
|
RET
|
|
|
|
; The two byte number at DP0 becomes itself plus the one at DP2. Only memory changes, so
|
|
; it survives the return: a routine cannot hand back a pointer or a register.
|
|
sbfsAddWord:
|
|
INCD.0
|
|
INCD.2
|
|
LDA.0
|
|
LDB.2
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
STA.0
|
|
DECD.0
|
|
DECD.2
|
|
LDA.0
|
|
LDB.2
|
|
ADD ; Carries in from the low half. Nothing between the two touches it.
|
|
MVQA
|
|
STA.0
|
|
RET
|
|
|
|
; Compares the two byte number at DP0 with the one at DP2. Q is zero if they are equal,
|
|
; and the Carry Flag is set if the one at DP0 is the smaller. Both come back, because
|
|
; neither Q nor the Status register is put back by a return.
|
|
sbfsCompareWord:
|
|
LDA.0
|
|
LDB.2
|
|
CCF
|
|
SUB ; The high bytes settle it unless they are the same.
|
|
BNQ sbfsCompareDone
|
|
INCD.0
|
|
INCD.2
|
|
LDA.0
|
|
LDB.2
|
|
CCF
|
|
SUB
|
|
sbfsCompareDone:
|
|
RET
|
|
|
|
; The byte at DP0 against the byte at DP2. Q is zero if they are the same.
|
|
sbfsSameByte:
|
|
LDA.0
|
|
LDB.2
|
|
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.
|
|
;
|
|
; A deleted entry and one that was never used are the same thing, which is the whole of
|
|
; what deleting is here: the entry is zeroed and its blocks stop being spoken for. THE
|
|
; BLOCKS THEMSELVES ARE LEFT EXACTLY AS THEY WERE, so what was in a file is still on the
|
|
; disk until something is put over the top of it. Worth knowing if anything is ever meant
|
|
; to be private. The host tool does the same, so the two agree about what a deleted disk
|
|
; looks like.
|
|
;
|
|
; Nothing is compacted. Deleting leaves a hole, and because files are contiguous a hole is
|
|
; only usable by something that fits inside it. That is the price of the directory being
|
|
; the whole allocation map, and tidying it up is an ordinary program somebody can write
|
|
; rather than anything the format has to say.
|
|
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
|
|
|
|
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
|
|
|
|
; sbfsFind leaves DP3 on the entry and SbfsBlock on the directory block it came out of,
|
|
; which is everything needed to change it and put it back.
|
|
PSHD.3
|
|
POPD.0
|
|
INIB 0d32
|
|
sbfsDeleteWipe:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BNB sbfsDeleteWipe
|
|
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsDeleteFailed
|
|
|
|
; And the free count goes back up. It is a note rather than the truth, but a note worth
|
|
; keeping right.
|
|
RSTA
|
|
SETD.0 SbfsBlock
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsDeleteFailed
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d12
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsAddWord
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
RET
|
|
|
|
sbfsDeleteFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Renaming ----
|
|
;
|
|
; 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
|
|
; away, then write the new one - loses the lot if there turns out to be nowhere to put it.
|
|
; Renaming is the cheapest thing this filesystem can do and the only one that can be left
|
|
; until last, so it is what the order is built around.
|
|
sbfsRename:
|
|
; Where the old name is, kept somewhere that finding things will not tread on.
|
|
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 sbfsCopyName
|
|
|
|
; 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
|
|
LDD.0.2
|
|
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.
|
|
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.
|
|
SETD.0 SbfsNewName
|
|
INIA 0d22
|
|
SETD.2 SbfsCount
|
|
STA.2
|
|
sbfsRenameName:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BNA sbfsRenameName
|
|
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
RET
|
|
|
|
sbfsRenameFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Writing a file a block at a time ----
|
|
;
|
|
; The mirror of reading one, and it needs something reading does not: state. A read is a
|
|
; whole question in itself - here is a name and an index, hand me that block - but a safe
|
|
; write cannot be, because the new file has to exist somewhere before the old one is thrown
|
|
; away, and something has to remember which temporary belongs to which name between one
|
|
; block and the next.
|
|
;
|
|
; ONE WRITE IS OPEN AT A TIME AND COSMOS HOLDS IT, rather than the program being handed
|
|
; something to keep. The careful order below is the one sbfsSaveFile uses and it is not
|
|
; obvious; leaving it to each program that streams would mean every one of them getting it
|
|
; right separately, and the cost of getting it wrong is somebody's file.
|
|
;
|
|
; sbfsStreamStart DP0 names it, SbfsFileBlocks and SbfsFileTail say how big
|
|
; sbfsStreamWrite DP1 is the block, SbfsIndex says which one it is
|
|
; sbfsStreamDone the temporary takes the name, and the old file goes
|
|
;
|
|
; NOTHING THAT ALREADY EXISTS IS TOUCHED UNTIL THE LAST OF THOSE. The room for the whole
|
|
; file is taken at the start, so a disk that cannot hold it says so while the old one is
|
|
; still there - which is better than the order sbfsSaveFile has to use, where the size is
|
|
; only known once the caller has the bytes in hand.
|
|
|
|
sbfsStreamStart:
|
|
; WHERE THE NAME IS, KEPT FIRST OF ALL. DP0 holds it on the way in and everything below
|
|
; wants DP0 for something else, so the pointer has to be put somewhere before the first
|
|
; of those - not after, which is a routine that walks whatever it last happened to point
|
|
; at and reports that it could find no room.
|
|
SETD.2 SbfsStreamPath
|
|
STD.0.2
|
|
|
|
; The size, put aside before anything walks the disk. Finding things overwrites the place
|
|
; a size is normally said, because finding describes whatever it last looked at.
|
|
SETD.0 SbfsStreamBlocks
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
SETD.1 SbfsStreamTail
|
|
STA.1
|
|
|
|
RSTA
|
|
SETD.0 SbfsStreamOpen
|
|
STA.0 ; Not open until it is.
|
|
|
|
SETD.2 SbfsStreamPath
|
|
LDD.0.2
|
|
CALL sbfsWalkParent
|
|
BNQ sbfsStreamNo
|
|
SETD.0 SbfsAt
|
|
SETD.1 SbfsStreamParent
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsWanted
|
|
SETD.1 SbfsStreamLeaf
|
|
CALL sbfsCopyName
|
|
|
|
; Refused if that name belongs to a directory, and refused now rather than at the end
|
|
; with a written temporary nothing would ever come back for.
|
|
CALL sbfsStreamWhere
|
|
CALL sbfsScanFor
|
|
BNQ sbfsStreamFresh
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BNQ sbfsStreamNo
|
|
sbfsStreamFresh:
|
|
|
|
; A temporary left by a stream that did not finish would be in the way, so it goes - but
|
|
; only if it really is one. See sbfsSaveFile: the flag says so and the name does not,
|
|
; because "sbfs.out" is a name somebody may have chosen for themselves.
|
|
CALL sbfsStreamTemp
|
|
CALL sbfsScanFor
|
|
BNQ sbfsStreamNoTemp
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x04
|
|
AND
|
|
BRQ sbfsStreamNo ; Somebody's own file, under a name we wanted. Left alone.
|
|
CALL sbfsWipeFound
|
|
sbfsStreamNoTemp:
|
|
|
|
; And the room for all of it, taken while the old file is still safe.
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 SbfsStreamBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsStreamTail
|
|
LDA.0
|
|
SETD.1 SbfsFileTail
|
|
STA.1
|
|
|
|
CALL sbfsStreamTemp
|
|
CALL sbfsCreateTempAt
|
|
BNQ sbfsStreamNo
|
|
|
|
; WHERE THE TEMPORARY BEGINS, KEPT NOW. Nothing moves a file once it is made, so every
|
|
; block after this one can be written without looking it up again. Finding it each time
|
|
; worked and was not even slow - a scan stops the moment it matches, and a temporary
|
|
; lands in an early slot - but it is a walk of the directory per block for an answer that
|
|
; cannot have changed, and on a disk whose early entries are all taken it would be a walk
|
|
; of the whole thing.
|
|
SETD.0 SbfsStreamAt
|
|
SETD.2 SbfsFileStart
|
|
CALL sbfsSetWord
|
|
|
|
; ---- And which disk all of that is on ----
|
|
;
|
|
; A write stream is the one thing here that lives across service calls, so it is the one
|
|
; thing that can have the drive changed underneath it. Copying between two disks is exactly
|
|
; that: every osFileBlock re-resolves the SOURCE path and goes to its drive, and then
|
|
; osFileWrite has to come back here. The path was walked above, so the drive is right now.
|
|
INA 0x24
|
|
SETD.0 SbfsStreamDrive
|
|
STA.0
|
|
|
|
INIA 0x01
|
|
SETD.0 SbfsStreamOpen
|
|
STA.0
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Back to the disk the open stream belongs to, whatever has been read in between.
|
|
sbfsStreamHere:
|
|
SETD.0 SbfsStreamDrive
|
|
LDA.0
|
|
CALL sbfsUse
|
|
RET
|
|
|
|
sbfsStreamNo:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; DP1 is the block and SbfsIndex says which one. The temporary is found again each time,
|
|
; because everything in between has been reading other blocks over the one it lives in.
|
|
sbfsStreamWrite:
|
|
SETD.0 SbfsStreamOpen
|
|
LDA.0
|
|
BRA sbfsStreamNo
|
|
CALL sbfsStreamHere
|
|
|
|
; Where the file is and how big it is, said again rather than looked up: all three were
|
|
; settled when the temporary was made, and everything since has been describing whatever
|
|
; it last looked at.
|
|
SETD.0 SbfsFileStart
|
|
SETD.2 SbfsStreamAt
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 SbfsStreamBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsStreamTail
|
|
LDA.0
|
|
SETD.2 SbfsFileTail
|
|
STA.2
|
|
|
|
CALL sbfsWriteOne
|
|
RET
|
|
|
|
; DP1 is where the block should go and SbfsIndex says which one. The other direction of
|
|
; sbfsStreamWrite, and the thing that lets a writer keep only ONE block of a file in hand:
|
|
; anything writing two parts of a file at once has to be able to put a block down, go
|
|
; somewhere else, and pick it up again where it left off.
|
|
sbfsStreamFetch:
|
|
SETD.0 SbfsStreamOpen
|
|
LDA.0
|
|
BRA sbfsStreamNo
|
|
CALL sbfsStreamHere
|
|
|
|
SETD.0 SbfsStreamTo
|
|
STD.1.0
|
|
|
|
SETD.0 SbfsFileStart
|
|
SETD.2 SbfsStreamAt
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 SbfsStreamBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsStreamTail
|
|
LDA.0
|
|
SETD.2 SbfsFileTail
|
|
STA.2
|
|
|
|
SETD.0 SbfsStreamTo
|
|
LDD.1.0
|
|
CALL sbfsReadOne
|
|
RET
|
|
|
|
; SbfsFileBlocks and SbfsFileTail say how big it turned out to be, which need not be how
|
|
; big it was started at.
|
|
;
|
|
; A WRITER MAY ASK FOR MORE ROOM THAN IT ENDS UP USING, and that is not laziness on its
|
|
; part. Some sizes are not knowable until the last byte is out: the assembler cannot say
|
|
; how many vectors a program installs until it has resolved them, and by then the file it
|
|
; is writing into must already exist. So the room is taken generously at the start, where
|
|
; running out costs nothing, and the size is told the truth here.
|
|
;
|
|
; The blocks that were asked for and not used go back to the free count. It is a note
|
|
; rather than the authority - the directory is - but a note worth keeping right, and two
|
|
; disks holding the same files must hold the same bytes.
|
|
sbfsStreamDone:
|
|
SETD.0 SbfsStreamOpen
|
|
LDA.0
|
|
BRA sbfsStreamNo
|
|
CALL sbfsStreamHere
|
|
|
|
; What it really came to, put aside before anything walks the disk.
|
|
SETD.0 SbfsStreamNewBlocks
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
SETD.1 SbfsStreamNewTail
|
|
STA.1
|
|
|
|
; ---- MORE THAN WAS RESERVED IS REFUSED ----
|
|
;
|
|
; A writer may finish smaller than it asked for, which is the whole point of being told
|
|
; the size here. It may not finish BIGGER. The blocks after a file belong to whatever
|
|
; comes next, so an entry claiming more than was set aside for it claims somebody else's
|
|
; - and nothing anywhere would say so, because a directory entry is the only record of
|
|
; what a file owns. The count of free blocks would go wrong in the same breath, the
|
|
; subtraction below running backwards past zero.
|
|
;
|
|
; osFileWrite already refuses a block index past the end. This is the same bound from the
|
|
; other side, and it was missing: the index was checked because writing off the end was
|
|
; the obvious way to reach a neighbour, and committing a larger size reaches the same
|
|
; neighbour by simply claiming it.
|
|
;
|
|
; CHECKED BEFORE ANYTHING IS TOUCHED, which is why the temporary is found twice. The old
|
|
; file is deleted a few lines down, and a refusal after that point would have destroyed
|
|
; the thing it was protecting.
|
|
CALL sbfsStreamTemp
|
|
CALL sbfsScanFor
|
|
BNQ sbfsStreamNo
|
|
|
|
; What it was given, both halves of it.
|
|
SETD.0 SbfsStreamResBlocks
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
SETD.1 SbfsStreamResTail
|
|
STA.1
|
|
|
|
; And the room that came to, kept for the free count at the end.
|
|
CALL sbfsFileExtent
|
|
SETD.0 SbfsStreamSpare
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsSetWord
|
|
|
|
; THE SIZE IS COMPARED, NOT THE ROOM IT TAKES UP. Those are not the same question: one
|
|
; block and a tail occupies exactly what two whole blocks occupy, so a file reserving the
|
|
; first and committing the second claims no block it was not given - and still reports
|
|
; two hundred and forty six bytes more than were ever written to it, which are whatever
|
|
; the disk had there before. Bounding the blocks alone would have called that fine.
|
|
SETD.0 SbfsStreamResBlocks
|
|
SETD.2 SbfsStreamNewBlocks
|
|
CALL sbfsCompareWord
|
|
BRC sbfsStreamNo ; More whole blocks than it was given.
|
|
BNQ sbfsStreamFits ; Fewer, so the tail cannot matter.
|
|
SETD.0 SbfsStreamResTail
|
|
LDA.0
|
|
SETD.2 SbfsStreamNewTail
|
|
LDB.2
|
|
CCF
|
|
SUB
|
|
BRC sbfsStreamNo ; The same blocks, and a longer tail.
|
|
sbfsStreamFits:
|
|
|
|
; Now, and not before, the old one goes. It may not be there at all, which is what
|
|
; writing something for the first time looks like from here.
|
|
;
|
|
; The same two writes and the same gap between them as sbfsSaveFile has, and the same
|
|
; answer: what stops here is recoverable by hand and by nothing else. The free count
|
|
; goes back afterwards, and a machine stopping THERE only leaves the note wrong, which
|
|
; the directory can always settle.
|
|
CALL sbfsStreamWhere
|
|
CALL sbfsScanFor
|
|
BNQ sbfsStreamNoOld
|
|
CALL sbfsWipeFound
|
|
sbfsStreamNoOld:
|
|
|
|
; And the temporary takes its name and its true size, which together are the whole of
|
|
; what committing is. Found again, because deleting the old one read over the block it
|
|
; lives in.
|
|
CALL sbfsStreamTemp
|
|
CALL sbfsScanFor
|
|
BNQ sbfsStreamNo
|
|
|
|
; Finished: the flag goes down in the same block write that gives it its name and its
|
|
; size. Flat rather than cleared, for the reason sbfsSaveFile gives.
|
|
PSHD.3
|
|
POPD.1
|
|
INIA 0x01
|
|
STA.1
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d06
|
|
SETD.0 SbfsStreamLeaf
|
|
CALL sbfsCopyName
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d03
|
|
SETD.0 SbfsStreamNewBlocks
|
|
CALL sbfsCopyWord
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d05
|
|
SETD.0 SbfsStreamNewTail
|
|
LDA.0
|
|
STA.1
|
|
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsStreamNo
|
|
|
|
; And the difference goes back, which is what it was given less what it kept. That can
|
|
; no longer be negative: the check at the top refused the only case where it could.
|
|
CALL sbfsStreamSize
|
|
CALL sbfsFileExtent
|
|
SETD.0 SbfsStreamSpare
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsSubWord
|
|
|
|
RSTA
|
|
SETD.0 SbfsBlock
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsStreamNo
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d12
|
|
SETD.2 SbfsStreamSpare
|
|
CALL sbfsAddWord
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
BNQ sbfsStreamNo
|
|
|
|
RSTA
|
|
SETD.0 SbfsStreamOpen
|
|
STA.0
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; The size the writer says it came to, put back where the extent arithmetic reads it from.
|
|
; Said again rather than kept, because finding anything overwrites those two: they are where
|
|
; a find describes whatever it last looked at.
|
|
sbfsStreamSize:
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 SbfsStreamNewBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsStreamNewTail
|
|
LDA.0
|
|
SETD.1 SbfsFileTail
|
|
STA.1
|
|
RET
|
|
|
|
; The directory the file is going in, and the name it will end up under. Said again before
|
|
; each step, because every step goes to the disk and leaves the walk somewhere else.
|
|
sbfsStreamWhere:
|
|
SETD.0 SbfsStreamParent
|
|
SETD.1 SbfsAt
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsStreamLeaf
|
|
SETD.1 SbfsWanted
|
|
CALL sbfsCopyName
|
|
RET
|
|
|
|
; The same directory, under the name a half written file is kept as. Its own rather than
|
|
; the one a whole file save uses, so that neither can ever be handed the other's.
|
|
sbfsStreamTemp:
|
|
SETD.0 SbfsStreamParent
|
|
SETD.1 SbfsAt
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsStreamName
|
|
SETD.1 SbfsWanted
|
|
CALL sbfsKeepName
|
|
RET
|
|
|
|
; ---- Saving over something that is already there ----
|
|
;
|
|
; DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it
|
|
; now is. Q is zero if it was saved.
|
|
;
|
|
; This is the routine every tool that edits a document wants, and the reason it is here
|
|
; rather than in each of them is that the careful order is not obvious and getting it wrong
|
|
; destroys somebody's work:
|
|
;
|
|
; make a temporary nothing is lost if there is nowhere to put it
|
|
; write it
|
|
; delete the original only once the new one is safely down
|
|
; rename the temporary
|
|
;
|
|
; The obvious order - delete, create, write - looks fine and is a trap. Files here are
|
|
; contiguous, so a file that has grown may not fit where it was, and a create can be
|
|
; refused for want of a run long enough even on a disk with plenty of free blocks. Do it
|
|
; that way round and the original is already gone when that happens.
|
|
sbfsSaveFile:
|
|
SETD.2 SbfsSaveName
|
|
STD.0.2
|
|
SETD.2 SbfsSaveData
|
|
STD.1.2
|
|
|
|
; How big it is, kept aside: finding and deleting things both overwrite the place the
|
|
; size is normally said, because both of them describe whatever they last looked at.
|
|
SETD.0 SbfsSaveBlocks
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
SETD.1 SbfsSaveTail
|
|
STA.1
|
|
|
|
; 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 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
|
|
INIB 0x02
|
|
AND
|
|
BNQ sbfsSaveFailed
|
|
sbfsSaveNotThere:
|
|
|
|
; A temporary left behind by a save that did not finish would be in the way, so it goes.
|
|
;
|
|
; WHAT MAKES IT ONE IS THE FLAG AND NOT THE NAME. "sbfs.part" is a name a person is
|
|
; perfectly entitled to give a file of their own, and this used to delete whatever
|
|
; answered to it - so saving anything at all, once, quietly destroyed that file. The
|
|
; entry now says what it is, and something that is not ours stops the save instead.
|
|
CALL sbfsSaveTemp
|
|
CALL sbfsScanFor
|
|
BNQ sbfsSaveNoTemp
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x04
|
|
AND
|
|
BRQ sbfsSaveFailed ; Somebody's own file, under a name we wanted. Left alone.
|
|
CALL sbfsWipeFound
|
|
sbfsSaveNoTemp:
|
|
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 SbfsSaveBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsSaveTail
|
|
LDA.0
|
|
SETD.1 SbfsFileTail
|
|
STA.1
|
|
|
|
CALL sbfsSaveTemp
|
|
CALL sbfsCreateTempAt
|
|
BNQ sbfsSaveFailed
|
|
|
|
SETD.2 SbfsSaveData
|
|
LDD.1.2
|
|
CALL sbfsWriteFile
|
|
BNQ sbfsSaveFailed
|
|
|
|
; 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.
|
|
;
|
|
; EVERYTHING ABOVE THIS POINT CAN FAIL AND COST NOTHING. Below it there are two block
|
|
; writes and a gap between them, and a machine that stops in that gap has deleted the
|
|
; old file and not yet named the new one. The bytes are all there under the temporary's
|
|
; name and one rename gets them back, but nothing does that automatically and nothing
|
|
; here claims to: this is safe against the ways a save fails while it is running, not
|
|
; against the machine stopping. Closing the gap wants a journal, which is a great deal
|
|
; of disk to buy back two writes.
|
|
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
|
|
|
|
; FINISHED, WHICH IS THE FLAG AND THE NAME TOGETHER. Written flat rather than by
|
|
; clearing the one bit: a temporary is a file, so in use is the only other thing it can
|
|
; ever have been, and the whole byte is known.
|
|
PSHD.3
|
|
POPD.1
|
|
INIA 0x01
|
|
STA.1
|
|
|
|
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.1 SbfsWanted
|
|
CALL sbfsKeepName
|
|
RET
|
|
|
|
sbfsSaveFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- How the last start went ----
|
|
;
|
|
; One byte of the superblock, written by the loader before it hands over and cleared by the
|
|
; system once it is running. A system that never gets that far leaves the mark set, and the
|
|
; loader seeing it still set next time is how a machine that cannot start says so to the
|
|
; only thing in a position to do anything about it.
|
|
;
|
|
; Kept here rather than in either of them because BOTH read and write it, and two pieces of
|
|
; code with their own idea of where a byte lives is the thing this filesystem has two
|
|
; implementations and a byte for byte comparison to avoid.
|
|
;
|
|
; sbfsBootState leaves the state in SbfsStateWas, and Q zero if the disk answered.
|
|
;
|
|
; NOT IN A, and the first version of this tried to. CALL restores A, so a routine cannot
|
|
; hand anything back in it: the value was set, the RET put the caller's own A back over it,
|
|
; and every state read as whatever the caller happened to be holding. It is the trap this
|
|
; system documents in its own manual and it still catches people.
|
|
sbfsBootState:
|
|
RSTA
|
|
SETD.0 SbfsBlock
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsBootStateNo
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d17
|
|
LDA.0
|
|
SETD.1 SbfsStateWas
|
|
STA.1
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: SbfsStateWas is what the disk says.
|
|
RET
|
|
|
|
sbfsBootStateNo:
|
|
RSTA
|
|
SETD.0 SbfsStateWas
|
|
STA.0
|
|
INIB 0d1
|
|
CCF
|
|
ADD ; Q is not zero, and the state reads as settled.
|
|
RET
|
|
|
|
; A is what to write. Q is zero if it went down.
|
|
;
|
|
; The superblock is read back before it is changed rather than kept from the mount, because
|
|
; everything between then and now has been reading other blocks over the buffer it was in.
|
|
sbfsSetBootState:
|
|
SETD.0 SbfsStateWants
|
|
STA.0
|
|
RSTA
|
|
SETD.0 SbfsBlock
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsBootStateNo
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
SETD.0 SbfsStateWants
|
|
LDA.0
|
|
SETD.1 SbfsBuffer
|
|
DPUP.1 0d17
|
|
STA.1
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferIn
|
|
CALL sbfsWriteBlock
|
|
RET
|
|
|
|
#Data
|
|
|
|
SbfsStateWas:
|
|
0x00
|
|
SbfsStateWants:
|
|
0x00
|
|
|
|
SbfsMagic:
|
|
"SBFS"
|
|
|
|
; ---- Which disk this is, in eight bytes ----
|
|
;
|
|
; THE ORDER AND THE ADJACENCY ARE LOAD BEARING. These four are everything that distinguishes
|
|
; one mounted disk from another, and they are together so that changing drives is one copy
|
|
; out and one copy in. Nothing else may be put between them.
|
|
;
|
|
; Everything else in this file is either a constant or scratch for the operation being done
|
|
; now, and only one operation is ever being done - which is why a filesystem of 3,300 lines
|
|
; needs an eight byte record to know more than one disk. The rest never learns there is more
|
|
; than one.
|
|
;
|
|
; The version is not here. It is checked at mount and thrown away, because a version one
|
|
; disk's zero parent already reads as "in the root", which is where all of its files are.
|
|
SbfsMountLive:
|
|
SbfsDirStart:
|
|
0x00 0x00
|
|
SbfsDirBlocks:
|
|
0x00 0x00
|
|
SbfsDiskBlocks:
|
|
0x00 0x00
|
|
SbfsCwd:
|
|
0x00 0x00
|
|
|
|
; One record a drive, and the live eight above are whichever is selected. Four, because the
|
|
; controller has four.
|
|
SbfsMountTable:
|
|
#Reserve 0d32
|
|
; Which drive the live record belongs to, and which drives were found to have a disk on them
|
|
; that this can read. A bit a drive, so drive n is bit n.
|
|
SbfsDrive:
|
|
0x00
|
|
SbfsMounted:
|
|
0x00
|
|
SbfsDriveCount:
|
|
0x00
|
|
SbfsDriveAt:
|
|
0x00
|
|
SbfsPathWanted:
|
|
0x00
|
|
|
|
SbfsFileStart:
|
|
0x00 0x00
|
|
SbfsFileBlocks:
|
|
0x00 0x00
|
|
SbfsFileTail:
|
|
0x00
|
|
SbfsBlock:
|
|
0x00 0x00
|
|
SbfsWantBlocks:
|
|
0x00 0x00
|
|
SbfsCandidate:
|
|
0x00 0x00
|
|
SbfsCandEnd:
|
|
0x00 0x00
|
|
; Whether the candidate had to move at all during the pass just finished. See sbfsAllocate.
|
|
SbfsAllocMoved:
|
|
0x00
|
|
SbfsEntryStart:
|
|
0x00 0x00
|
|
SbfsEntryEnd:
|
|
0x00 0x00
|
|
SbfsScratch:
|
|
0x00 0x00
|
|
SbfsOne:
|
|
0x00 0x01
|
|
SbfsCount:
|
|
0x00
|
|
SbfsMatchLeft:
|
|
0x00
|
|
SbfsLeft:
|
|
0x00
|
|
; ---- Where the machine is ----
|
|
;
|
|
; The working directory, written the way a parent is written: an entry index plus one, so
|
|
; zero is the root and a freshly booted machine is already there. A path that does not
|
|
; begin with a separator is measured from here.
|
|
;
|
|
; It lives here rather than in the shell because it is what a relative path MEANS, and the
|
|
; thing that resolves a path is here. Keeping it in the shell would mean either handing it
|
|
; down on every call or having the shell paste it onto the front of every name, and the
|
|
; second of those is how a name that is already absolute gets ruined.
|
|
|
|
; ---- 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
|
|
|
|
; ---- 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:
|
|
0x00
|
|
SbfsAt:
|
|
0x00 0x00
|
|
SbfsScanIndex:
|
|
0x00 0x00
|
|
|
|
; ---- What the disk's one buffer holds, or is about to ----
|
|
SbfsBufferBlock:
|
|
0x00 0x00
|
|
SbfsBufferKnown:
|
|
0x00
|
|
SbfsAheadIndex:
|
|
0x00 0x00
|
|
SbfsTarget:
|
|
0x00 0x00
|
|
SbfsUpParent:
|
|
0x00 0x00
|
|
SbfsFoundFlags:
|
|
0x00
|
|
|
|
; What the next entry made is to be marked with. sbfsCreateAt sets it to a plain file and
|
|
; sbfsCreateTempAt to a temporary, so it is never read without having just been written.
|
|
SbfsMakeFlags:
|
|
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
|
|
|
|
; ---- What a walk through the directory keeps between calls ----
|
|
|
|
SbfsWalkBlock:
|
|
0x00 0x00
|
|
SbfsWalkLeft:
|
|
0x00
|
|
SbfsWalkCount:
|
|
0x00
|
|
SbfsWalkPointer:
|
|
0x00 0x00
|
|
SbfsWalkAt:
|
|
0x00 0x00
|
|
|
|
; Twenty three bytes for a name of twenty two, so that the last one is a zero nothing
|
|
; ever writes over and the name is always a string.
|
|
SbfsName:
|
|
#Reserve 0d23
|
|
|
|
SbfsWanted:
|
|
#Reserve 0d22
|
|
|
|
; ---- What renaming and saving keep ----
|
|
|
|
; A name on its way into an entry, padded to the twenty two bytes an entry holds.
|
|
SbfsNewName:
|
|
#Reserve 0d22
|
|
|
|
; Where a name lives, kept across a search, which needs the pointers for itself.
|
|
SbfsSavedName:
|
|
0x00 0x00
|
|
|
|
SbfsSaveName:
|
|
0x00 0x00
|
|
SbfsSaveData:
|
|
0x00 0x00
|
|
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.
|
|
SbfsTempName:
|
|
"sbfs.part"
|
|
|
|
; ---- What a file being written a block at a time keeps ----
|
|
SbfsStreamDrive:
|
|
0x00
|
|
SbfsStreamOpen:
|
|
0x00
|
|
SbfsStreamPath:
|
|
0x00 0x00
|
|
SbfsStreamFrom:
|
|
0x00 0x00
|
|
SbfsStreamTo:
|
|
0x00 0x00
|
|
SbfsStreamNewBlocks:
|
|
0x00 0x00
|
|
SbfsStreamNewTail:
|
|
0x00
|
|
SbfsStreamSpare:
|
|
0x00 0x00
|
|
SbfsStreamResBlocks:
|
|
0x00 0x00
|
|
SbfsStreamResTail:
|
|
0x00
|
|
SbfsStreamAt:
|
|
0x00 0x00
|
|
SbfsStreamParent:
|
|
0x00 0x00
|
|
SbfsStreamLeaf:
|
|
#Reserve 0d23
|
|
SbfsStreamBlocks:
|
|
0x00 0x00
|
|
SbfsStreamTail:
|
|
0x00
|
|
SbfsStreamName:
|
|
"sbfs.out"
|
|
|
|
SbfsBuffer:
|
|
#Reserve 0d256
|