|
- ; 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 GetB, PutB, 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 @+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 @+4
- .equ FS_HANDLES @+FS_METASIZE
- .equ FS_RAMEND @+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 whether 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.
-
- fsblkGetB:
- push ix
- ld ix, FS_BLK
- call _blkGetB
- pop ix
- ret
-
- fsblkRead:
- push ix
- ld ix, FS_BLK
- call _blkRead
- pop ix
- ret
-
- fsblkPutB:
- push ix
- ld ix, FS_BLK
- call _blkPutB
- 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
-
- ; 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
-
- ; Set size of file handle (IX) to value in HL.
- ; This writes directly in handle's metadata.
- fsSetSize:
- push hl ; --> lvl 1
- 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 ; <-- lvl 1
- ; blkdev is at the right spot, HL is back to its original value, let's
- ; write it both in the metadata block and in its file handle's cache.
- push hl ; --> lvl 1
- ; now let's write our new filesize both in blkdev and in file handle's
- ; cache.
- ld a, l
- ld (ix+4), a
- call fsblkPutB
- ld a, h
- ld (ix+5), a
- call fsblkPutB
- pop hl ; <-- lvl 1
- 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.
- fsGetB:
- call fsWithinBounds
- jr z, .proceed
- ; We want to unset Z, but also return 0 to ensure that a GetB that
- ; doesn't check Z doesn't end up with false data.
- xor a
- jp unsetZ ; returns
- .proceed:
- push hl
- call fsPlaceH
- call fsblkGetB
- cp a ; ensure Z
- pop hl
- ret
-
- ; Write byte A in handle (IX) at position HL.
- ; Z is set on success, unset if handle is at the end of the file.
- ; TODO: detect end of block alloc
- fsPutB:
- push hl
- call fsPlaceH
- call fsblkPutB
- pop hl
- ; if HL is out of bounds, increase bounds
- call fsWithinBounds
- ret z
- inc hl ; our filesize is now HL+1
- jp fsSetSize
-
- ; 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
|