diff options
| author | user <user@clank> | 2026-09-24 23:08:35 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-24 23:08:35 +0200 |
| commit | 1430515ad419f2fddeb0265337a0da043901411b (patch) | |
| tree | e94c53cb7d59ca4826242b81afeb3de3c655d02e /src | |
| parent | usr: touch, tr, rev, cut, uniq (diff) | |
| download | gbos-1430515ad419f2fddeb0265337a0da043901411b.tar.gz gbos-1430515ad419f2fddeb0265337a0da043901411b.tar.xz gbos-1430515ad419f2fddeb0265337a0da043901411b.zip | |
fs: hard links - ln(1), and rm that removes a NAME
gbfs has carried I_NLINK since the beginning and nothing ever incremented it,
so a file could only have one name. SYS_LINK adds a second directory entry
pointing at the same inode: the data is stored once, and both names read it.
That makes rm's job different. It now decrements the link count and only frees
the inode and its blocks when the LAST name goes - removing one of two links
used to free blocks the other still pointed at.
Refused, with the reasons that matter here: linking a directory (it would make
a cycle the tree walkers cannot survive), an existing name, and anything under
/bin, which is ROM.
The syscall takes a request block because the trap needs HL for its dispatch
table - and ln copies both paths to the stack first, since that block is a
static living at $A000, on top of the command line it is reading.
Diffstat (limited to 'src')
| -rw-r--r-- | src/fs.asm | 108 | ||||
| -rw-r--r-- | src/kdata.asm | 3 | ||||
| -rw-r--r-- | src/syscall.asm | 120 |
3 files changed, 230 insertions, 1 deletions
@@ -1275,7 +1275,25 @@ sys_remove:: or a jr z, .no ld [wFsInode], a - call file_truncate ; A = inode (frees data blocks) + ; Removing a NAME, not necessarily the file: drop the link count and only + ; free the inode and its blocks when the last link goes. Without this, + ; `rm` on one of two hard links would free blocks the other still uses. + ld a, [wFsInode] + call inode_ptr + ld a, l + add I_NLINK + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [hl] + or a + jr z, .lastlink ; 0 already (old file): treat as the last one + dec a + ld [hl], a + jr nz, .keep ; other names still point here +.lastlink + call file_truncate_cur ; frees the data blocks ld a, [wFsInode] call inode_ptr xor a @@ -1286,10 +1304,98 @@ sys_remove:: call free_inode xor a ret +.keep + ld a, [wFsInode] + call write_inode ; just the new link count + xor a + ret .no ld a, $FF ret +; file_truncate_cur - file_truncate for wFsInode (it takes the inode in A). +file_truncate_cur: + ld a, [wFsInode] + jp file_truncate + +; ============================================================================= +; sys_link(DE = &linkreq) -> A = 0 ok / $FF failed +; req[0..1] = existing path, req[2..3] = new path +; +; A hard link is just a second directory entry pointing at the same inode, +; which gbfs was already able to express - I_NLINK existed and nothing ever +; incremented it. +; ============================================================================= +sys_link:: + ld a, e + ld [wFsStatReq], a ; (shared scratch: the request pointer) + ld a, d + ld [wFsStatReq+1], a + ld h, d + ld l, e + ld a, [hl+] + ld e, a + ld a, [hl] + ld d, a ; DE = existing path + call copy_path + call resolve + or a + jp z, .fail + ld [wFsInode], a ; stash it first: the tests below clobber A + cp BIN_INO + jr z, .fail ; /bin is ROM: not linkable + and BIN_FILE + jr nz, .fail + ld a, [wFsInode] + call inode_ptr + ld a, [hl] + cp IT_FILE + jr nz, .fail ; directories: no (it would make a cycle) + ; now the new name + ld a, [wFsStatReq] + add 2 + ld l, a + ld a, [wFsStatReq+1] + adc 0 + ld h, a + ld a, [hl+] + ld e, a + ld a, [hl] + ld d, a ; DE = new path + push de + call copy_path + call resolve_parent ; A = parent dir, wFsNameBuf = new name + pop de + or a + jr z, .fail + cp BIN_INO + jr z, .fail + ld [wFsDir], a + call dir_find ; must not already exist + or a + jr nz, .fail + call dir_add ; wFsDir + wFsInode + wFsNameBuf + or a + jr z, .fail + ld a, [wFsInode] ; one more name points here + call inode_ptr + ld a, l + add I_NLINK + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [hl] + inc a + ld [hl], a + ld a, [wFsInode] + call write_inode + xor a + ret +.fail + ld a, $FF + ret + ; ============================================================================= ; sys_mkdir(DE = path) -> A = 0 ok / $FF ; ============================================================================= diff --git a/src/kdata.asm b/src/kdata.asm index b6c2b68..9322b4e 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -68,6 +68,9 @@ wBootA:: DS 1 ; A at entry: console model from the boot ROM wBootB:: DS 1 ; B at entry: bit0 set = AGB (GBA in CGB mode) wBootFsFresh::DS 1 ; fs_init formatted a blank disk this boot wTicks:: DS 4 ; monotonic 64 Hz system tick (TimerISR), LE +wEpoch:: DS 4 ; wall clock: unix seconds at wEpochTick (LE), 0 = unset +wEpochTick:: DS 4 ; the tick wEpoch was taken at (LE) +wTimeTmp:: DS 4 ; sys_gettime scratch (elapsed ticks -> seconds) wTickTog:: DS 1 ; TimerISR fires at 128 Hz in double-speed mode; ; this toggle keeps wTicks counting at 64 Hz wBatch:: DS 1 ; inside a sys_write: defer terminal tile rendering diff --git a/src/syscall.asm b/src/syscall.asm index b548328..a4719fc 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -74,6 +74,9 @@ SyscallTable: dw sys_stat ; 37 STAT dw sys_fsstat ; 38 FSSTAT dw sys_isatty ; 39 ISATTY + dw sys_link ; 40 LINK + dw sys_settime ; 41 SETTIME + dw sys_gettime ; 42 GETTIME ; ----------------------------------------------------------------------------- sys_nosys: @@ -335,6 +338,123 @@ sys_sleep: ret ; ----------------------------------------------------------------------------- +; sys_settime(DE = buf[4]) - unix seconds, little-endian. There is no RTC on an +; MBC5 cart, so the wall clock is an epoch stamp plus the tick it was taken at +; (ntpdate(1) fetches the stamp over SNTP); sys_gettime adds the elapsed ticks +; back. It does not survive a power cycle - run ntpdate from /rc. +; ----------------------------------------------------------------------------- +sys_settime: + ld hl, wEpoch + ld b, 4 +.cp + ld a, [de] + ld [hl+], a + inc de + dec b + jr nz, .cp + ld hl, wTicks ; remember when this stamp was true + ld de, wEpochTick + ld b, 4 +.cpt + ld a, [hl+] + ld [de], a + inc de + dec b + jr nz, .cpt + xor a + ret + +; ----------------------------------------------------------------------------- +; sys_gettime(DE = buf[4]) -> A = 1 if the clock has been set, 0 if not. +; buf = wEpoch + (wTicks - wEpochTick) / 64, little-endian. +; ----------------------------------------------------------------------------- +sys_gettime: + push de + ; elapsed = wTicks - wEpochTick, byte-wise with borrow (32-bit) + ld hl, wTimeTmp + ld a, [wTicks+0] + ld b, a + ld a, [wEpochTick+0] + ld c, a + ld a, b + sub c + ld [hl+], a + ld a, [wTicks+1] + ld b, a + ld a, [wEpochTick+1] + ld c, a + ld a, b + sbc c + ld [hl+], a + ld a, [wTicks+2] + ld b, a + ld a, [wEpochTick+2] + ld c, a + ld a, b + sbc c + ld [hl+], a + ld a, [wTicks+3] + ld b, a + ld a, [wEpochTick+3] + ld c, a + ld a, b + sbc c + ld [hl], a + ; elapsed >>= 6 (ticks -> seconds), 32-bit, six single-bit shifts + ld b, 6 +.shift + ld hl, wTimeTmp+3 + srl [hl] + dec hl + rr [hl] + dec hl + rr [hl] + dec hl + rr [hl] + dec b + jr nz, .shift + ; out = wEpoch + elapsed + pop de + ld hl, wEpoch + ld a, [hl+] + ld b, a + ld a, [wTimeTmp+0] + add b + ld [de], a + inc de + ld a, [hl+] + ld b, a + ld a, [wTimeTmp+1] + adc b + ld [de], a + inc de + ld a, [hl+] + ld b, a + ld a, [wTimeTmp+2] + adc b + ld [de], a + inc de + ld a, [hl] + ld b, a + ld a, [wTimeTmp+3] + adc b + ld [de], a + ; A = is the clock set? (wEpoch != 0) + ld a, [wEpoch+0] + ld b, a + ld a, [wEpoch+1] + or b + ld b, a + ld a, [wEpoch+2] + or b + ld b, a + ld a, [wEpoch+3] + or b + ret z + ld a, 1 + ret + +; ----------------------------------------------------------------------------- ; sys_isatty() -> A = 1 if this process's stdin is the console, 0 if it is a ; file or a pipe. The shell uses it to tell an interactive session from a ; script (`sh < file`): no prompt, no echo, and EOF is just the end. |
