collapseos/kernel/fs.asm
Virgil Dupras 495d69c1cb sdc: make writing more solid
By not waiting until the SD card wan't busy any more after a write, we
exposed ourselves to errors if another operation was attempted right
after it.
2019-06-07 14:59:53 -04:00

555 lines
13 KiB
NASM

; fs
;
; Collapse OS filesystem (CFS) is not made to be convenient, but to be simple.
; This is little more than "named storage blocks". Characteristics:
;
; * a filesystem sits upon a blockdev. It needs GetC, PutC, Seek.
; * No directory. Use filename prefix to group.
; * First block of each file has metadata. Others are raw data.
; * No FAT. Files are a chain of blocks of a predefined size. To enumerate
; files, you go through metadata blocks.
; * Fixed allocation. File size is determined at allocation time and cannot be
; grown, only shrunk.
; * New allocations try to find spots to fit in, but go at the end if no spot is
; large enough.
; * Block size is 0x100, max block count per file is 8bit, that means that max
; file size: 64k - metadata overhead.
;
; *** Selecting a "source" blockdev
;
; This unit exposes "fson" shell command to "mount" CFS upon the currently
; selected device, at the point where its seekptr currently sits. This checks
; if we have a valid first block and spits an error otherwise.
;
; "fson" takes an optional argument which is a number. If non-zero, we don't
; error out if there's no metadata: we create a new CFS fs with an empty block.
;
; The can only be one "mounted" fs at once. Selecting another blockdev through
; "bsel" doesn't affect the currently mounted fs, which can still be interacted
; with (which is important if we want to move data around).
;
; *** Block metadata
;
; At the beginning of the first block of each file, there is this data
; structure:
;
; 3b: Magic number "CFS"
; 1b: Allocated block count, including the first one. Except for the "ending"
; block, this is never zero.
; 2b: Size of file in bytes (actually written). Little endian.
; 26b: file name, null terminated. last byte must be null.
;
; That gives us 32 bytes of metadata for first first block, leaving a maximum
; file size of 0xffe0.
;
; *** Last block of the chain
;
; The last block of the chain is either a block that has no valid block next to
; it or a block that reports a 0 allocated block count.
;
; However, to simplify processing, whenever fsNext encounter a chain end of the
; first type (a valid block with > 0 allocated blocks), it places an empty block
; at the end of the chain. This makes the whole "end of chain" processing much
; easier: we assume that we always have a 0 block at the end.
;
; *** Deleted files
;
; When a file is deleted, its name is set to null. This indicates that the
; allocated space is up for grabs.
;
; *** File "handles"
;
; Programs will not typically open files themselves. How it works with CFS is
; that it exposes an API to plug target files in a blockdev ID. This all
; depends on how you glue parts together, but ideally, you'll have two
; fs-related blockdev IDs: one for reading, one for writing.
;
; Being plugged into the blockdev system, programs will access the files as they
; would with any other block device.
;
; *** Creating a new FS
;
; A valid Collapse OS filesystem is nothing more than the 3 bytes 'C', 'F', 'S'
; next to each other. Placing them at the right place is all you have to do to
; create your FS.
; *** DEFINES ***
; Number of handles we want to support
; FS_HANDLE_COUNT
; *** CONSTS ***
.equ FS_MAX_NAME_SIZE 0x1a
.equ FS_BLOCKSIZE 0x100
.equ FS_METASIZE 0x20
.equ FS_META_ALLOC_OFFSET 3
.equ FS_META_FSIZE_OFFSET 4
.equ FS_META_FNAME_OFFSET 6
; Size in bytes of a FS handle:
; * 4 bytes for starting offset of the FS block
; * 2 bytes for file size
.equ FS_HANDLE_SIZE 6
.equ FS_ERR_NO_FS 0x5
.equ FS_ERR_NOT_FOUND 0x6
; *** VARIABLES ***
; A copy of BLOCKDEV_SEL when the FS was mounted. 0 if no FS is mounted.
.equ FS_BLK FS_RAMSTART
; Offset at which our FS start on mounted device
; This pointer is 32 bits. 32 bits pointers are a bit awkward: first two bytes
; are high bytes *low byte first*, and then the low two bytes, same order.
; When loaded in HL/DE, the four bytes are loaded in this order: E, D, L, H
.equ FS_START FS_BLK+BLOCKDEV_SIZE
; This variable below contain the metadata of the last block we moved
; to. We read this data in memory to avoid constant seek+read operations.
.equ FS_META FS_START+4
.equ FS_HANDLES FS_META+FS_METASIZE
.equ FS_RAMEND FS_HANDLES+FS_HANDLE_COUNT*FS_HANDLE_SIZE
; *** DATA ***
P_FS_MAGIC:
.db "CFS", 0
; *** CODE ***
fsInit:
xor a
ld hl, FS_BLK
ld b, FS_RAMEND-FS_BLK
call fill
ret
; *** Navigation ***
; Seek to the beginning. Errors out if no FS is mounted.
; Sets Z if success, unset if error
fsBegin:
call fsIsOn
ret nz
push hl
push de
push af
ld de, (FS_START)
ld hl, (FS_START+2)
ld a, BLOCKDEV_SEEK_ABSOLUTE
call fsblkSeek
pop af
pop de
pop hl
call fsReadMeta
jp fsIsValid ; sets Z, returns
; Change current position to the next block with metadata. If it can't (if this
; is the last valid block), doesn't move.
; Sets Z according to whether we moved.
fsNext:
push bc
push hl
ld a, (FS_META+FS_META_ALLOC_OFFSET)
or a ; cp 0
jr z, .error ; if our block allocates 0 blocks, this is the
; end of the line.
ld b, a ; we will seek A times
.loop:
ld a, BLOCKDEV_SEEK_FORWARD
ld hl, FS_BLOCKSIZE
call fsblkSeek
djnz .loop
call fsReadMeta
jr nz, .createChainEnd
call fsIsValid
jr nz, .createChainEnd
; We're good! We have a valid FS block.
; Meta is already read. Nothing to do!
cp a ; ensure Z
jr .end
.createChainEnd:
; We are on an invalid block where a valid block should be. This is
; the end of the line, but we should mark it a bit more explicitly.
; Let's initialize an empty block
call fsInitMeta
call fsWriteMeta
; continue out to error condition: we're still at the end of the line.
.error:
call unsetZ
.end:
pop hl
pop bc
ret
; Reads metadata at current fsblk and place it in FS_META.
; Returns Z according to whether the operation succeeded.
fsReadMeta:
push bc
push hl
ld b, FS_METASIZE
ld hl, FS_META
call fsblkRead ; Sets Z
pop hl
pop bc
ret nz
; Only rewind on success
jr _fsRewindAfterMeta
; Writes metadata in FS_META at current fsblk.
; Returns Z according to whether the fsblkWrite operation succeeded.
fsWriteMeta:
push bc
push hl
ld b, FS_METASIZE
ld hl, FS_META
call fsblkWrite ; Sets Z
pop hl
pop bc
ret nz
; Only rewind on success
jr _fsRewindAfterMeta
_fsRewindAfterMeta:
; return back to before the read op
push af
push hl
ld a, BLOCKDEV_SEEK_BACKWARD
ld hl, FS_METASIZE
call fsblkSeek
pop hl
pop af
ret
; Initializes FS_META with "CFS" followed by zeroes
fsInitMeta:
push af
push bc
push de
push hl
ld hl, P_FS_MAGIC
ld de, FS_META
ld bc, 3
ldir
xor a
ld hl, FS_META+3
ld b, FS_METASIZE-3
call fill
pop hl
pop de
pop bc
pop af
ret
; Create a new file with A blocks allocated to it and with its new name at
; (HL).
; Before doing so, enumerate all blocks in search of a deleted file with
; allocated space big enough. If it does, it will either take the whole space
; if the allocated space asked is exactly the same, or of it isn't, split the
; free space in 2 and create a new deleted metadata block next to the newly
; created block.
; Places fsblk to the newly allocated block. You have to write the new
; filename yourself.
fsAlloc:
push bc
push de
ld c, a ; Let's store our A arg somewhere...
call fsBegin
jr nz, .end ; not a valid block? hum, something's wrong
; First step: find last block
push hl ; keep HL for later
.loop1:
call fsNext
jr nz, .found ; end of the line
call fsIsDeleted
jr nz, .loop1 ; not deleted? loop
; This is a deleted block. Maybe it fits...
ld a, (FS_META+FS_META_ALLOC_OFFSET)
cp c ; Same as asked size?
jr z, .found ; yes? great!
; TODO: handle case where C < A (block splitting)
jr .loop1
.found:
; We've reached last block. Two situations are possible at this point:
; 1 - the block is the "end of line" block
; 2 - the block is a deleted block that we we're re-using.
; In both case, the processing is the same: write new metadata.
; At this point, the blockdev is placed right where we want to allocate
; But first, let's prepare the FS_META we're going to write
call fsInitMeta
ld a, c ; C == the number of blocks user asked for
ld (FS_META+FS_META_ALLOC_OFFSET), a
pop hl ; now we want our HL arg
; TODO: stop after null char. we're filling meta with garbage here.
ld de, FS_META+FS_META_FNAME_OFFSET
ld bc, FS_MAX_NAME_SIZE
ldir
; Good, FS_META ready.
; Ok, now we can write our metadata
call fsWriteMeta
.end:
pop de
pop bc
ret
; Place fsblk to the filename with the name in (HL).
; Sets Z on success, unset when not found.
fsFindFN:
push de
call fsBegin
jr nz, .end ; nothing to find, Z is unset
ld a, FS_MAX_NAME_SIZE
.loop:
ld de, FS_META+FS_META_FNAME_OFFSET
call strncmp
jr z, .end ; Z is set
call fsNext
jr z, .loop
; End of the chain, not found
call unsetZ
.end:
pop de
ret
; *** Metadata ***
; Sets Z according to whether the current block in FS_META is valid.
; Don't call other FS routines without checking block validity first: other
; routines don't do checks.
fsIsValid:
push hl
push de
ld a, 3
ld hl, FS_META
ld de, P_FS_MAGIC
call strncmp
; The result of Z is our result.
pop de
pop hl
ret
; Returns wheter current block is deleted in Z flag.
fsIsDeleted:
ld a, (FS_META+FS_META_FNAME_OFFSET)
cp 0 ; Z flag is our answer
ret
; *** blkdev methods ***
; When "mounting" a FS, we copy the current blkdev's routine privately so that
; we can still access the FS even if blkdev selection changes. These routines
; below mimic blkdev's methods, but for our private mount.
fsblkGetC:
push ix
ld ix, FS_BLK
call _blkGetC
pop ix
ret
fsblkRead:
push ix
ld ix, FS_BLK
call _blkRead
pop ix
ret
fsblkPutC:
push ix
ld ix, FS_BLK
call _blkPutC
pop ix
ret
fsblkWrite:
push ix
ld ix, FS_BLK
call _blkWrite
pop ix
ret
fsblkSeek:
push ix
ld ix, FS_BLK
call _blkSeek
pop ix
ret
fsblkTell:
push ix
ld ix, FS_BLK
call _blkTell
pop ix
ret
; *** Handling ***
; Open file at current position into handle at (IX)
fsOpen:
push hl
push af
; Starting pos
ld a, (FS_BLK+4)
ld (ix), a
ld a, (FS_BLK+5)
ld (ix+1), a
ld a, (FS_BLK+6)
ld (ix+2), a
ld a, (FS_BLK+7)
ld (ix+3), a
; file size
ld hl, (FS_META+FS_META_FSIZE_OFFSET)
ld (ix+4), l
ld (ix+5), h
pop af
pop hl
ret
; Place FS blockdev at proper position for file handle in (IX) at position HL.
fsPlaceH:
push af
push de
push hl
; Move fsdev to beginning of block
ld e, (ix)
ld d, (ix+1)
ld l, (ix+2)
ld h, (ix+3)
ld a, BLOCKDEV_SEEK_ABSOLUTE
call fsblkSeek
; skip metadata
ld a, BLOCKDEV_SEEK_FORWARD
ld hl, FS_METASIZE
call fsblkSeek
pop hl
pop de
; go to specified pos
ld a, BLOCKDEV_SEEK_FORWARD
call fsblkSeek
pop af
ret
; Advance file handle in (IX) by one byte
; Sets Z according to whether HL is within bounds for file handle at (IX), that
; is, if it is smaller than file size.
fsWithinBounds:
push de
; file size
ld e, (ix+4)
ld d, (ix+5)
call cpHLDE
pop de
jr nc, .outOfBounds ; HL >= DE
cp a ; ensure Z
ret
.outOfBounds:
jp unsetZ ; returns
; Adjust, if needed, file size of handle (IX) to HL+1.
; This adjustment only happens if this makes file size grow.
fsAdjustBounds:
call fsWithinBounds
ret z
; Not within bounds? let's increase them
push hl
ld hl, 0
call fsPlaceH ; fs blkdev is now at beginning of content
; we need the blkdev to be on filesize's offset
ld hl, FS_METASIZE-FS_META_FSIZE_OFFSET
ld a, BLOCKDEV_SEEK_BACKWARD
call fsblkSeek
pop hl
; blkdev is at the right spot, HL is back to its original value, let's
; write it.
push hl
inc hl ; We write HL+1, remember
; now let's write our new filesize both in blkdev and in file handle's
; cache.
ld a, l
ld (ix+4), a
call fsblkPutC
ld a, h
ld (ix+5), a
call fsblkPutC
pop hl
xor a ; ensure Z
ret
; Read a byte in handle at (IX) at position HL and put it into A.
; Z is set on success, unset if handle is at the end of the file.
fsGetC:
call fsWithinBounds
jr z, .proceed
; We want to unset Z, but also return 0 to ensure that a GetC that
; doesn't check Z doesn't end up with false data.
xor a
jp unsetZ ; returns
.proceed:
push hl
call fsPlaceH
call fsblkGetC
pop hl
ret
; Write byte A in handle (IX) and advance the handle's position.
; Z is set on success, unset if handle is at the end of the file.
; TODO: detect end of block alloc
fsPutC:
push hl
call fsPlaceH
call fsblkPutC
pop hl
jp fsAdjustBounds ; returns
; Mount the fs subsystem upon the currently selected blockdev at current offset.
; Verify is block is valid and error out if its not, mounting nothing.
; Upon mounting, copy currently selected device in FS_BLK.
fsOn:
push hl
push de
push bc
; We have to set blkdev routines early before knowing whether the
; mounting succeeds because methods like fsReadMeta uses fsblk* methods.
ld hl, BLOCKDEV_SEL
ld de, FS_BLK
ld bc, BLOCKDEV_SIZE
ldir ; copy!
call fsblkTell
ld (FS_START), de
ld (FS_START+2), hl
call fsReadMeta
jr nz, .error
call fsIsValid
jr nz, .error
; success
xor a
jr .end
.error:
; couldn't mount. Let's reset our variables.
xor a
ld b, FS_META-FS_BLK ; reset routine pointers and FS ptrs
ld hl, FS_BLK
call fill
ld a, FS_ERR_NO_FS
.end:
pop bc
pop de
pop hl
ret
; Sets Z according to whether we have a filesystem mounted.
fsIsOn:
; check whether (FS_BLK) is zero
push hl
push de
ld hl, (FS_BLK)
ld de, 0
call cpHLDE
jr nz, .mounted
; if equal, it means our FS is not mounted
call unsetZ
jr .end
.mounted:
cp a ; ensure Z
.end:
pop de
pop hl
ret