aboutsummaryrefslogtreecommitdiffstats
path: root/src/fs.asm
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-24 14:15:46 +0200
committeruser <user@clank>2026-09-24 14:15:46 +0200
commit50a56b48f22cd31868403f1fde10cf8367344e2e (patch)
tree57c606182378dde67ce63d115ea07f98cc96b709 /src/fs.asm
parentusr: xxd - hex dump a file or stdin (diff)
downloadgbos-50a56b48f22cd31868403f1fde10cf8367344e2e.tar.gz
gbos-50a56b48f22cd31868403f1fde10cf8367344e2e.tar.xz
gbos-50a56b48f22cd31868403f1fde10cf8367344e2e.zip
fs: mount the builtins at /bin
The commands only existed in the kernel's program table, so the only way to find out what the machine could do was to already know (or read the source). They are now a directory in the tree: /# ls / /# xxd -l 16 /bin/uname bin 0000: cd 6b 43 06 00 0e 00 f7 .kC..... readme 0008: 18 fe 47 0e 03 f7 c9 21 ..G....! /# ls -l /bin | head -2 - 1 16384 worker /bin is synthetic, not disk: dir_find resolves "bin" in the root to BIN_INO and names inside it through the NameTable, so `ls /bin` lists exactly what the shell can run - one table, one truth, no second list to keep in sync. The root listing carries "bin" as a synthetic first entry so it is discoverable by walking the tree. A program image is inode BIN_FILE|progid: it stats as a 16 KiB file (a program IS its ROM bank) and reads through rom_getb, which maps that bank, takes the byte, and maps the CALLER'S text bank back before returning - the caller is itself executing from $4000, so leaving the wrong bank mapped would return into another program's code. Verified byte-for-byte against the host's view of the blob. The tree is read-only: create/remove/write inside /bin all fail. xxd grows -l (stop after N bytes), because a program image is 2048 dump lines - more than a pipe's temp file can hold, let alone the screen. Two traps worth remembering, both found the hard way here: `and BIN_FILE` destroys A, so the inode must be reloaded before inode_ptr (missing it once made ls -l stat inode 0 and call every file an empty non-directory); and ls now copies its directory argument to the stack first, because stat()'s request block is a static living at $A000 - on top of the command line it was reading.
Diffstat (limited to 'src/fs.asm')
-rw-r--r--src/fs.asm237
1 files changed, 236 insertions, 1 deletions
diff --git a/src/fs.asm b/src/fs.asm
index 30dfdd6..5019015 100644
--- a/src/fs.asm
+++ b/src/fs.asm
@@ -242,6 +242,31 @@ dir_write:
; dir_find - dir inode in wFsDir, name in wFsNameBuf -> A = inode (0 if none)
; -----------------------------------------------------------------------------
dir_find::
+ ; /bin is synthetic. Inside it, names come from the kernel's NameTable;
+ ; in the root, the name "bin" resolves to it. Everything else is disk.
+ ld a, [wFsDir]
+ cp BIN_INO
+ jr z, .inbin
+ cp ROOT_INO
+ jr nz, .disk
+ ld hl, .binname
+ ld de, wFsNameBuf
+ call fs_streq
+ jr nz, .disk
+ ld a, BIN_INO
+ ret
+.inbin
+ ld de, wFsNameBuf
+ call sys_lookup ; A = program id, or $FF
+ cp $FF
+ jr z, .nobin
+ or BIN_FILE ; -> the synthetic inode for that program
+ ret
+.nobin
+ xor a
+ ret
+.binname: db "bin", 0
+.disk
call dir_load
ld c, 0
.l
@@ -264,10 +289,27 @@ dir_find::
ld a, [hl]
ret
+; fs_streq(HL, DE) -> Z if the two NUL-terminated strings match.
+fs_streq:
+ ld a, [de]
+ cp [hl]
+ ret nz
+ or a
+ ret z
+ inc hl
+ inc de
+ jr fs_streq
+
; -----------------------------------------------------------------------------
; dir_add - dir in wFsDir, child in wFsInode, name in wFsNameBuf -> A = 1/0
; -----------------------------------------------------------------------------
dir_add::
+ ld a, [wFsDir] ; /bin is read-only: it is ROM
+ cp BIN_INO
+ jr nz, .notbin
+ xor a
+ ret
+.notbin
call dir_load
ld c, 0
.l
@@ -676,10 +718,18 @@ sys_open::
call copy_path ; path -> wFsPath
call resolve_parent ; A = parent dir inode, wFsNameBuf = final name
or a
- jr z, .fail
+ jp z, .fail
ld [wFsDir], a
call dir_find ; A = target inode or 0
ld [wFsInode], a
+ cp BIN_INO
+ jp z, .isdir ; /bin: a directory, not a file
+ and BIN_FILE
+ jp nz, .romfile ; a program image: read-only, no inode on disk
+ ld a, [wFsDir]
+ cp BIN_INO
+ jp z, .fail ; creating inside /bin: it is ROM
+ ld a, [wFsInode]
or a
jr z, .notdir ; doesn't exist yet: no type to check
call inode_ptr
@@ -728,6 +778,11 @@ sys_open::
ld a, [wFsMode]
cp O_APPEND
jr z, .allocof ; append: keep the data, seek to the end below
+.romfile
+ ld a, [wFsMode]
+ or a
+ jr nz, .fail ; O_WRITE / O_APPEND on a program image
+ jp .allocof
.trunc
ld a, [wFsInode]
call file_truncate
@@ -850,6 +905,9 @@ sys_getb::
ld e, a
ld a, [hl]
ld d, a ; DE = pos
+ ld a, [wFsInode]
+ and BIN_FILE
+ jp nz, rom_getb ; /bin/<prog>: the bytes are in ROM, not on disk
; pos < size ?
ld a, [wFsInode]
call inode_ptr
@@ -911,6 +969,125 @@ sys_getb::
scf
ret
+binname: db "bin", 0
+
+; list_copy_name - HL = NUL-terminated name -> copy it to the caller's buffer
+; (wFsOFPtr) and return A = 1. Shared by the /bin and root-"bin" list paths.
+list_copy_name:
+ ld a, [wFsOFPtr]
+ ld e, a
+ ld a, [wFsOFPtr+1]
+ ld d, a
+ ld b, 15
+.cp
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ or a
+ jr z, .done
+ dec b
+ jr nz, .cp
+ xor a
+ ld [de], a
+.done
+ ld a, 1
+ ret
+
+; bin_list - the index-th name from the kernel's NameTable, so `ls /bin` shows
+; exactly what the shell can run: one table, one truth.
+bin_list:
+ ld hl, NameTable
+ ld a, [wFsSlot]
+ ld c, a ; entries still to skip
+.scan
+ ld a, [hl]
+ or a
+ jr z, .none ; end of table: no such index
+ ld a, c
+ or a
+ jp z, list_copy_name ; this is the one
+ dec c
+.skip
+ ld a, [hl+]
+ or a
+ jr nz, .skip ; past the name
+ inc hl ; past the program id
+ jr .scan
+.none
+ xor a
+ ret
+
+; =============================================================================
+; rom_getb - read one byte of a /bin program image. DE = position, wFsInode =
+; BIN_FILE|progid, wFsOFPtr = the open-file entry. Returns like sys_getb:
+; A = byte with CF clear, or CF set at EOF.
+;
+; A program image IS its 16 KiB ROM bank, so this maps that bank into
+; $4000-$7FFF, takes the byte, and maps the CALLER'S text bank back before
+; returning - the caller is itself running from $4000, so leaving the wrong
+; bank mapped would return into another program's code.
+; =============================================================================
+rom_getb:
+ ld a, d ; pos >= BIN_SIZE ($4000) -> EOF
+ cp HIGH(BIN_SIZE)
+ jr c, .inrange
+ scf
+ ret
+.inrange
+ push de
+ ld a, [wFsInode]
+ and $7F ; program id
+ add a
+ add a ; *4: ProgramTable entry = bank(2) + entry(2)
+ ld hl, ProgramTable
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl+]
+ ld b, a ; bank lo
+ ld a, [hl]
+ ld c, a ; bank hi
+ ld a, b
+ ld [MBC5_ROMB_LO], a
+ ld a, c
+ ld [MBC5_ROMB_HI], a
+ pop de
+ push de
+ ld a, d
+ add $40 ; HL = $4000 + pos
+ ld h, a
+ ld l, e
+ ld a, [hl]
+ ld [wFsByte], a
+ ; put the caller's own text bank back
+ ld a, [wCurProc]
+ add PROC_ROMB
+ ld l, a
+ ld a, [wCurProc+1]
+ adc 0
+ ld h, a
+ ld a, [hl+]
+ ld [MBC5_ROMB_LO], a
+ ld a, [hl]
+ ld [MBC5_ROMB_HI], a
+ pop de
+ inc de ; pos++
+ ld a, [wFsOFPtr]
+ add OF_POS
+ ld l, a
+ ld a, [wFsOFPtr+1]
+ adc 0
+ ld h, a
+ ld a, e
+ ld [hl+], a
+ ld a, d
+ ld [hl], a
+ ld a, [wFsByte]
+ and a ; CF = 0
+ ret
+
; =============================================================================
; sys_putb(B = fd, E = byte)
; =============================================================================
@@ -1030,6 +1207,21 @@ sys_list::
ld [wFsSlot], a ; wanted index
ld a, [wListDir]
ld [wFsDir], a
+ cp BIN_INO
+ jp z, bin_list ; /bin comes from the program table
+ cp ROOT_INO
+ jr nz, .ondisk
+ ; the root carries a synthetic "bin" as its first entry, so the builtins
+ ; can be found by walking the tree instead of by knowing they are there
+ ld a, [wFsSlot]
+ or a
+ jr nz, .shift
+ ld hl, binname
+ jp list_copy_name
+.shift
+ dec a
+ ld [wFsSlot], a ; the disk entries follow it
+.ondisk
call dir_load
ld c, 0 ; entry cursor
ld b, 0 ; used counter
@@ -1175,10 +1367,16 @@ sys_chdir::
or a
jr z, .fail
ld [wFsInode], a
+ cp BIN_INO
+ jr z, .setcwd ; /bin: a directory, just not one on disk
+ and BIN_FILE
+ jr nz, .fail ; a program image is not a directory
+ ld a, [wFsInode]
call inode_ptr
ld a, [hl] ; I_TYPE
cp IT_DIR
jr nz, .fail
+.setcwd
ld a, [wCurProc]
add PROC_CWD
ld l, a
@@ -1217,6 +1415,11 @@ sys_stat::
or a
jr z, .fail
ld [wFsInode], a
+ cp BIN_INO
+ jr z, .statbindir
+ and BIN_FILE
+ jr nz, .statbinfile
+ ld a, [wFsInode] ; the AND above clobbered it
call inode_ptr ; HL = &inode: type, nlink, size lo, size hi
ld a, [wFsStatReq]
add SR_TYPE
@@ -1235,6 +1438,32 @@ sys_stat::
ld [de], a ; SR_INO follows SR_SIZE
xor a
ret
+.statbindir
+ ld hl, .bindir
+ jr .statfixed
+.statbinfile
+ ld hl, .binfile
+.statfixed
+ ; type, nlink, size lo, size hi from a fixed template, then the inode
+ ld a, [wFsStatReq]
+ add SR_TYPE
+ ld e, a
+ ld a, [wFsStatReq+1]
+ adc 0
+ ld d, a
+ ld b, 4
+.cpfx
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .cpfx
+ ld a, [wFsInode]
+ ld [de], a
+ xor a
+ ret
+.bindir: db IT_DIR, 1, 0, 0
+.binfile: db IT_FILE, 1, LOW(BIN_SIZE), HIGH(BIN_SIZE)
.fail
ld a, $FF
ret
@@ -1297,10 +1526,16 @@ sys_opendir::
or a
jr z, .fail
ld [wFsInode], a
+ cp BIN_INO
+ jr z, .setdir ; /bin: listed from the program table
+ and BIN_FILE
+ jr nz, .fail
+ ld a, [wFsInode]
call inode_ptr
ld a, [hl]
cp IT_DIR
jr nz, .fail
+.setdir
ld a, [wFsInode]
ld [wListDir], a
xor a