DPUP takes an immediate, so an offset of one is legal and does exactly the right thing. It is also three bytes where INCD is two, and reads as "offset the pointer up by one" where INCD reads as "step the pointer". 56 of them across 15 files: the system, the assembler, the editor, and eight test programs. CosmOS is 9,564 bytes to 9,537, the native assembler 11,648 to 11,635, and every program in the repository together 49 bytes lighter. The worst offender was numbers.asm, written this week, where every sixteen bit helper reaches the low byte and comes back the long way round. It is the file every other part of the assembler includes, so it is the first thing anybody reads when they go looking - and it was teaching them the long way. Pattern matched off sbfs.asm rather than off the instruction table I had just embedded in two programs. THIS IS NOT TWO WAYS TO DO ONE THING. DPUP takes an arbitrary number, so one is inevitably among them; INCD earns its place by making the common case a byte cheaper. The overlap is structural and the choice is a usage question, which is a linter's job rather than an ISA's - "DPUP.n 0d01: INCD.n does this in a byte less" is a mechanical rule with no judgement in it. Nothing needed re-recording, which was not a foregone conclusion: cosmosBreak prints the system addresses the registers happened to hold, and they did not move. Both assemblers still produce identical bytes and CosmOS still builds itself to a fixed point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
1334 lines
29 KiB
NASM
1334 lines
29 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.
|
|
;
|
|
; 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.
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d04
|
|
LDA.0
|
|
INIB 0d1
|
|
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
|
|
SETD.0 SbfsBuffer
|
|
DPUP.0 0d06
|
|
SETD.1 SbfsDiskBlocks
|
|
CALL sbfsCopyWord
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: mounted.
|
|
RET
|
|
|
|
; Finds a file by name. DP0 points at a name, ending in a zero byte. Q is zero if it was
|
|
; found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it.
|
|
sbfsFind:
|
|
SETD.1 SbfsWanted
|
|
CALL sbfsKeepName
|
|
|
|
; Start at the first directory block and work through them all.
|
|
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 files.
|
|
|
|
sbfsFindBlock:
|
|
CALL sbfsReadBlock
|
|
BRQ sbfsFindLoaded
|
|
RET ; The read failed.
|
|
sbfsFindLoaded:
|
|
SETD.1 SbfsBuffer
|
|
CALL sbfsBufferOut
|
|
|
|
; Eight entries to a block, thirty two bytes each.
|
|
SETD.2 SbfsBuffer
|
|
INIA 0d8
|
|
SETD.1 SbfsCount
|
|
STA.1
|
|
|
|
sbfsFindEntry:
|
|
LDA.2
|
|
INIB 0x01
|
|
AND
|
|
BRQ sbfsFindNext ; The in use bit is down, so this entry is free.
|
|
CALL sbfsMatchEntry
|
|
BRQ sbfsFindFound
|
|
|
|
sbfsFindNext:
|
|
DPUP.2 0d32
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA sbfsFindEntry
|
|
|
|
sbfsFindNextBlock:
|
|
SETD.0 SbfsBlock
|
|
CALL sbfsStepWord
|
|
SETD.1 SbfsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRA sbfsFindMissing
|
|
BRI sbfsFindBlock
|
|
|
|
sbfsFindMissing:
|
|
; Nothing of that name. Q has to be something other than zero to say so.
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
sbfsFindFound:
|
|
; DP2 is on the entry. Keep it in DP3 while the pieces are taken out of it: DP3 is the
|
|
; pointer a CALL does not put back, so it is the only one that survives a subroutine.
|
|
; Copying it into DP0 has to be done here rather than in a routine of its own, for the
|
|
; same reason: a routine that set DP0 would have the assignment undone by its own RET.
|
|
PSHD.2
|
|
POPD.3
|
|
|
|
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
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: found.
|
|
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
|
|
|
|
sbfsNext:
|
|
; 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.
|
|
BRI sbfsWalkScan
|
|
|
|
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
|
|
|
|
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:
|
|
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
|
|
|
|
; 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
|
|
|
|
CALL sbfsReadBlock
|
|
BNQ sbfsReadOneDone ; The read failed, and Q says so.
|
|
|
|
PSHD.3
|
|
POPD.1
|
|
CALL sbfsBufferOut
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: read.
|
|
sbfsReadOneDone:
|
|
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.
|
|
sbfsAllocate:
|
|
CALL sbfsFirstData
|
|
|
|
sbfsAllocTry:
|
|
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.
|
|
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 try again from the far end of whatever is in the way.
|
|
POPD.2
|
|
SETD.0 SbfsCandidate
|
|
SETD.2 SbfsEntryEnd
|
|
CALL sbfsSetWord
|
|
BRI sbfsAllocTry
|
|
|
|
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
|
|
|
|
; 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:
|
|
SETD.1 SbfsWanted
|
|
CALL sbfsKeepName
|
|
|
|
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
|
|
INIA 0x01
|
|
STA.3 ; In use.
|
|
|
|
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
|
|
|
|
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:
|
|
SETD.0 SbfsBlock
|
|
LDA.0
|
|
OUTA 0x20
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x21
|
|
INIA 0x02
|
|
OUTA 0x22
|
|
INA 0x23
|
|
INIB 0x02
|
|
AND
|
|
RET
|
|
|
|
; Reads the block named by SbfsBlock into the disk's buffer. Q is zero if it worked.
|
|
sbfsReadBlock:
|
|
SETD.0 SbfsBlock
|
|
LDA.0
|
|
OUTA 0x20
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x21
|
|
INIA 0x01
|
|
OUTA 0x22
|
|
INA 0x23
|
|
INIB 0x02
|
|
AND ; Q is the error bit, so zero means it worked.
|
|
RET
|
|
|
|
; 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
|
|
|
|
; ---- 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
|
|
|
|
; 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.
|
|
;
|
|
; 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
|
|
|
|
PSHD.1
|
|
POPD.0
|
|
SETD.1 SbfsNewName
|
|
CALL sbfsKeepName ; Twenty two bytes, padded, the way an entry holds one.
|
|
|
|
; Refused if something already answers to the new name. Two entries with one name is a
|
|
; disk that cannot be searched sensibly: a search answers with whichever it meets first,
|
|
; so the other becomes unreachable without ever having been deleted.
|
|
SETD.0 SbfsNewName
|
|
CALL sbfsFind
|
|
BRQ sbfsRenameFailed
|
|
|
|
SETD.2 SbfsSavedName
|
|
LDD.0.2
|
|
CALL sbfsFind
|
|
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
|
|
|
|
; ---- 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
|
|
|
|
; A temporary left behind by a save that did not finish would be in the way. Whether
|
|
; there was one is not worth asking about, since either answer leads here.
|
|
SETD.0 SbfsTempName
|
|
CALL sbfsDelete
|
|
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 SbfsSaveBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsSaveTail
|
|
LDA.0
|
|
SETD.1 SbfsFileTail
|
|
STA.1
|
|
|
|
SETD.0 SbfsTempName
|
|
CALL sbfsCreate
|
|
BNQ sbfsSaveFailed
|
|
|
|
SETD.2 SbfsSaveData
|
|
LDD.1.2
|
|
CALL sbfsWriteFile
|
|
BNQ sbfsSaveFailed
|
|
|
|
; Now, and not before, the old one goes. It may not exist, which is what saving something
|
|
; for the first time looks like from here.
|
|
SETD.2 SbfsSaveName
|
|
LDD.0.2
|
|
CALL sbfsDelete
|
|
|
|
SETD.0 SbfsTempName
|
|
SETD.2 SbfsSaveName
|
|
LDD.1.2
|
|
CALL sbfsRename
|
|
RET
|
|
|
|
sbfsSaveFailed:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
#Data
|
|
|
|
SbfsMagic:
|
|
"SBFS"
|
|
|
|
SbfsDirStart:
|
|
0x00 0x00
|
|
SbfsDirBlocks:
|
|
0x00 0x00
|
|
SbfsFileStart:
|
|
0x00 0x00
|
|
SbfsFileBlocks:
|
|
0x00 0x00
|
|
SbfsFileTail:
|
|
0x00
|
|
SbfsBlock:
|
|
0x00 0x00
|
|
SbfsDiskBlocks:
|
|
0x00 0x00
|
|
SbfsWantBlocks:
|
|
0x00 0x00
|
|
SbfsCandidate:
|
|
0x00 0x00
|
|
SbfsCandEnd:
|
|
0x00 0x00
|
|
SbfsEntryStart:
|
|
0x00 0x00
|
|
SbfsEntryEnd:
|
|
0x00 0x00
|
|
SbfsScratch:
|
|
0x00 0x00
|
|
SbfsOne:
|
|
0x00 0x01
|
|
SbfsCount:
|
|
0x00
|
|
SbfsMatchLeft:
|
|
0x00
|
|
SbfsLeft:
|
|
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
|
|
|
|
; 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"
|
|
|
|
SbfsBuffer:
|
|
#Reserve 0d256
|