From dc23d2f22cafeb6b97ba460789a834a6c586314d Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 19:53:53 +0200 Subject: ahci: SATA driver + FAT32 persistent disk mounted at /disk - ahci.rs: polling AHCI on PCI class 1/6/1, bus mastering + ABAR map, port 0 up, IDENTIFY + READ/WRITE DMA EXT through one 4KiB DMA buffer - fat.rs: BPB parse, whole-FAT RAM cache with write-back to both copies, LFN read/write, dirs, multi-cluster files, FSInfo free-count sync, space-padded dot entries ('..' = cluster 0 under root), fsck-clean - fs.rs: /disk mount node + 24-slot ephemeral node pool; ls/cat/hexdump/ echo/mkdir/touch/rm/edit all work on disk paths unchanged - QEMU: explicit bus assignment (disk=ide.0, ESP=ide.1); the implicit fat:rw:esp drive was stealing AHCI port 0 - Debugged against QEMU source: header DW0 is CFL in bits 4:0 / PRDTL in bits 31:16 (Linux libahci layout), FIS count at bytes 12-13, device at byte 7; QEMU rejects H2D FIS flags bits 6:4, so direction is the header W bit only - Verified: full shell round-trip on /disk, reboot persistence (poweroff then mdir/mtype and a fresh boot), fsck.fat clean --- CONTEXT.md | 41 ++- Makefile | 18 +- rsdos/src/main.rs | 10 + rsnix/src/ahci.rs | 355 +++++++++++++++++++++ rsnix/src/fat.rs | 934 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ rsnix/src/fs.rs | 164 +++++++++- rsnix/src/gop.rs | 6 +- rsnix/src/lib.rs | 2 + rsnix/src/pci.rs | 33 ++ rsnix/src/vt.rs | 4 +- scripts/qemu.sh | 5 +- 11 files changed, 1556 insertions(+), 16 deletions(-) create mode 100644 rsnix/src/ahci.rs create mode 100644 rsnix/src/fat.rs diff --git a/CONTEXT.md b/CONTEXT.md index 8fa4025..24a4b1e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -97,10 +97,18 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: - `paging.rs` — identity page-table helpers (map/unmap/query, large-page split) - `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback - `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization -- `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration +- `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration; `config_write` + - 64-bit BAR reader for the AHCI driver +- `ahci.rs` — SATA AHCI host driver (polling, DMA, no interrupts): scans PCI for + class 1/subclass 6/prog-if 1, maps ABAR, starts port 0 (FRE/ST/DMAE), runs + IDENTIFY + READ/WRITE DMA EXT through a 4 KiB DMA buffer +- `fat.rs` — FAT32: BPB parse, whole-FAT cache in RAM with write-back to both FAT + copies, cluster chains, directory scan with checksum-validated LFN + reassembly, LFN + 8.3 writing, FSInfo free-count maintenance, mkdir/rm/read/write - `fs.rs` — VFS: dirs, ramfs files, char devices (`/dev/mem`, `rtc`, `pci`, `version`, `meminfo`, `null`, `zero`, `console`, `serial`; `/tmp` ramfs; - `/etc/motd`) + `/etc/motd`); `/disk` mount node + 24-slot ephemeral node pool so shell + commands work on disk paths unchanged - `gop.rs` — framebuffer rasteriser (glyph blits, pixel shifts); the text surface itself lives in `vt` - `vt.rs` — terminal emulator: cell grid, cursor, ANSI CSI parser (H/J/K, @@ -159,6 +167,23 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: `tty` raw mode, stdout → `vt`, `std::fs` → VFS, SIGWINCH → fixed geometry. Verified in QEMU: edit/type/navigate/insert/save/exit, exact bytes via `cat`/`hexdump`, syntax colours via framebuffer dump. +✅ **AHCI + FAT32 (persistent disk)** — a SATA AHCI driver (polling DMA, + no interrupts) plus a FAT32 filesystem, mounted as `/disk`; every shell + command works on disk paths: `ls`/`cat`/`hexdump`/`echo >`/`>>`/ + `mkdir`/`touch`/`rm`/`edit`. Whole-FAT cache with write-back to both + FAT copies, checksum-validated LFN read/write, multi-cluster files, + space-padded `.`/`..` dot entries (`..` → cluster 0 under the root), + FSInfo free-count maintenance (fsck-clean after a session, verified + with `fsck.fat -n`). **Files survive reboot** — verified by + poweroff → `mdir`/`mtype` on the image and by a fresh boot seeing the + prior session's files. + + QEMU setup: `-drive id=disk,if=none,format=raw,file=disk.img` + + `-device ide-hd,drive=disk,bus=ide.0` puts the data disk on AHCI port + 0; the ESP gets `bus=ide.1`. (The implicit `-drive file=fat:rw:esp` + otherwise grabs port 0.) Make targets (`run`, `run-gui`, `debug`) + attach `$(DISK_IMG)` (default `/tmp/disk.img`). OVMF writes its + `NvVars` varstore file onto the first FAT volume — harmless. ## Known issues / perf notes @@ -181,8 +206,16 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: the whole framebuffer at all. - **Editor redraw** re-blits changed cells per keystroke (fine at 1280×800); the "optimize drawing someday" item. -- **ramfs only**: edited files live in memory, lost on reboot until disk - support (AHCI/FAT32) lands. +- **AHCI quirks learned (QEMU ground truth)**: the command header layout is + FIS length in DW0 bits 4:0 and PRDTL in bits 31:16 (not the classic + CFL-at-24 bit-fields); the H2D FIS sector count lives at bytes 12-13 with + the device register at byte 7 (byte 10 is LBA[47:40] — no separate + device byte); QEMU rejects H2D FISes with flags bits 6:4 set, so the + write direction is signalled purely by the header W bit (1<<6). + Real hardware follows the same FIS layout; the header layout matches + Linux libahci. +- **ramfs only for `/`**: the root filesystem is still the ramfs (lost on + reboot); persistent files live under `/disk` (AHCI/FAT32). ## Target audience diff --git a/Makefile b/Makefile index 1d60062..7701d9d 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ export INITRD=$(ESP_DIR)/initramfs.cpio.gz export BOOTLOADER=rsboot/rsboot.efi export OVMF=/usr/share/edk2/x64/OVMF.4m.fd export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI +# Attach a raw disk image as the SATA drive (disk.img -> ide.0, ESP -> ide.1). +# Create one with: mkfs.fat -F 32 -n RSDOS disk.img && mcopy -i disk.img files :: +export DISK_IMG ?= /tmp/disk.img .PHONY: all run run-gui clean image debug release fmt bootloader boot-image @@ -43,7 +46,10 @@ $(INITRD): | $(ESP_DIR) run: all qemu-system-x86_64 \ -bios $(OVMF) \ - -drive file=fat:rw:$(ESP_DIR),format=raw \ + -drive file=$(DISK_IMG),format=raw,if=none,id=disk \ + -device ide-hd,drive=disk,bus=ide.0 \ + -drive file=fat:rw:$(ESP_DIR),format=raw,if=none,id=esp \ + -device ide-hd,drive=esp,bus=ide.1 \ -serial stdio \ -m 256M \ -machine q35 \ @@ -54,7 +60,10 @@ run: all run-gui: all qemu-system-x86_64 \ -bios $(OVMF) \ - -drive file=fat:rw:$(ESP_DIR),format=raw \ + -drive file=$(DISK_IMG),format=raw,if=none,id=disk \ + -device ide-hd,drive=disk,bus=ide.0 \ + -drive file=fat:rw:$(ESP_DIR),format=raw,if=none,id=esp \ + -device ide-hd,drive=esp,bus=ide.1 \ -serial stdio \ -m 256M \ -machine q35 \ @@ -66,7 +75,10 @@ run-gui: all debug: all qemu-system-x86_64 \ -bios $(OVMF) \ - -drive file=fat:rw:$(ESP_DIR),format=raw \ + -drive file=$(DISK_IMG),format=raw,if=none,id=disk \ + -device ide-hd,drive=disk,bus=ide.0 \ + -drive file=fat:rw:$(ESP_DIR),format=raw,if=none,id=esp \ + -device ide-hd,drive=esp,bus=ide.1 \ -serial stdio \ -m 256M \ -machine q35 \ diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index c36660e..181d3e2 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs @@ -166,6 +166,16 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT let devices = rsnix::pci::probe(); rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); + // Disk: AHCI SATA driver, then mount a FAT32 volume if one is attached. + if !rsnix::ahci::init() { + rsnix::serial_println!("ahci: no disk attached"); + } + if rsnix::fat::init() { + rsnix::serial_println!("fat: volume mounted at /disk"); + } else { + rsnix::serial_println!("fat: no FAT32 volume"); + } + let (dirs, files) = rsnix::fs::init(); rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files); if rsnix::fs::self_test() { diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs new file mode 100644 index 0000000..83d0d27 --- /dev/null +++ b/rsnix/src/ahci.rs @@ -0,0 +1,355 @@ +// --------------------------------------------------------------------------- +// AHCI — SATA host-controller driver (polling, DMA, no interrupts). +// +// Finds the chipset's AHCI controller (class 1 / subclass 6 / prog-if 1, +// e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts port 0 +// and exposes sector-level reads/writes for the FAT32 filesystem. +// +// Commands are issued through command-slot 0 with one 4 KiB DMA buffer; +// completion is polled via PxCI (no ISR, no MSI). Everything is +// identity-mapped, so "physical" DMA addresses are plain kernel pointers. +// The display is never involved, so this is a straightforward polling +// loop like the rest of the single-threaded kernel. +// --------------------------------------------------------------------------- + +use crate::pci; + +const SECTOR: usize = 512; + +// ABAR (HBA memory space) offsets. +const GHC: usize = 0x04; +const PI: usize = 0x0C; +const PORT_BASE: usize = 0x100; +const PORT_STRIDE: usize = 0x80; + +// Port register offsets (relative to the port's base). +const P_CLB: usize = 0x00; +const P_CLBU: usize = 0x04; +const P_FB: usize = 0x08; +const P_FBU: usize = 0x0C; +const P_IS: usize = 0x10; +const P_CMD: usize = 0x18; +const P_TFD: usize = 0x20; +const P_SSTS: usize = 0x28; +const P_SCTL: usize = 0x2C; +const P_CI: usize = 0x38; + +const CMD_ST: u32 = 1 << 0; // start +const CMD_SUD: u32 = 1 << 1; // spin-up device +const CMD_FRE: u32 = 1 << 4; // FIS receive enable +const CMD_DMAE: u32 = 1 << 23; // DMA enable (QEMU/ICH9) — required or DMA commands silently transfer nothing +const CMD_FR: u32 = 1 << 14; // FIS receive running +const CMD_CR: u32 = 1 << 15; // command list running + +const ATA_IDENTIFY: u8 = 0xEC; +const ATA_READ_DMA_EXT: u8 = 0x25; +const ATA_WRITE_DMA_EXT: u8 = 0x35; + +/// One attached disk: register map + DMA structures. +struct Disk { + port: usize, // port register base = ABAR + 0x100 + clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned + fis: usize, // FIS receive area, 256 B aligned + ct: usize, // command table, 256 B, 128 B aligned + buf: usize, // 4 KiB DMA data buffer (page aligned) + sectors: u64, // total sectors (LBA48 from IDENTIFY) +} + +static mut DISK: Option = None; + +fn reg(a: usize) -> u32 { + unsafe { core::ptr::read_volatile(a as *const u32) } +} + +fn wreg(a: usize, v: u32) { + unsafe { core::ptr::write_volatile(a as *mut u32, v) } +} + +/// Scan bus 0 for the AHCI controller (SATA, AHCI prog-if). +fn find_ahci() -> Option<(u8, u8)> { + for dev in 0..32u8 { + for func in 0..8u8 { + let id = pci::config_read(0, dev, func, 0); + if id == 0xFFFF_FFFF { + if func == 0 { + break; + } + continue; + } + let cc = pci::config_read(0, dev, func, 0x08); + let class = (cc >> 24) as u8; + let subclass = (cc >> 16) as u8; + let prog_if = (cc >> 8) as u8; + if class == 0x01 && subclass == 0x06 && prog_if == 0x01 { + return Some((dev, func)); + } + } + } + None +} + +/// Probe, map and start the first AHCI port. Returns true if a disk is +/// ready for sector I/O. Called once at boot before the filesystem init. +pub fn init() -> bool { + let Some((dev, func)) = find_ahci() else { + return false; + }; + // Enable memory space + bus mastering so MMIO and DMA work. + let cmd_reg = pci::config_read(0, dev, func, 0x04); + pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); + + let Some(abar) = pci::bar64(0, dev, func, 5) else { + return false; + }; + crate::serial_println!( + "ahci: controller at {:02x}:{:02x}.{} abar={:#x}", + 0, + dev, + func, + abar + ); + + // AHCI enable (GHC.AE) before touching port registers. + wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31)); + + // Pick the first implemented port. + let pi = reg(abar as usize + PI); + let mut port_idx = 0u32; + while port_idx < 32 && (pi >> port_idx) & 1 == 0 { + port_idx += 1; + } + if port_idx >= 32 { + crate::serial_println!("ahci: no ports implemented (pi={:#x})", pi); + return false; + } + let port = abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE; + + // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd + // table, page 1 = data buffer. + let Some(mem) = crate::memory::alloc_pages(2) else { + crate::serial_println!("ahci: cannot allocate DMA pages"); + return false; + }; + let clb = mem; + let fis = mem + 1024; + let ct = mem + 1280; + let buf = mem + 4096; + + // Stop the port if firmware left it running. + let mut cmd = reg(port + P_CMD); + if cmd & CMD_ST != 0 { + wreg(port + P_CMD, cmd & !CMD_ST); + let mut i = 0; + while reg(port + P_CMD) & CMD_CR != 0 && i < 1_000_000 { + i += 1; + } + cmd = reg(port + P_CMD); + } + if cmd & CMD_FRE != 0 { + wreg(port + P_CMD, cmd & !CMD_FRE); + let mut i = 0; + while reg(port + P_CMD) & CMD_FR != 0 && i < 1_000_000 { + i += 1; + } + } + unsafe { + core::ptr::write_bytes(clb as *mut u8, 0, 1024); + core::ptr::write_bytes(fis as *mut u8, 0, 256); + core::ptr::write_bytes(ct as *mut u8, 0, 256); + } + + wreg(port + P_CLB, clb as u32); + wreg(port + P_CLBU, 0); + wreg(port + P_FB, fis as u32); + wreg(port + P_FBU, 0); + + // Allow device detection (clear PxSCTL.DET/IPM), then wait for the + // link to come up (PxSSTS.DET == 3, communication established). + wreg(port + P_SCTL, 0); + let mut link_ok = false; + for _ in 0..10_000_000 { + if reg(port + P_SSTS) & 0xF == 3 { + link_ok = true; + break; + } + } + if !link_ok { + crate::serial_println!( + "ahci: no device on port {} (ssts={:#x})", + port_idx, + reg(port + P_SSTS) + ); + return false; + } + + // Start the command engine (DMAE required for DMA commands). + wreg(port + P_CMD, CMD_FRE | CMD_SUD | CMD_DMAE); + wreg(port + P_CMD, CMD_ST | CMD_FRE | CMD_SUD | CMD_DMAE); + + let mut d = Disk { + port, + clb, + fis, + ct, + buf, + sectors: 0, + }; + // IDENTIFY DEVICE as a sanity check + to learn the capacity. + if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() { + crate::serial_println!("ahci: IDENTIFY failed"); + return false; + } + let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) }; + d.sectors = (id[100] as u64) + | ((id[101] as u64) << 16) + | ((id[102] as u64) << 32) + | ((id[103] as u64) << 48); + if d.sectors == 0 { + d.sectors = id[60] as u64; // LBA28 fallback + } + let mut model = [0u8; 40]; + for i in 0..20 { + model[i * 2] = (id[27 + i] >> 8) as u8; + model[i * 2 + 1] = id[27 + i] as u8; + } + let model = core::str::from_utf8(&model).unwrap_or("?").trim(); + crate::serial_println!( + "ahci: port {} up — {} sectors (~{} MB) \"{}\"", + port_idx, + d.sectors, + d.sectors / 2048, + model + ); + unsafe { + DISK = Some(d); + } + true +} + +/// Issue one command through slot 0 and poll for completion. +/// `count` is sectors (1-8); data flows through the disk's DMA buffer. +fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> { + if count == 0 || count > 8 { + return Err(()); + } + // Wait for slot 0 to be free. + let mut i = 0; + while reg(d.port + P_CI) & 1 != 0 && i < 10_000_000 { + i += 1; + } + if reg(d.port + P_CI) & 1 != 0 { + return Err(()); + } + + unsafe { + core::ptr::write_bytes(d.ct as *mut u8, 0, 256); + } + + // Host-to-device FIS (20 bytes) at the command table start. + let fis = d.ct as *mut u8; + unsafe { + *fis = 0x27; // FIS type H2D + *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4 + *(fis.add(2)) = cmd; + // FIS field layout (SATA spec; verified against QEMU/Linux): + // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48], + // 11 features high, 12-13 sector count. + *(fis.add(4)) = lba as u8; + *(fis.add(5)) = (lba >> 8) as u8; + *(fis.add(6)) = (lba >> 16) as u8; + *(fis.add(7)) = 0x40 | (((lba >> 24) & 0x0F) as u8); + *(fis.add(8)) = (lba >> 24) as u8; + *(fis.add(9)) = (lba >> 32) as u8; + *(fis.add(10)) = (lba >> 40) as u8; + *(fis.add(11)) = 0; // features high + *(fis.add(12)) = count as u8; + *(fis.add(13)) = (count >> 8) as u8; + *(fis.add(14)) = 0; // ICC + *(fis.add(15)) = 0; // control + } + + // PRD entry at the command table +0x80. + let prd = (d.ct + 0x80) as *mut u32; + unsafe { + *prd = d.buf as u32; + *prd.add(1) = 0; + *prd.add(2) = 0; + *prd.add(3) = (count * SECTOR as u32) - 1; // DBC, no I bit + } + + // Command header 0. Layout per Linux libahci / QEMU AHCICmdHdr: + // DW0 bits 4:0 = CFL (FIS length in dwords), bit 6 = W, + // bits 31:16 = PRDTL; DW1 status; DW2/3 = command table address. + let hdr = d.clb as *mut u32; + unsafe { + *hdr = 5 | (1 << 16) | if write { 1 << 6 } else { 0 }; + *hdr.add(1) = 0; + *hdr.add(2) = d.ct as u32; + *hdr.add(3) = 0; + } + + // Issue and poll. + wreg(d.port + P_CI, 1); + let mut i = 0; + while reg(d.port + P_CI) & 1 != 0 && i < 100_000_000 { + i += 1; + } + if reg(d.port + P_CI) & 1 != 0 { + crate::serial_println!( + "ahci: command {:#x} lba={} cnt={} timed out (tfd={:#x} ci={:#x} cmd={:#x} prdbc={:#x})", + cmd, + lba, + count, + reg(d.port + P_TFD), + reg(d.port + P_CI), + reg(d.port + P_CMD), + unsafe { *(d.clb as *const u32).add(1) } + ); + return Err(()); + } + // Error? Clear any port error bits either way. + if reg(d.port + P_TFD) & 1 != 0 { + wreg(d.port + 0x30, 0xFFFF_FFFF); // PxSERR: clear + crate::serial_println!( + "ahci: command {:#x} error (tfd={:#x})", + cmd, + reg(d.port + P_TFD) + ); + return Err(()); + } + let is = reg(d.port + P_IS); + if is != 0 { + wreg(d.port + P_IS, is); // write-1-to-clear + } + Ok(()) +} + +/// DMA `count` sectors starting at `lba` into the internal buffer. +pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { + let d = unsafe { DISK.as_ref() }.ok_or(())?; + if lba + count as u64 > d.sectors { + return Err(()); + } + issue(d, ATA_READ_DMA_EXT, lba, count, false) +} + +/// DMA `count` sectors starting at `lba` out of the internal buffer. +pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { + let d = unsafe { DISK.as_ref() }.ok_or(())?; + if lba + count as u64 > d.sectors { + return Err(()); + } + issue(d, ATA_WRITE_DMA_EXT, lba, count, true) +} + +/// Pointer to the internal 4 KiB DMA buffer. +pub fn buf() -> *mut u8 { + unsafe { DISK.as_ref() } + .map(|d| d.buf as *mut u8) + .unwrap_or(core::ptr::null_mut()) +} + +/// Total sector count of the attached disk. +pub fn sector_count() -> u64 { + unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0) +} diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs new file mode 100644 index 0000000..70fa688 --- /dev/null +++ b/rsnix/src/fat.rs @@ -0,0 +1,934 @@ +// --------------------------------------------------------------------------- +// FAT32 — read/write a FAT32 volume over the AHCI sector interface. +// +// Mounts the volume's boot sector (BPB), caches the whole FAT in RAM, +// follows cluster chains, and exposes file/directory operations used by +// the VFS's Disk nodes: list, find, read, write, truncate, create (files +// and dirs), remove. Long filenames are read back and written for names +// that don't fit 8.3; short names are generated the standard way +// (uppercased, 8+3, "~N" truncation). All timestamps are zero — this is +// a toy kernel, not a backup tool. +// +// Single-threaded like the rest of the kernel; no caching beyond the FAT +// itself, so every op re-reads directory sectors (cheap: the disk is +// polled DMA at µs scale in QEMU). +// --------------------------------------------------------------------------- + +extern crate alloc; + +use crate::ahci; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +const SECTOR: usize = 512; +const EOC_MASK: u32 = 0x0FFF_FFFF; +const EOC: u32 = 0x0FFF_FFFF; + +/// One directory-listed entry (or a resolved file). +pub struct Entry { + pub name: String, + pub is_dir: bool, + pub size: u64, + pub cluster: u32, +} + +struct Volume { + sectors_per_cluster: u8, + reserved: u16, + fat_count: u8, + fat_size: u32, // sectors per FAT + root_cluster: u32, + first_data: u32, // LBA of cluster 2 + cluster_count: u32, + fsinfo: Option, // sector number of the FSInfo structure + fat: Vec, // FAT 1 cached in RAM +} + +static mut VOL: Option = None; + +pub fn is_mounted() -> bool { + unsafe { VOL.is_some() } +} + +pub fn root_cluster() -> u32 { + unsafe { VOL.as_ref() }.map(|v| v.root_cluster).unwrap_or(0) +} + +// --- sector I/O ----------------------------------------------------------- + +fn read_sector(lba: u64, out: &mut [u8; SECTOR]) -> Result<(), ()> { + ahci::read_sectors(lba, 1)?; + unsafe { core::ptr::copy(ahci::buf(), out.as_mut_ptr(), SECTOR) }; + Ok(()) +} + +fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> { + unsafe { core::ptr::copy(data.as_ptr(), ahci::buf(), SECTOR) }; + ahci::write_sectors(lba, 1) +} + +// --- cluster math ---------------------------------------------------------- + +fn v() -> &'static mut Volume { + unsafe { VOL.as_mut() }.unwrap() +} + +fn spc() -> usize { + v().sectors_per_cluster as usize +} + +fn cluster_size() -> usize { + spc() * SECTOR +} + +fn cluster_lba(c: u32) -> u64 { + v().first_data as u64 + ((c as u64 - 2) * spc() as u64) +} + +fn fat_off(c: u32) -> usize { + c as usize * 4 +} + +fn read_fat_entry(c: u32) -> u32 { + let off = fat_off(c); + u32::from_le_bytes([ + v().fat[off], + v().fat[off + 1], + v().fat[off + 2], + v().fat[off + 3], + ]) & EOC_MASK +} + +/// Update a FAT entry in the cache and write the affected sector back to +/// both FAT copies. +fn write_fat_entry(c: u32, val: u32) -> Result<(), ()> { + let off = fat_off(c); + v().fat[off..off + 4].copy_from_slice(&(val & EOC_MASK).to_le_bytes()); + let sector_idx = off / SECTOR; + let mut buf = [0u8; SECTOR]; + buf.copy_from_slice(&v().fat[sector_idx * SECTOR..(sector_idx + 1) * SECTOR]); + let lba1 = v().reserved as u64 + sector_idx as u64; + let lba2 = lba1 + v().fat_size as u64; + write_sector(lba1, &buf)?; + write_sector(lba2, &buf) +} + +fn is_eoc(c: u32) -> bool { + c >= 0x0FFF_FFF8 +} + +fn next_cluster(c: u32) -> Result { + let n = read_fat_entry(c); + if is_eoc(n) { + Err(()) + } else { + Ok(n) + } +} + +/// Allocate a fresh cluster: mark it EOC in the FAT and zero its data. +fn alloc_cluster() -> Result { + let total = v().cluster_count + 2; + for c in 2..total { + if read_fat_entry(c) == 0 { + write_fat_entry(c, EOC)?; + let zero = vec![0u8; cluster_size()]; + write_cluster(c, &zero)?; + fsinfo_adjust(-1); + return Ok(c); + } + } + crate::serial_println!("fat: disk full"); + Err(()) +} + +fn free_chain(mut c: u32) { + let mut freed = 0u64; + let mut guard = 0; + while !is_eoc(read_fat_entry(c)) && guard < 1_000_000 { + let nxt = read_fat_entry(c); + let _ = write_fat_entry(c, 0); + c = nxt; + freed += 1; + guard += 1; + } + let _ = write_fat_entry(c, 0); + freed += 1; + if freed > 0 { + fsinfo_adjust(freed as i64); + } +} + +/// Keep FSInfo.FSFreeCount in sync with cluster allocations/frees so +/// fsck and other tools see an accurate free count. Falls back to a full +/// recount when the stored value is the "unknown" marker. +fn fsinfo_adjust(delta: i64) { + let Some(s) = v().fsinfo else { + return; + }; + let mut b = [0u8; SECTOR]; + if read_sector(s as u64, &mut b).is_err() { + return; + } + if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" { + return; // not a valid FSInfo structure + } + let mut n = u32::from_le_bytes([b[488], b[489], b[490], b[491]]); + if n == 0xFFFF_FFFF { + n = free_clusters(); + } + let n = (n as i64 + delta).max(0) as u32; + b[488..492].copy_from_slice(&n.to_le_bytes()); + let _ = write_sector(s as u64, &b); +} + +/// Rewrite FSInfo.FSFreeCount with the true free-cluster count. +fn fsinfo_recount() { + let Some(s) = v().fsinfo else { + return; + }; + let mut b = [0u8; SECTOR]; + if read_sector(s as u64, &mut b).is_err() { + return; + } + if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" { + return; + } + let n = free_clusters(); + b[488..492].copy_from_slice(&n.to_le_bytes()); + let _ = write_sector(s as u64, &b); +} + +/// Count free clusters by scanning the cached FAT. +fn free_clusters() -> u32 { + let mut n = 0u32; + for c in 2..v().cluster_count + 2 { + if read_fat_entry(c) == 0 { + n += 1; + } + } + n +} + +fn read_cluster(c: u32, buf: &mut [u8]) -> Result<(), ()> { + if spc() <= 8 { + ahci::read_sectors(cluster_lba(c), spc() as u32)?; + unsafe { core::ptr::copy(ahci::buf(), buf.as_mut_ptr(), cluster_size()) }; + } else { + for s in 0..spc() { + ahci::read_sectors(cluster_lba(c) + s as u64, 1)?; + unsafe { + core::ptr::copy(ahci::buf(), buf.as_mut_ptr().add(s * SECTOR), SECTOR); + } + } + } + Ok(()) +} + +fn write_cluster(c: u32, buf: &[u8]) -> Result<(), ()> { + if spc() <= 8 { + unsafe { core::ptr::copy(buf.as_ptr(), ahci::buf(), cluster_size()) }; + ahci::write_sectors(cluster_lba(c), spc() as u32) + } else { + for s in 0..spc() { + unsafe { + core::ptr::copy(buf.as_ptr().add(s * SECTOR), ahci::buf(), SECTOR); + } + ahci::write_sectors(cluster_lba(c) + s as u64, 1)?; + } + Ok(()) + } +} + +// --- directory scanning ---------------------------------------------------- + +/// Checksum of an SFN's 11 bytes; used to bind LFN entries to their SFN. +fn lfn_checksum(sfn: &[u8; 11]) -> u8 { + let mut s: u8 = 0; + for &b in sfn { + s = s.rotate_right(1).wrapping_add(b); + } + s +} + +fn sfn_decode(e: &[u8]) -> String { + let nts = e[12]; + let mut s = String::new(); + for &b in &e[0..8] { + if b == b' ' || b == 0 { + break; + } + let c = if nts & 0x08 != 0 { + b.to_ascii_lowercase() + } else { + b + }; + s.push(c as char); + } + let mut ext = String::new(); + for &b in &e[8..11] { + if b == b' ' || b == 0 { + break; + } + let c = if nts & 0x10 != 0 { + b.to_ascii_lowercase() + } else { + b + }; + ext.push(c as char); + } + if !ext.is_empty() { + s.push('.'); + s.push_str(&ext); + } + if s.is_empty() { + s.push('?'); + } + s +} + +fn utf16_to_string(v: &[u16]) -> String { + let mut s = String::new(); + for &c in v { + if c == 0 { + break; + } + if let Some(ch) = char::from_u32(c as u32) { + s.push(ch); + } + } + s +} + +struct DirItem { + name: String, + attr: u8, + cluster: u32, + size: u32, + sector: u64, // LBA of the entry's sector (for in-place updates) + offset: usize, +} + +/// Scan a directory's cluster chain, reassembling long filenames. +fn scan_dir(dir_cluster: u32) -> Result, ()> { + let cs = cluster_size(); + let mut buf = vec![0u8; cs]; + let mut items = Vec::new(); + let mut lfn: Vec = Vec::new(); + let mut lfn_sum = 0u8; + let mut cur: Option = Some(dir_cluster); + let mut guard = 0; + while let Some(c) = cur { + guard += 1; + if guard > 1_000_000 { + return Err(()); // loop protection on a corrupt chain + } + read_cluster(c, &mut buf)?; + let mut done = false; + for (i, e) in buf.chunks_exact(32).enumerate() { + match e[0] { + 0x00 => { + done = true; + break; + } + 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN + _ => { + let attr = e[11]; + if attr == 0x0F { + // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, 28-31). + for k in 0..13 { + let pos = if k < 5 { + 1 + k * 2 + } else if k < 11 { + 14 + (k - 5) * 2 + } else { + 28 + (k - 11) * 2 + }; + let ch = u16::from_le_bytes([e[pos], e[pos + 1]]); + if ch == 0x0000 { + break; + } + if ch != 0xFFFF { + lfn.push(ch); + } + } + lfn_sum = e[13]; + } else { + // Short-name entry: bind any collected LFN. + let mut sfn = [0u8; 11]; + sfn.copy_from_slice(&e[0..11]); + let name = if !lfn.is_empty() && lfn_checksum(&sfn) == lfn_sum { + utf16_to_string(&lfn) + } else { + sfn_decode(e) + }; + let cluster = u16::from_le_bytes([e[26], e[27]]) as u32 + | ((u16::from_le_bytes([e[20], e[21]]) as u32) << 16); + items.push(DirItem { + name, + attr, + cluster, + size: u32::from_le_bytes([e[28], e[29], e[30], e[31]]), + sector: cluster_lba(c) + (i * 32 / SECTOR) as u64, + offset: (i * 32) % SECTOR, + }); + lfn.clear(); + } + } + } + } + if done { + break; + } + cur = next_cluster(c).ok(); + } + Ok(items) +} + +/// Public listing: no `.` / `..`. +pub fn list_dir(dir_cluster: u32) -> Result, ()> { + Ok(scan_dir(dir_cluster)? + .into_iter() + .filter(|i| i.name != "." && i.name != "..") + .map(|i| Entry { + name: i.name, + is_dir: i.attr & 0x10 != 0, + size: i.size as u64, + cluster: i.cluster, + }) + .collect()) +} + +/// Find an entry by name (case-insensitive), returning the scan record +/// (which knows the entry's sector for in-place updates). +fn find_item(parent_dir_cluster: u32, name: &str) -> Result { + for i in scan_dir(parent_dir_cluster)? { + if i.name.eq_ignore_ascii_case(name) { + return Ok(i); + } + } + Err(()) +} + +/// Find an entry by name (case-insensitive). +pub fn find(parent_dir_cluster: u32, name: &str) -> Result { + find_item(parent_dir_cluster, name).map(|i| Entry { + name: i.name, + is_dir: i.attr & 0x10 != 0, + size: i.size as u64, + cluster: i.cluster, + }) +} + +// --- file data -------------------------------------------------------------- + +pub fn read_file( + parent_dir_cluster: u32, + name: &str, + buf: &mut [u8], + pos: u64, +) -> Result { + if buf.is_empty() { + return Ok(0); + } + let it = find(parent_dir_cluster, name)?; + if it.is_dir || it.cluster == 0 { + return Err(()); + } + if pos >= it.size { + return Ok(0); + } + let cs = cluster_size() as u64; + let mut cluster = it.cluster; + let mut skip = pos; + while skip >= cs { + cluster = next_cluster(cluster)?; + skip -= cs; + } + let mut cbuf = vec![0u8; cluster_size()]; + let mut remaining = buf.len() as u64; + if remaining > it.size - pos { + remaining = it.size - pos; + } + let mut out = 0usize; + while remaining > 0 { + read_cluster(cluster, &mut cbuf)?; + let from = skip as usize; + let n = ((cs as usize) - from).min(remaining as usize); + buf[out..out + n].copy_from_slice(&cbuf[from..from + n]); + out += n; + remaining -= n as u64; + if remaining > 0 { + cluster = next_cluster(cluster)?; + } + skip = 0; + } + Ok(out) +} + +pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result { + if buf.is_empty() { + return Ok(0); + } + let it = find_item(parent_dir_cluster, name)?; + if it.attr & 0x10 != 0 { + return Err(()); + } + let cs = cluster_size() as u64; + let end = pos + buf.len() as u64; + + // Ensure a first cluster exists. + let mut cluster = it.cluster; + if cluster == 0 { + cluster = alloc_cluster()?; + update_entry(&it, Some(cluster), None)?; + } + // Walk to the cluster containing `pos`, extending the chain as needed. + let mut skip = pos; + let mut cur = cluster; + while skip >= cs { + match next_cluster(cur) { + Ok(n) => cur = n, + Err(_) => { + let n = alloc_cluster()?; + write_fat_entry(cur, n)?; + cur = n; + } + } + skip -= cs; + } + // Write the data. + let mut cbuf = vec![0u8; cluster_size()]; + let mut remaining = buf.len() as u64; + let mut in_off = 0usize; + while remaining > 0 { + read_cluster(cur, &mut cbuf)?; + let from = skip as usize; + let n = ((cs as usize) - from).min(remaining as usize); + cbuf[from..from + n].copy_from_slice(&buf[in_off..in_off + n]); + write_cluster(cur, &cbuf)?; + in_off += n; + remaining -= n as u64; + if remaining > 0 { + // Make sure the chain continues past this cluster. + match next_cluster(cur) { + Ok(n) => cur = n, + Err(_) => { + let n = alloc_cluster()?; + write_fat_entry(cur, n)?; + cur = n; + } + } + } + skip = 0; + } + if end > it.size as u64 { + update_entry(&it, None, Some(end as u32))?; + } + Ok(buf.len()) +} + +pub fn set_len(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { + let it = find_item(parent_dir_cluster, name)?; + if it.attr & 0x10 != 0 { + return Err(()); + } + let cs = cluster_size() as u64; + let needed = if len == 0 { + 0 + } else { + ((len - 1) / cs + 1) as usize + }; + let have = chain_len(it.cluster); + if it.cluster == 0 { + if needed == 0 { + update_entry(&it, None, Some(0))?; + } else { + let first = alloc_cluster()?; + if needed > 1 { + extend_chain(first, needed - 1)?; + } + update_entry(&it, Some(first), Some(len as u32))?; + } + } else if needed < have { + truncate_chain(it.cluster, needed); + update_entry( + &it, + if needed == 0 { Some(0) } else { None }, + Some(len as u32), + )?; + } else if needed > have { + extend_chain(it.cluster, needed - have)?; + update_entry(&it, None, Some(len as u32))?; + } else { + update_entry(&it, None, Some(len as u32))?; + } + Ok(()) +} + +fn chain_len(mut c: u32) -> usize { + let mut n = 0usize; + let mut guard = 0; + while c != 0 && !is_eoc(read_fat_entry(c)) && guard < 1_000_000 { + c = read_fat_entry(c); + n += 1; + guard += 1; + } + if c != 0 { + n += 1; + } + n +} + +fn truncate_chain(mut c: u32, keep: usize) { + if keep == 0 { + free_chain(c); + return; + } + let mut idx = 0usize; + let mut guard = 0; + while idx + 1 < keep { + let n = read_fat_entry(c); + if is_eoc(n) { + return; + } + c = n; + idx += 1; + guard += 1; + if guard > 1_000_000 { + return; + } + } + let nxt = read_fat_entry(c); + if !is_eoc(nxt) { + free_chain(nxt); + let _ = write_fat_entry(c, EOC); + } +} + +fn extend_chain(c: u32, add: usize) -> Result<(), ()> { + let mut last = c; + let mut guard = 0; + loop { + let n = read_fat_entry(last); + if is_eoc(n) { + break; + } + last = n; + guard += 1; + if guard > 1_000_000 { + return Err(()); + } + } + for _ in 0..add { + let n = alloc_cluster()?; + write_fat_entry(last, n)?; + last = n; + } + Ok(()) +} + +/// Rewrite one field of a directory entry in place (its sector was cached +/// at scan time; the parent dir chain never moves while we hold it). +fn update_entry(it: &DirItem, cluster: Option, size: Option) -> Result<(), ()> { + let mut sec = [0u8; SECTOR]; + read_sector(it.sector, &mut sec)?; + let e = &mut sec[it.offset..it.offset + 32]; + if let Some(c) = cluster { + e[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes())); + e[26..28].copy_from_slice(&((c as u16).to_le_bytes())); + } + if let Some(s) = size { + e[28..32].copy_from_slice(&s.to_le_bytes()); + } + write_sector(it.sector, &sec) +} + +// --- creation / removal ------------------------------------------------------ + +/// Uppercased 8.3 short name for `name`; `needs_lfn` if the original name +/// can't round-trip through the short name (too long or lowercase). +fn to_8_3(name: &str) -> Option<([u8; 11], bool)> { + if name.is_empty() || name.contains('/') || name == "." || name == ".." { + return None; + } + let upper = name.to_ascii_uppercase(); + let (n, e) = match upper.rfind('.') { + Some(i) => (&upper[..i], Some(&upper[i + 1..])), + None => (upper.as_str(), None), + }; + if n.is_empty() { + return None; + } + let mut sfn = [b' '; 11]; + let nb = n.as_bytes(); + for i in 0..8 { + if i < nb.len() { + sfn[i] = nb[i]; + } + } + if let Some(ext) = e { + let eb = ext.as_bytes(); + for i in 0..3 { + if i < eb.len() { + sfn[8 + i] = eb[i]; + } + } + } + let needs = nb.len() > 8 || e.map(|x| x.len() > 3).unwrap_or(false) || name != upper; + Some((sfn, needs)) +} + +/// Write the LFN entries (if any) + SFN entry for `name` into the parent +/// directory, extending its chain with a fresh zeroed cluster if needed. +fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> { + let (sfn, needs_lfn) = to_8_3(name).ok_or(())?; + let lfn_chars: Vec = name.encode_utf16().collect(); + let groups = if needs_lfn { + lfn_chars.len().div_ceil(13) + } else { + 0 + }; + let needed = groups + 1; + let cs = cluster_size(); + let mut buf = vec![0u8; cs]; + let mut cur: Option = Some(parent); + let mut guard = 0; + loop { + guard += 1; + if guard > 1_000_000 { + return Err(()); + } + let c = cur.ok_or(())?; + read_cluster(c, &mut buf)?; + let slots = cs / 32; + let mut run = 0usize; + let mut start = 0usize; + for i in 0..slots { + if buf[i * 32] == 0x00 || buf[i * 32] == 0xE5 { + run += 1; + if run == 1 { + start = i; + } + if run >= needed { + break; + } + } else { + run = 0; + } + } + if run >= needed { + if groups > 0 { + for g in 0..groups { + let seq = (groups - g) as u8 | if g == groups - 1 { 0x40 } else { 0 }; + let e = &mut buf[(start + g) * 32..(start + g) * 32 + 32]; + e.fill(0); + e[0] = seq; + e[11] = 0x0F; + e[13] = lfn_checksum(&sfn); + for k in 0..13 { + let ci = g * 13 + k; + let ch: u16 = if ci < lfn_chars.len() { + lfn_chars[ci] + } else if ci == lfn_chars.len() { + 0 + } else { + 0xFFFF + }; + let pos = if k < 5 { + 1 + k * 2 + } else if k < 11 { + 14 + (k - 5) * 2 + } else { + 28 + (k - 11) * 2 + }; + e[pos] = (ch & 0xFF) as u8; + e[pos + 1] = (ch >> 8) as u8; + } + } + } + let e = &mut buf[(start + groups) * 32..(start + groups) * 32 + 32]; + e.fill(0); + e[0..11].copy_from_slice(&sfn); + e[11] = if is_dir { 0x10 } else { 0x20 }; + e[20..22].copy_from_slice(&(((cluster >> 16) as u16).to_le_bytes())); + e[26..28].copy_from_slice(&((cluster as u16).to_le_bytes())); + write_cluster(c, &buf)?; + return Ok(()); + } + // Chain full: extend with a fresh zeroed cluster and retry. + match next_cluster(c) { + Ok(n) => cur = Some(n), + Err(_) => { + let n = alloc_cluster()?; + write_fat_entry(c, n)?; + cur = Some(n); + } + } + } +} + +pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { + if find(parent_dir_cluster, name).is_ok() { + return Err(()); + } + let cluster = if is_dir { + let c = alloc_cluster()?; + // "." and ".." entries. + let cs = cluster_size(); + let mut buf = vec![0u8; cs]; + let dot = &mut buf[0..32]; + dot.fill(0); + dot[0] = b'.'; + dot[1..11].fill(b' '); // SFN is space-padded + dot[11] = 0x10; + dot[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes())); + dot[26..28].copy_from_slice(&((c as u16).to_le_bytes())); + let dotdot = &mut buf[32..64]; + dotdot.fill(0); + dotdot[0] = b'.'; + dotdot[1] = b'.'; + dotdot[2..11].fill(b' '); // SFN is space-padded + dotdot[11] = 0x10; + // ".." points at the parent; the root has cluster number 0. + let parent_cluster = if parent_dir_cluster == v().root_cluster { + 0 + } else { + parent_dir_cluster + }; + dotdot[20..22].copy_from_slice(&(((parent_cluster >> 16) as u16).to_le_bytes())); + dotdot[26..28].copy_from_slice(&((parent_cluster as u16).to_le_bytes())); + write_cluster(c, &buf)?; + c + } else { + 0 + }; + add_dir_entry(parent_dir_cluster, name, is_dir, cluster) +} + +pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { + let it = find_item(parent_dir_cluster, name)?; + if it.attr & 0x10 != 0 { + // Only empty directories may be removed. + let kids = list_dir(it.cluster)?; + if !kids.is_empty() { + return Err(()); + } + free_chain(it.cluster); + } else if it.cluster != 0 { + free_chain(it.cluster); + } + // Mark the SFN and its leading LFN entries deleted. + let mut sec = [0u8; SECTOR]; + read_sector(it.sector, &mut sec)?; + let mut off = it.offset; + sec[off] = 0xE5; + while off >= 32 { + let prev = &sec[off - 32..off]; + if prev[11] == 0x0F { + sec[off - 32] = 0xE5; + off -= 32; + } else { + break; + } + } + write_sector(it.sector, &sec) +} + +// --- mount ------------------------------------------------------------------- + +/// Parse the BPB, cache the FAT and validate the volume. Returns true if +/// a usable FAT32 volume is mounted. +pub fn init() -> bool { + let mut bpb = [0u8; SECTOR]; + if read_sector(0, &mut bpb).is_err() { + crate::serial_println!("fat: no disk"); + return false; + } + let bytes_per_sector = u16::from_le_bytes([bpb[11], bpb[12]]); + if bytes_per_sector != 512 { + crate::serial_println!("fat: unsupported sector size {}", bytes_per_sector); + return false; + } + let spc = bpb[13]; + if spc == 0 || spc & (spc - 1) != 0 { + crate::serial_println!("fat: bad sectors/cluster {}", spc); + return false; + } + let reserved = u16::from_le_bytes([bpb[14], bpb[15]]); + let fat_count = bpb[16]; + let total16 = u16::from_le_bytes([bpb[19], bpb[20]]); + let total32 = u32::from_le_bytes([bpb[32], bpb[33], bpb[34], bpb[35]]); + let total = if total16 != 0 { + total16 as u32 + } else { + total32 + }; + let fat_size = u32::from_le_bytes([bpb[36], bpb[37], bpb[38], bpb[39]]); + let root_cluster = u32::from_le_bytes([bpb[44], bpb[45], bpb[46], bpb[47]]); + let fsinfo_sec = u16::from_le_bytes([bpb[48], bpb[49]]) as u32; + if fat_size == 0 || root_cluster < 2 || reserved == 0 || fat_count == 0 { + crate::serial_println!("fat: not FAT32 (root={} fat={})", root_cluster, fat_size); + return false; + } + let first_data = reserved as u32 + fat_count as u32 * fat_size; + if first_data >= total { + crate::serial_println!("fat: bad geometry"); + return false; + } + let cluster_count = (total - first_data) / spc as u32; + + // Load the whole FAT into RAM (chunked through the 4 KiB DMA buffer). + let fat_bytes = fat_size as usize * SECTOR; + let mut fat = alloc::vec![0u8; fat_bytes]; + let mut lba = reserved as u64; + let mut done = 0usize; + while done < fat.len() { + let n = ((fat.len() - done) / SECTOR).min(8) as u32; + if ahci::read_sectors(lba, n).is_err() { + crate::serial_println!("fat: FAT read failed at lba {}", lba); + return false; + } + unsafe { + core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR); + } + lba += n as u64; + done += n as usize * SECTOR; + } + // Sanity: FAT[0] holds the media byte, FAT[1] should be an EOC. + let e0 = u32::from_le_bytes([fat[0], fat[1], fat[2], fat[3]]); + let e1 = u32::from_le_bytes([fat[4], fat[5], fat[6], fat[7]]); + if e0 & 0xFF != 0xF8 || e1 & EOC_MASK != EOC { + crate::serial_println!( + "fat: bad FAT markers ({:#x} {:#x})", + e0 & 0xFF, + e1 & EOC_MASK + ); + return false; + } + unsafe { + VOL = Some(Volume { + sectors_per_cluster: spc, + reserved, + fat_count, + fat_size, + root_cluster, + first_data, + cluster_count, + fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec), + fat, + }); + } + // Re-sync the FSInfo free-cluster count (may be stale from earlier + // sessions that predate FSInfo maintenance). + fsinfo_recount(); + let count = list_dir(root_cluster).map(|v| v.len()).unwrap_or(0); + crate::serial_println!( + "fat: FAT32 mounted — {} clusters ({} bytes each), root: {} entries", + cluster_count, + cluster_size(), + count + ); + true +} diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index df9308b..6950171 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs @@ -51,6 +51,19 @@ enum NodeData { Regular(Vec), Dir(Vec), Char(CharDevice), + Disk(DiskInfo), +} + +/// A node backed by the FAT32 volume (`/disk` mount). `parent` is the +/// directory cluster holding the entry (used to re-locate it on every +/// op, so it stays correct as the volume changes), `cluster` the entry's +/// own first cluster. +struct DiskInfo { + parent: u32, + name: String, + is_dir: bool, + size: u64, + cluster: u32, } struct Node { @@ -161,6 +174,23 @@ pub fn init() -> (usize, usize) { ), ); + // Mount the FAT32 volume at /disk when one is present. + if crate::fat::is_mounted() { + let root_cl = crate::fat::root_cluster(); + add( + "disk", + Some(root), + FileType::Dir, + NodeData::Disk(DiskInfo { + parent: root_cl, + name: String::new(), + is_dir: true, + size: 0, + cluster: root_cl, + }), + ); + } + let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); let files = nodes().len() - dirs; (dirs, files) @@ -180,22 +210,52 @@ fn lookup(dir: usize, name: &str) -> Option { None } -/// Resolve an absolute path (supports `.` and `..`). +/// Resolve an absolute path (supports `.` and `..`). Paths under +/// `/disk` walk the mounted FAT32 volume through ephemeral Disk nodes. pub fn resolve(path: &str) -> Option { if nodes().is_empty() { return None; } let mut cur = 0usize; // root + // (cluster, parent, name) of directories descended into under /disk. + let mut dstack: Vec<(u32, u32, String)> = Vec::new(); for seg in path.split('/') { match seg { "" | "." => {} ".." => { - if let Some(p) = nodes()[cur].parent { + if let Some((cl, p, n)) = dstack.pop() { + cur = materialize_disk(DiskInfo { + parent: p, + name: n, + is_dir: true, + size: 0, + cluster: cl, + }); + } else if let Some(p) = nodes()[cur].parent { cur = p; } } name => { - cur = lookup(cur, name)?; + let disk_dir: Option = match &nodes()[cur].data { + NodeData::Disk(info) => Some(info.cluster), + _ => None, + }; + if let Some(dir_cluster) = disk_dir { + let e = crate::fat::find(dir_cluster, name).ok()?; + let d = DiskInfo { + parent: dir_cluster, + name: e.name.clone(), + is_dir: e.is_dir, + size: e.size, + cluster: e.cluster, + }; + if e.is_dir { + dstack.push((e.cluster, dir_cluster, e.name)); + } + cur = materialize_disk(d); + } else { + cur = lookup(cur, name)?; + } } } } @@ -209,12 +269,34 @@ pub fn kind(h: usize) -> FileType { pub fn size(h: usize) -> u64 { match &nodes()[h].data { NodeData::Regular(v) => v.len() as u64, + NodeData::Disk(i) => i.size, _ => 0, } } -/// Entries of a directory as (name, kind, size) — tombstoned nodes skipped. +/// Entries of a directory as (name, kind, size) — tombstoned nodes +/// skipped; disk directories are listed fresh from the FAT volume. pub fn children(h: usize) -> Vec<(String, FileType, u64)> { + if let NodeData::Disk(i) = &nodes()[h].data { + let cl = i.cluster; + return crate::fat::list_dir(cl) + .map(|es| { + es.into_iter() + .map(|e| { + ( + e.name, + if e.is_dir { + FileType::Dir + } else { + FileType::Regular + }, + e.size, + ) + }) + .collect() + }) + .unwrap_or_default(); + } let kids = match &nodes()[h].data { NodeData::Dir(v) => v.clone(), _ => return Vec::new(), @@ -275,6 +357,7 @@ pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result { buf[..n].copy_from_slice(&bytes[p..p + n]); Ok(n) } + NodeData::Disk(i) => crate::fat::read_file(i.parent, &i.name, buf, pos), NodeData::Dir(_) => Err(()), } } @@ -311,6 +394,7 @@ pub fn write(h: usize, buf: &[u8], pos: u64) -> Result { } _ => Err(()), // read-only devices }, + NodeData::Disk(i) => crate::fat::write_file(i.parent, &i.name, buf, pos), NodeData::Dir(_) => Err(()), } } @@ -322,6 +406,7 @@ pub fn set_len(h: usize, len: u64) -> Result<(), ()> { v.resize(len as usize, 0); Ok(()) } + NodeData::Disk(i) => crate::fat::set_len(i.parent, &i.name, len), _ => Err(()), } } @@ -331,6 +416,30 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result { if name.is_empty() || name.contains('/') || kind == FileType::Char { return Err(()); } + // Disk directory: create through the FAT volume. + let disk_parent: Option = match &nodes()[parent].data { + NodeData::Disk(i) => { + if !i.is_dir { + return Err(()); + } + Some(i.cluster) + } + _ => None, + }; + if let Some(p) = disk_parent { + if crate::fat::find(p, name).is_ok() { + return Err(()); + } + crate::fat::create(p, name, kind == FileType::Dir)?; + let e = crate::fat::find(p, name)?; + return Ok(materialize_disk(DiskInfo { + parent: p, + name: e.name, + is_dir: e.is_dir, + size: e.size, + cluster: e.cluster, + })); + } if lookup(parent, name).is_some() { return Err(()); } @@ -344,6 +453,15 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result { /// Remove a regular file or an empty directory (tombstone). pub fn remove(h: usize) -> Result<(), ()> { + // Disk nodes: delete through the FAT volume (dirs must be empty). + if let NodeData::Disk(i) = &nodes()[h].data { + if i.name.is_empty() { + return Err(()); // the /disk mount itself + } + let p = i.parent; + let n = i.name.clone(); + return crate::fat::remove(p, &n); + } let parent = match nodes()[h].parent { Some(p) => p, None => return Err(()), @@ -414,6 +532,44 @@ pub fn remove_path(path: &str) -> Result<(), ()> { remove(h) } +// --- disk mount ------------------------------------------------------------ + +/// Round-robin pool of ephemeral disk nodes so a long-lived shell doesn't +/// grow the node table forever. +const DISK_SLOTS: usize = 24; +static mut DISK_POOL: *mut Vec = core::ptr::null_mut(); +static mut DISK_ROBIN: usize = 0; + +fn materialize_disk(info: DiskInfo) -> usize { + unsafe { + if DISK_POOL.is_null() { + DISK_POOL = Box::into_raw(Box::new(Vec::new())); + } + let pool = &mut *DISK_POOL; + let idx = DISK_ROBIN % DISK_SLOTS; + DISK_ROBIN = DISK_ROBIN.wrapping_add(1); + let kind = if info.is_dir { + FileType::Dir + } else { + FileType::Regular + }; + if idx < pool.len() { + let h = pool[idx]; + let n = &mut nodes()[h]; + n.name = info.name.clone(); + n.kind = kind; + n.parent = None; + n.data = NodeData::Disk(info); + n.removed = false; + return h; + } + let name = info.name.clone(); + let h = add(&name, None, kind, NodeData::Disk(info)); + pool.push(h); + h + } +} + // --- character devices ---------------------------------------------------- fn version_text() -> String { diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index fa14eae..77cc514 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs @@ -167,7 +167,11 @@ impl Framebuffer { if SHADOW.is_null() { // No shadow (allocation failed): fall back to shifting the // display directly. - ptr::copy(self.base.add(n * row_bytes), self.base, total - n * row_bytes); + ptr::copy( + self.base.add(n * row_bytes), + self.base, + total - n * row_bytes, + ); let last_row = self.base.add(total - n * row_bytes); for i in 0..(n * row_words) { ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index a8e279a..fc823ea 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs @@ -2,6 +2,8 @@ extern crate alloc; +pub mod ahci; +pub mod fat; pub mod font; pub mod fs; pub mod gop; diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs index 2d201f3..efdbc01 100644 --- a/rsnix/src/pci.rs +++ b/rsnix/src/pci.rs @@ -57,6 +57,39 @@ pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 { inl(CONFIG_DATA) } +/// Write a dword to a device's config space (`reg` must be dword-aligned). +pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { + let addr = 0x8000_0000 + | ((bus as u32) << 16) + | ((device as u32) << 11) + | ((func as u32) << 8) + | ((reg as u32) & 0xFC); + outl(CONFIG_ADDRESS, addr); + outl(CONFIG_DATA, val) +} + +/// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit +/// BARs. Returns None for I/O BARs or invalid values. +pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option { + let reg_off = 0x10 + bar_index * 4; + let lo = config_read(bus, device, func, reg_off); + if lo & 1 == 1 { + return None; // I/O BAR + } + let kind = (lo >> 1) & 0x3; + let hi = if kind == 2 { + config_read(bus, device, func, reg_off + 4) + } else { + 0 + }; + let base = ((hi as u64) << 32) | (lo as u64 & 0xFFFF_FFF0); + if base == 0 { + None + } else { + Some(base) + } +} + /// Enumerate every device/function present on all buses. pub fn probe() -> alloc::vec::Vec { let mut out = alloc::vec::Vec::new(); diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 88129f2..59378f3 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs @@ -432,9 +432,7 @@ fn scroll_up() { // appears promptly. const TICK_LIMIT: u16 = 4; let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); - if PENDING_SCROLLS >= SCROLL_BATCH - || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT - { + if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { sync_scroll(); } } diff --git a/scripts/qemu.sh b/scripts/qemu.sh index b75bed4..123d9d0 100755 --- a/scripts/qemu.sh +++ b/scripts/qemu.sh @@ -40,7 +40,10 @@ while [ $# -gt 0 ]; do ;; --image | -i) _img_file="$(printf -- '-drive file=%s,format=raw,if=none,id=hd' "$2")" - DRIVE_ARGS=("$_img_file" '-device' 'ide-hd,drive=hd') + # Explicit buses: data disk -> ide.0 (port 0), ESP -> ide.1. + DRIVE_ARGS=("$_img_file" '-device' 'ide-hd,drive=hd,bus=ide.0' \ + '-drive' 'file=fat:rw:esp,format=raw,if=none,id=esp' \ + '-device' 'ide-hd,drive=esp,bus=ide.1') shift 2 ;; --bios | -b) -- cgit v1.2.3