; 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