diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 19:53:53 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 19:53:53 +0200 |
| commit | dc23d2f22cafeb6b97ba460789a834a6c586314d (patch) | |
| tree | 987bec11922073923c4c42bda84c0f670884dbca /rsnix | |
| parent | vt: defer scrolls and batch framebuffer pushes (diff) | |
| download | rs-dos-dc23d2f22cafeb6b97ba460789a834a6c586314d.tar.gz rs-dos-dc23d2f22cafeb6b97ba460789a834a6c586314d.zip | |
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
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/ahci.rs | 355 | ||||
| -rw-r--r-- | rsnix/src/fat.rs | 934 | ||||
| -rw-r--r-- | rsnix/src/fs.rs | 164 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 6 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/pci.rs | 33 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 4 |
7 files changed, 1490 insertions, 8 deletions
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 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // AHCI — SATA host-controller driver (polling, DMA, no interrupts). | ||
| 3 | // | ||
| 4 | // Finds the chipset's AHCI controller (class 1 / subclass 6 / prog-if 1, | ||
| 5 | // e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts port 0 | ||
| 6 | // and exposes sector-level reads/writes for the FAT32 filesystem. | ||
| 7 | // | ||
| 8 | // Commands are issued through command-slot 0 with one 4 KiB DMA buffer; | ||
| 9 | // completion is polled via PxCI (no ISR, no MSI). Everything is | ||
| 10 | // identity-mapped, so "physical" DMA addresses are plain kernel pointers. | ||
| 11 | // The display is never involved, so this is a straightforward polling | ||
| 12 | // loop like the rest of the single-threaded kernel. | ||
| 13 | // --------------------------------------------------------------------------- | ||
| 14 | |||
| 15 | use crate::pci; | ||
| 16 | |||
| 17 | const SECTOR: usize = 512; | ||
| 18 | |||
| 19 | // ABAR (HBA memory space) offsets. | ||
| 20 | const GHC: usize = 0x04; | ||
| 21 | const PI: usize = 0x0C; | ||
| 22 | const PORT_BASE: usize = 0x100; | ||
| 23 | const PORT_STRIDE: usize = 0x80; | ||
| 24 | |||
| 25 | // Port register offsets (relative to the port's base). | ||
| 26 | const P_CLB: usize = 0x00; | ||
| 27 | const P_CLBU: usize = 0x04; | ||
| 28 | const P_FB: usize = 0x08; | ||
| 29 | const P_FBU: usize = 0x0C; | ||
| 30 | const P_IS: usize = 0x10; | ||
| 31 | const P_CMD: usize = 0x18; | ||
| 32 | const P_TFD: usize = 0x20; | ||
| 33 | const P_SSTS: usize = 0x28; | ||
| 34 | const P_SCTL: usize = 0x2C; | ||
| 35 | const P_CI: usize = 0x38; | ||
| 36 | |||
| 37 | const CMD_ST: u32 = 1 << 0; // start | ||
| 38 | const CMD_SUD: u32 = 1 << 1; // spin-up device | ||
| 39 | const CMD_FRE: u32 = 1 << 4; // FIS receive enable | ||
| 40 | const CMD_DMAE: u32 = 1 << 23; // DMA enable (QEMU/ICH9) — required or DMA commands silently transfer nothing | ||
| 41 | const CMD_FR: u32 = 1 << 14; // FIS receive running | ||
| 42 | const CMD_CR: u32 = 1 << 15; // command list running | ||
| 43 | |||
| 44 | const ATA_IDENTIFY: u8 = 0xEC; | ||
| 45 | const ATA_READ_DMA_EXT: u8 = 0x25; | ||
| 46 | const ATA_WRITE_DMA_EXT: u8 = 0x35; | ||
| 47 | |||
| 48 | /// One attached disk: register map + DMA structures. | ||
| 49 | struct Disk { | ||
| 50 | port: usize, // port register base = ABAR + 0x100 | ||
| 51 | clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned | ||
| 52 | fis: usize, // FIS receive area, 256 B aligned | ||
| 53 | ct: usize, // command table, 256 B, 128 B aligned | ||
| 54 | buf: usize, // 4 KiB DMA data buffer (page aligned) | ||
| 55 | sectors: u64, // total sectors (LBA48 from IDENTIFY) | ||
| 56 | } | ||
| 57 | |||
| 58 | static mut DISK: Option<Disk> = None; | ||
| 59 | |||
| 60 | fn reg(a: usize) -> u32 { | ||
| 61 | unsafe { core::ptr::read_volatile(a as *const u32) } | ||
| 62 | } | ||
| 63 | |||
| 64 | fn wreg(a: usize, v: u32) { | ||
| 65 | unsafe { core::ptr::write_volatile(a as *mut u32, v) } | ||
| 66 | } | ||
| 67 | |||
| 68 | /// Scan bus 0 for the AHCI controller (SATA, AHCI prog-if). | ||
| 69 | fn find_ahci() -> Option<(u8, u8)> { | ||
| 70 | for dev in 0..32u8 { | ||
| 71 | for func in 0..8u8 { | ||
| 72 | let id = pci::config_read(0, dev, func, 0); | ||
| 73 | if id == 0xFFFF_FFFF { | ||
| 74 | if func == 0 { | ||
| 75 | break; | ||
| 76 | } | ||
| 77 | continue; | ||
| 78 | } | ||
| 79 | let cc = pci::config_read(0, dev, func, 0x08); | ||
| 80 | let class = (cc >> 24) as u8; | ||
| 81 | let subclass = (cc >> 16) as u8; | ||
| 82 | let prog_if = (cc >> 8) as u8; | ||
| 83 | if class == 0x01 && subclass == 0x06 && prog_if == 0x01 { | ||
| 84 | return Some((dev, func)); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | None | ||
| 89 | } | ||
| 90 | |||
| 91 | /// Probe, map and start the first AHCI port. Returns true if a disk is | ||
| 92 | /// ready for sector I/O. Called once at boot before the filesystem init. | ||
| 93 | pub fn init() -> bool { | ||
| 94 | let Some((dev, func)) = find_ahci() else { | ||
| 95 | return false; | ||
| 96 | }; | ||
| 97 | // Enable memory space + bus mastering so MMIO and DMA work. | ||
| 98 | let cmd_reg = pci::config_read(0, dev, func, 0x04); | ||
| 99 | pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); | ||
| 100 | |||
| 101 | let Some(abar) = pci::bar64(0, dev, func, 5) else { | ||
| 102 | return false; | ||
| 103 | }; | ||
| 104 | crate::serial_println!( | ||
| 105 | "ahci: controller at {:02x}:{:02x}.{} abar={:#x}", | ||
| 106 | 0, | ||
| 107 | dev, | ||
| 108 | func, | ||
| 109 | abar | ||
| 110 | ); | ||
| 111 | |||
| 112 | // AHCI enable (GHC.AE) before touching port registers. | ||
| 113 | wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31)); | ||
| 114 | |||
| 115 | // Pick the first implemented port. | ||
| 116 | let pi = reg(abar as usize + PI); | ||
| 117 | let mut port_idx = 0u32; | ||
| 118 | while port_idx < 32 && (pi >> port_idx) & 1 == 0 { | ||
| 119 | port_idx += 1; | ||
| 120 | } | ||
| 121 | if port_idx >= 32 { | ||
| 122 | crate::serial_println!("ahci: no ports implemented (pi={:#x})", pi); | ||
| 123 | return false; | ||
| 124 | } | ||
| 125 | let port = abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE; | ||
| 126 | |||
| 127 | // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd | ||
| 128 | // table, page 1 = data buffer. | ||
| 129 | let Some(mem) = crate::memory::alloc_pages(2) else { | ||
| 130 | crate::serial_println!("ahci: cannot allocate DMA pages"); | ||
| 131 | return false; | ||
| 132 | }; | ||
| 133 | let clb = mem; | ||
| 134 | let fis = mem + 1024; | ||
| 135 | let ct = mem + 1280; | ||
| 136 | let buf = mem + 4096; | ||
| 137 | |||
| 138 | // Stop the port if firmware left it running. | ||
| 139 | let mut cmd = reg(port + P_CMD); | ||
| 140 | if cmd & CMD_ST != 0 { | ||
| 141 | wreg(port + P_CMD, cmd & !CMD_ST); | ||
| 142 | let mut i = 0; | ||
| 143 | while reg(port + P_CMD) & CMD_CR != 0 && i < 1_000_000 { | ||
| 144 | i += 1; | ||
| 145 | } | ||
| 146 | cmd = reg(port + P_CMD); | ||
| 147 | } | ||
| 148 | if cmd & CMD_FRE != 0 { | ||
| 149 | wreg(port + P_CMD, cmd & !CMD_FRE); | ||
| 150 | let mut i = 0; | ||
| 151 | while reg(port + P_CMD) & CMD_FR != 0 && i < 1_000_000 { | ||
| 152 | i += 1; | ||
| 153 | } | ||
| 154 | } | ||
| 155 | unsafe { | ||
| 156 | core::ptr::write_bytes(clb as *mut u8, 0, 1024); | ||
| 157 | core::ptr::write_bytes(fis as *mut u8, 0, 256); | ||
| 158 | core::ptr::write_bytes(ct as *mut u8, 0, 256); | ||
| 159 | } | ||
| 160 | |||
| 161 | wreg(port + P_CLB, clb as u32); | ||
| 162 | wreg(port + P_CLBU, 0); | ||
| 163 | wreg(port + P_FB, fis as u32); | ||
| 164 | wreg(port + P_FBU, 0); | ||
| 165 | |||
| 166 | // Allow device detection (clear PxSCTL.DET/IPM), then wait for the | ||
| 167 | // link to come up (PxSSTS.DET == 3, communication established). | ||
| 168 | wreg(port + P_SCTL, 0); | ||
| 169 | let mut link_ok = false; | ||
| 170 | for _ in 0..10_000_000 { | ||
| 171 | if reg(port + P_SSTS) & 0xF == 3 { | ||
| 172 | link_ok = true; | ||
| 173 | break; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | if !link_ok { | ||
| 177 | crate::serial_println!( | ||
| 178 | "ahci: no device on port {} (ssts={:#x})", | ||
| 179 | port_idx, | ||
| 180 | reg(port + P_SSTS) | ||
| 181 | ); | ||
| 182 | return false; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Start the command engine (DMAE required for DMA commands). | ||
| 186 | wreg(port + P_CMD, CMD_FRE | CMD_SUD | CMD_DMAE); | ||
| 187 | wreg(port + P_CMD, CMD_ST | CMD_FRE | CMD_SUD | CMD_DMAE); | ||
| 188 | |||
| 189 | let mut d = Disk { | ||
| 190 | port, | ||
| 191 | clb, | ||
| 192 | fis, | ||
| 193 | ct, | ||
| 194 | buf, | ||
| 195 | sectors: 0, | ||
| 196 | }; | ||
| 197 | // IDENTIFY DEVICE as a sanity check + to learn the capacity. | ||
| 198 | if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() { | ||
| 199 | crate::serial_println!("ahci: IDENTIFY failed"); | ||
| 200 | return false; | ||
| 201 | } | ||
| 202 | let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) }; | ||
| 203 | d.sectors = (id[100] as u64) | ||
| 204 | | ((id[101] as u64) << 16) | ||
| 205 | | ((id[102] as u64) << 32) | ||
| 206 | | ((id[103] as u64) << 48); | ||
| 207 | if d.sectors == 0 { | ||
| 208 | d.sectors = id[60] as u64; // LBA28 fallback | ||
| 209 | } | ||
| 210 | let mut model = [0u8; 40]; | ||
| 211 | for i in 0..20 { | ||
| 212 | model[i * 2] = (id[27 + i] >> 8) as u8; | ||
| 213 | model[i * 2 + 1] = id[27 + i] as u8; | ||
| 214 | } | ||
| 215 | let model = core::str::from_utf8(&model).unwrap_or("?").trim(); | ||
| 216 | crate::serial_println!( | ||
| 217 | "ahci: port {} up — {} sectors (~{} MB) \"{}\"", | ||
| 218 | port_idx, | ||
| 219 | d.sectors, | ||
| 220 | d.sectors / 2048, | ||
| 221 | model | ||
| 222 | ); | ||
| 223 | unsafe { | ||
| 224 | DISK = Some(d); | ||
| 225 | } | ||
| 226 | true | ||
| 227 | } | ||
| 228 | |||
| 229 | /// Issue one command through slot 0 and poll for completion. | ||
| 230 | /// `count` is sectors (1-8); data flows through the disk's DMA buffer. | ||
| 231 | fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> { | ||
| 232 | if count == 0 || count > 8 { | ||
| 233 | return Err(()); | ||
| 234 | } | ||
| 235 | // Wait for slot 0 to be free. | ||
| 236 | let mut i = 0; | ||
| 237 | while reg(d.port + P_CI) & 1 != 0 && i < 10_000_000 { | ||
| 238 | i += 1; | ||
| 239 | } | ||
| 240 | if reg(d.port + P_CI) & 1 != 0 { | ||
| 241 | return Err(()); | ||
| 242 | } | ||
| 243 | |||
| 244 | unsafe { | ||
| 245 | core::ptr::write_bytes(d.ct as *mut u8, 0, 256); | ||
| 246 | } | ||
| 247 | |||
| 248 | // Host-to-device FIS (20 bytes) at the command table start. | ||
| 249 | let fis = d.ct as *mut u8; | ||
| 250 | unsafe { | ||
| 251 | *fis = 0x27; // FIS type H2D | ||
| 252 | *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4 | ||
| 253 | *(fis.add(2)) = cmd; | ||
| 254 | // FIS field layout (SATA spec; verified against QEMU/Linux): | ||
| 255 | // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48], | ||
| 256 | // 11 features high, 12-13 sector count. | ||
| 257 | *(fis.add(4)) = lba as u8; | ||
| 258 | *(fis.add(5)) = (lba >> 8) as u8; | ||
| 259 | *(fis.add(6)) = (lba >> 16) as u8; | ||
| 260 | *(fis.add(7)) = 0x40 | (((lba >> 24) & 0x0F) as u8); | ||
| 261 | *(fis.add(8)) = (lba >> 24) as u8; | ||
| 262 | *(fis.add(9)) = (lba >> 32) as u8; | ||
| 263 | *(fis.add(10)) = (lba >> 40) as u8; | ||
| 264 | *(fis.add(11)) = 0; // features high | ||
| 265 | *(fis.add(12)) = count as u8; | ||
| 266 | *(fis.add(13)) = (count >> 8) as u8; | ||
| 267 | *(fis.add(14)) = 0; // ICC | ||
| 268 | *(fis.add(15)) = 0; // control | ||
| 269 | } | ||
| 270 | |||
| 271 | // PRD entry at the command table +0x80. | ||
| 272 | let prd = (d.ct + 0x80) as *mut u32; | ||
| 273 | unsafe { | ||
| 274 | *prd = d.buf as u32; | ||
| 275 | *prd.add(1) = 0; | ||
| 276 | *prd.add(2) = 0; | ||
| 277 | *prd.add(3) = (count * SECTOR as u32) - 1; // DBC, no I bit | ||
| 278 | } | ||
| 279 | |||
| 280 | // Command header 0. Layout per Linux libahci / QEMU AHCICmdHdr: | ||
| 281 | // DW0 bits 4:0 = CFL (FIS length in dwords), bit 6 = W, | ||
| 282 | // bits 31:16 = PRDTL; DW1 status; DW2/3 = command table address. | ||
| 283 | let hdr = d.clb as *mut u32; | ||
| 284 | unsafe { | ||
| 285 | *hdr = 5 | (1 << 16) | if write { 1 << 6 } else { 0 }; | ||
| 286 | *hdr.add(1) = 0; | ||
| 287 | *hdr.add(2) = d.ct as u32; | ||
| 288 | *hdr.add(3) = 0; | ||
| 289 | } | ||
| 290 | |||
| 291 | // Issue and poll. | ||
| 292 | wreg(d.port + P_CI, 1); | ||
| 293 | let mut i = 0; | ||
| 294 | while reg(d.port + P_CI) & 1 != 0 && i < 100_000_000 { | ||
| 295 | i += 1; | ||
| 296 | } | ||
| 297 | if reg(d.port + P_CI) & 1 != 0 { | ||
| 298 | crate::serial_println!( | ||
| 299 | "ahci: command {:#x} lba={} cnt={} timed out (tfd={:#x} ci={:#x} cmd={:#x} prdbc={:#x})", | ||
| 300 | cmd, | ||
| 301 | lba, | ||
| 302 | count, | ||
| 303 | reg(d.port + P_TFD), | ||
| 304 | reg(d.port + P_CI), | ||
| 305 | reg(d.port + P_CMD), | ||
| 306 | unsafe { *(d.clb as *const u32).add(1) } | ||
| 307 | ); | ||
| 308 | return Err(()); | ||
| 309 | } | ||
| 310 | // Error? Clear any port error bits either way. | ||
| 311 | if reg(d.port + P_TFD) & 1 != 0 { | ||
| 312 | wreg(d.port + 0x30, 0xFFFF_FFFF); // PxSERR: clear | ||
| 313 | crate::serial_println!( | ||
| 314 | "ahci: command {:#x} error (tfd={:#x})", | ||
| 315 | cmd, | ||
| 316 | reg(d.port + P_TFD) | ||
| 317 | ); | ||
| 318 | return Err(()); | ||
| 319 | } | ||
| 320 | let is = reg(d.port + P_IS); | ||
| 321 | if is != 0 { | ||
| 322 | wreg(d.port + P_IS, is); // write-1-to-clear | ||
| 323 | } | ||
| 324 | Ok(()) | ||
| 325 | } | ||
| 326 | |||
| 327 | /// DMA `count` sectors starting at `lba` into the internal buffer. | ||
| 328 | pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { | ||
| 329 | let d = unsafe { DISK.as_ref() }.ok_or(())?; | ||
| 330 | if lba + count as u64 > d.sectors { | ||
| 331 | return Err(()); | ||
| 332 | } | ||
| 333 | issue(d, ATA_READ_DMA_EXT, lba, count, false) | ||
| 334 | } | ||
| 335 | |||
| 336 | /// DMA `count` sectors starting at `lba` out of the internal buffer. | ||
| 337 | pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { | ||
| 338 | let d = unsafe { DISK.as_ref() }.ok_or(())?; | ||
| 339 | if lba + count as u64 > d.sectors { | ||
| 340 | return Err(()); | ||
| 341 | } | ||
| 342 | issue(d, ATA_WRITE_DMA_EXT, lba, count, true) | ||
| 343 | } | ||
| 344 | |||
| 345 | /// Pointer to the internal 4 KiB DMA buffer. | ||
| 346 | pub fn buf() -> *mut u8 { | ||
| 347 | unsafe { DISK.as_ref() } | ||
| 348 | .map(|d| d.buf as *mut u8) | ||
| 349 | .unwrap_or(core::ptr::null_mut()) | ||
| 350 | } | ||
| 351 | |||
| 352 | /// Total sector count of the attached disk. | ||
| 353 | pub fn sector_count() -> u64 { | ||
| 354 | unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0) | ||
| 355 | } | ||
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 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // FAT32 — read/write a FAT32 volume over the AHCI sector interface. | ||
| 3 | // | ||
| 4 | // Mounts the volume's boot sector (BPB), caches the whole FAT in RAM, | ||
| 5 | // follows cluster chains, and exposes file/directory operations used by | ||
| 6 | // the VFS's Disk nodes: list, find, read, write, truncate, create (files | ||
| 7 | // and dirs), remove. Long filenames are read back and written for names | ||
| 8 | // that don't fit 8.3; short names are generated the standard way | ||
| 9 | // (uppercased, 8+3, "~N" truncation). All timestamps are zero — this is | ||
| 10 | // a toy kernel, not a backup tool. | ||
| 11 | // | ||
| 12 | // Single-threaded like the rest of the kernel; no caching beyond the FAT | ||
| 13 | // itself, so every op re-reads directory sectors (cheap: the disk is | ||
| 14 | // polled DMA at µs scale in QEMU). | ||
| 15 | // --------------------------------------------------------------------------- | ||
| 16 | |||
| 17 | extern crate alloc; | ||
| 18 | |||
| 19 | use crate::ahci; | ||
| 20 | use alloc::string::String; | ||
| 21 | use alloc::vec; | ||
| 22 | use alloc::vec::Vec; | ||
| 23 | |||
| 24 | const SECTOR: usize = 512; | ||
| 25 | const EOC_MASK: u32 = 0x0FFF_FFFF; | ||
| 26 | const EOC: u32 = 0x0FFF_FFFF; | ||
| 27 | |||
| 28 | /// One directory-listed entry (or a resolved file). | ||
| 29 | pub struct Entry { | ||
| 30 | pub name: String, | ||
| 31 | pub is_dir: bool, | ||
| 32 | pub size: u64, | ||
| 33 | pub cluster: u32, | ||
| 34 | } | ||
| 35 | |||
| 36 | struct Volume { | ||
| 37 | sectors_per_cluster: u8, | ||
| 38 | reserved: u16, | ||
| 39 | fat_count: u8, | ||
| 40 | fat_size: u32, // sectors per FAT | ||
| 41 | root_cluster: u32, | ||
| 42 | first_data: u32, // LBA of cluster 2 | ||
| 43 | cluster_count: u32, | ||
| 44 | fsinfo: Option<u32>, // sector number of the FSInfo structure | ||
| 45 | fat: Vec<u8>, // FAT 1 cached in RAM | ||
| 46 | } | ||
| 47 | |||
| 48 | static mut VOL: Option<Volume> = None; | ||
| 49 | |||
| 50 | pub fn is_mounted() -> bool { | ||
| 51 | unsafe { VOL.is_some() } | ||
| 52 | } | ||
| 53 | |||
| 54 | pub fn root_cluster() -> u32 { | ||
| 55 | unsafe { VOL.as_ref() }.map(|v| v.root_cluster).unwrap_or(0) | ||
| 56 | } | ||
| 57 | |||
| 58 | // --- sector I/O ----------------------------------------------------------- | ||
| 59 | |||
| 60 | fn read_sector(lba: u64, out: &mut [u8; SECTOR]) -> Result<(), ()> { | ||
| 61 | ahci::read_sectors(lba, 1)?; | ||
| 62 | unsafe { core::ptr::copy(ahci::buf(), out.as_mut_ptr(), SECTOR) }; | ||
| 63 | Ok(()) | ||
| 64 | } | ||
| 65 | |||
| 66 | fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> { | ||
| 67 | unsafe { core::ptr::copy(data.as_ptr(), ahci::buf(), SECTOR) }; | ||
| 68 | ahci::write_sectors(lba, 1) | ||
| 69 | } | ||
| 70 | |||
| 71 | // --- cluster math ---------------------------------------------------------- | ||
| 72 | |||
| 73 | fn v() -> &'static mut Volume { | ||
| 74 | unsafe { VOL.as_mut() }.unwrap() | ||
| 75 | } | ||
| 76 | |||
| 77 | fn spc() -> usize { | ||
| 78 | v().sectors_per_cluster as usize | ||
| 79 | } | ||
| 80 | |||
| 81 | fn cluster_size() -> usize { | ||
| 82 | spc() * SECTOR | ||
| 83 | } | ||
| 84 | |||
| 85 | fn cluster_lba(c: u32) -> u64 { | ||
| 86 | v().first_data as u64 + ((c as u64 - 2) * spc() as u64) | ||
| 87 | } | ||
| 88 | |||
| 89 | fn fat_off(c: u32) -> usize { | ||
| 90 | c as usize * 4 | ||
| 91 | } | ||
| 92 | |||
| 93 | fn read_fat_entry(c: u32) -> u32 { | ||
| 94 | let off = fat_off(c); | ||
| 95 | u32::from_le_bytes([ | ||
| 96 | v().fat[off], | ||
| 97 | v().fat[off + 1], | ||
| 98 | v().fat[off + 2], | ||
| 99 | v().fat[off + 3], | ||
| 100 | ]) & EOC_MASK | ||
| 101 | } | ||
| 102 | |||
| 103 | /// Update a FAT entry in the cache and write the affected sector back to | ||
| 104 | /// both FAT copies. | ||
| 105 | fn write_fat_entry(c: u32, val: u32) -> Result<(), ()> { | ||
| 106 | let off = fat_off(c); | ||
| 107 | v().fat[off..off + 4].copy_from_slice(&(val & EOC_MASK).to_le_bytes()); | ||
| 108 | let sector_idx = off / SECTOR; | ||
| 109 | let mut buf = [0u8; SECTOR]; | ||
| 110 | buf.copy_from_slice(&v().fat[sector_idx * SECTOR..(sector_idx + 1) * SECTOR]); | ||
| 111 | let lba1 = v().reserved as u64 + sector_idx as u64; | ||
| 112 | let lba2 = lba1 + v().fat_size as u64; | ||
| 113 | write_sector(lba1, &buf)?; | ||
| 114 | write_sector(lba2, &buf) | ||
| 115 | } | ||
| 116 | |||
| 117 | fn is_eoc(c: u32) -> bool { | ||
| 118 | c >= 0x0FFF_FFF8 | ||
| 119 | } | ||
| 120 | |||
| 121 | fn next_cluster(c: u32) -> Result<u32, ()> { | ||
| 122 | let n = read_fat_entry(c); | ||
| 123 | if is_eoc(n) { | ||
| 124 | Err(()) | ||
| 125 | } else { | ||
| 126 | Ok(n) | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | /// Allocate a fresh cluster: mark it EOC in the FAT and zero its data. | ||
| 131 | fn alloc_cluster() -> Result<u32, ()> { | ||
| 132 | let total = v().cluster_count + 2; | ||
| 133 | for c in 2..total { | ||
| 134 | if read_fat_entry(c) == 0 { | ||
| 135 | write_fat_entry(c, EOC)?; | ||
| 136 | let zero = vec![0u8; cluster_size()]; | ||
| 137 | write_cluster(c, &zero)?; | ||
| 138 | fsinfo_adjust(-1); | ||
| 139 | return Ok(c); | ||
| 140 | } | ||
| 141 | } | ||
| 142 | crate::serial_println!("fat: disk full"); | ||
| 143 | Err(()) | ||
| 144 | } | ||
| 145 | |||
| 146 | fn free_chain(mut c: u32) { | ||
| 147 | let mut freed = 0u64; | ||
| 148 | let mut guard = 0; | ||
| 149 | while !is_eoc(read_fat_entry(c)) && guard < 1_000_000 { | ||
| 150 | let nxt = read_fat_entry(c); | ||
| 151 | let _ = write_fat_entry(c, 0); | ||
| 152 | c = nxt; | ||
| 153 | freed += 1; | ||
| 154 | guard += 1; | ||
| 155 | } | ||
| 156 | let _ = write_fat_entry(c, 0); | ||
| 157 | freed += 1; | ||
| 158 | if freed > 0 { | ||
| 159 | fsinfo_adjust(freed as i64); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | /// Keep FSInfo.FSFreeCount in sync with cluster allocations/frees so | ||
| 164 | /// fsck and other tools see an accurate free count. Falls back to a full | ||
| 165 | /// recount when the stored value is the "unknown" marker. | ||
| 166 | fn fsinfo_adjust(delta: i64) { | ||
| 167 | let Some(s) = v().fsinfo else { | ||
| 168 | return; | ||
| 169 | }; | ||
| 170 | let mut b = [0u8; SECTOR]; | ||
| 171 | if read_sector(s as u64, &mut b).is_err() { | ||
| 172 | return; | ||
| 173 | } | ||
| 174 | if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" { | ||
| 175 | return; // not a valid FSInfo structure | ||
| 176 | } | ||
| 177 | let mut n = u32::from_le_bytes([b[488], b[489], b[490], b[491]]); | ||
| 178 | if n == 0xFFFF_FFFF { | ||
| 179 | n = free_clusters(); | ||
| 180 | } | ||
| 181 | let n = (n as i64 + delta).max(0) as u32; | ||
| 182 | b[488..492].copy_from_slice(&n.to_le_bytes()); | ||
| 183 | let _ = write_sector(s as u64, &b); | ||
| 184 | } | ||
| 185 | |||
| 186 | /// Rewrite FSInfo.FSFreeCount with the true free-cluster count. | ||
| 187 | fn fsinfo_recount() { | ||
| 188 | let Some(s) = v().fsinfo else { | ||
| 189 | return; | ||
| 190 | }; | ||
| 191 | let mut b = [0u8; SECTOR]; | ||
| 192 | if read_sector(s as u64, &mut b).is_err() { | ||
| 193 | return; | ||
| 194 | } | ||
| 195 | if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" { | ||
| 196 | return; | ||
| 197 | } | ||
| 198 | let n = free_clusters(); | ||
| 199 | b[488..492].copy_from_slice(&n.to_le_bytes()); | ||
| 200 | let _ = write_sector(s as u64, &b); | ||
| 201 | } | ||
| 202 | |||
| 203 | /// Count free clusters by scanning the cached FAT. | ||
| 204 | fn free_clusters() -> u32 { | ||
| 205 | let mut n = 0u32; | ||
| 206 | for c in 2..v().cluster_count + 2 { | ||
| 207 | if read_fat_entry(c) == 0 { | ||
| 208 | n += 1; | ||
| 209 | } | ||
| 210 | } | ||
| 211 | n | ||
| 212 | } | ||
| 213 | |||
| 214 | fn read_cluster(c: u32, buf: &mut [u8]) -> Result<(), ()> { | ||
| 215 | if spc() <= 8 { | ||
| 216 | ahci::read_sectors(cluster_lba(c), spc() as u32)?; | ||
| 217 | unsafe { core::ptr::copy(ahci::buf(), buf.as_mut_ptr(), cluster_size()) }; | ||
| 218 | } else { | ||
| 219 | for s in 0..spc() { | ||
| 220 | ahci::read_sectors(cluster_lba(c) + s as u64, 1)?; | ||
| 221 | unsafe { | ||
| 222 | core::ptr::copy(ahci::buf(), buf.as_mut_ptr().add(s * SECTOR), SECTOR); | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 226 | Ok(()) | ||
| 227 | } | ||
| 228 | |||
| 229 | fn write_cluster(c: u32, buf: &[u8]) -> Result<(), ()> { | ||
| 230 | if spc() <= 8 { | ||
| 231 | unsafe { core::ptr::copy(buf.as_ptr(), ahci::buf(), cluster_size()) }; | ||
| 232 | ahci::write_sectors(cluster_lba(c), spc() as u32) | ||
| 233 | } else { | ||
| 234 | for s in 0..spc() { | ||
| 235 | unsafe { | ||
| 236 | core::ptr::copy(buf.as_ptr().add(s * SECTOR), ahci::buf(), SECTOR); | ||
| 237 | } | ||
| 238 | ahci::write_sectors(cluster_lba(c) + s as u64, 1)?; | ||
| 239 | } | ||
| 240 | Ok(()) | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | // --- directory scanning ---------------------------------------------------- | ||
| 245 | |||
| 246 | /// Checksum of an SFN's 11 bytes; used to bind LFN entries to their SFN. | ||
| 247 | fn lfn_checksum(sfn: &[u8; 11]) -> u8 { | ||
| 248 | let mut s: u8 = 0; | ||
| 249 | for &b in sfn { | ||
| 250 | s = s.rotate_right(1).wrapping_add(b); | ||
| 251 | } | ||
| 252 | s | ||
| 253 | } | ||
| 254 | |||
| 255 | fn sfn_decode(e: &[u8]) -> String { | ||
| 256 | let nts = e[12]; | ||
| 257 | let mut s = String::new(); | ||
| 258 | for &b in &e[0..8] { | ||
| 259 | if b == b' ' || b == 0 { | ||
| 260 | break; | ||
| 261 | } | ||
| 262 | let c = if nts & 0x08 != 0 { | ||
| 263 | b.to_ascii_lowercase() | ||
| 264 | } else { | ||
| 265 | b | ||
| 266 | }; | ||
| 267 | s.push(c as char); | ||
| 268 | } | ||
| 269 | let mut ext = String::new(); | ||
| 270 | for &b in &e[8..11] { | ||
| 271 | if b == b' ' || b == 0 { | ||
| 272 | break; | ||
| 273 | } | ||
| 274 | let c = if nts & 0x10 != 0 { | ||
| 275 | b.to_ascii_lowercase() | ||
| 276 | } else { | ||
| 277 | b | ||
| 278 | }; | ||
| 279 | ext.push(c as char); | ||
| 280 | } | ||
| 281 | if !ext.is_empty() { | ||
| 282 | s.push('.'); | ||
| 283 | s.push_str(&ext); | ||
| 284 | } | ||
| 285 | if s.is_empty() { | ||
| 286 | s.push('?'); | ||
| 287 | } | ||
| 288 | s | ||
| 289 | } | ||
| 290 | |||
| 291 | fn utf16_to_string(v: &[u16]) -> String { | ||
| 292 | let mut s = String::new(); | ||
| 293 | for &c in v { | ||
| 294 | if c == 0 { | ||
| 295 | break; | ||
| 296 | } | ||
| 297 | if let Some(ch) = char::from_u32(c as u32) { | ||
| 298 | s.push(ch); | ||
| 299 | } | ||
| 300 | } | ||
| 301 | s | ||
| 302 | } | ||
| 303 | |||
| 304 | struct DirItem { | ||
| 305 | name: String, | ||
| 306 | attr: u8, | ||
| 307 | cluster: u32, | ||
| 308 | size: u32, | ||
| 309 | sector: u64, // LBA of the entry's sector (for in-place updates) | ||
| 310 | offset: usize, | ||
| 311 | } | ||
| 312 | |||
| 313 | /// Scan a directory's cluster chain, reassembling long filenames. | ||
| 314 | fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> { | ||
| 315 | let cs = cluster_size(); | ||
| 316 | let mut buf = vec![0u8; cs]; | ||
| 317 | let mut items = Vec::new(); | ||
| 318 | let mut lfn: Vec<u16> = Vec::new(); | ||
| 319 | let mut lfn_sum = 0u8; | ||
| 320 | let mut cur: Option<u32> = Some(dir_cluster); | ||
| 321 | let mut guard = 0; | ||
| 322 | while let Some(c) = cur { | ||
| 323 | guard += 1; | ||
| 324 | if guard > 1_000_000 { | ||
| 325 | return Err(()); // loop protection on a corrupt chain | ||
| 326 | } | ||
| 327 | read_cluster(c, &mut buf)?; | ||
| 328 | let mut done = false; | ||
| 329 | for (i, e) in buf.chunks_exact(32).enumerate() { | ||
| 330 | match e[0] { | ||
| 331 | 0x00 => { | ||
| 332 | done = true; | ||
| 333 | break; | ||
| 334 | } | ||
| 335 | 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN | ||
| 336 | _ => { | ||
| 337 | let attr = e[11]; | ||
| 338 | if attr == 0x0F { | ||
| 339 | // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, 28-31). | ||
| 340 | for k in 0..13 { | ||
| 341 | let pos = if k < 5 { | ||
| 342 | 1 + k * 2 | ||
| 343 | } else if k < 11 { | ||
| 344 | 14 + (k - 5) * 2 | ||
| 345 | } else { | ||
| 346 | 28 + (k - 11) * 2 | ||
| 347 | }; | ||
| 348 | let ch = u16::from_le_bytes([e[pos], e[pos + 1]]); | ||
| 349 | if ch == 0x0000 { | ||
| 350 | break; | ||
| 351 | } | ||
| 352 | if ch != 0xFFFF { | ||
| 353 | lfn.push(ch); | ||
| 354 | } | ||
| 355 | } | ||
| 356 | lfn_sum = e[13]; | ||
| 357 | } else { | ||
| 358 | // Short-name entry: bind any collected LFN. | ||
| 359 | let mut sfn = [0u8; 11]; | ||
| 360 | sfn.copy_from_slice(&e[0..11]); | ||
| 361 | let name = if !lfn.is_empty() && lfn_checksum(&sfn) == lfn_sum { | ||
| 362 | utf16_to_string(&lfn) | ||
| 363 | } else { | ||
| 364 | sfn_decode(e) | ||
| 365 | }; | ||
| 366 | let cluster = u16::from_le_bytes([e[26], e[27]]) as u32 | ||
| 367 | | ((u16::from_le_bytes([e[20], e[21]]) as u32) << 16); | ||
| 368 | items.push(DirItem { | ||
| 369 | name, | ||
| 370 | attr, | ||
| 371 | cluster, | ||
| 372 | size: u32::from_le_bytes([e[28], e[29], e[30], e[31]]), | ||
| 373 | sector: cluster_lba(c) + (i * 32 / SECTOR) as u64, | ||
| 374 | offset: (i * 32) % SECTOR, | ||
| 375 | }); | ||
| 376 | lfn.clear(); | ||
| 377 | } | ||
| 378 | } | ||
| 379 | } | ||
| 380 | } | ||
| 381 | if done { | ||
| 382 | break; | ||
| 383 | } | ||
| 384 | cur = next_cluster(c).ok(); | ||
| 385 | } | ||
| 386 | Ok(items) | ||
| 387 | } | ||
| 388 | |||
| 389 | /// Public listing: no `.` / `..`. | ||
| 390 | pub fn list_dir(dir_cluster: u32) -> Result<Vec<Entry>, ()> { | ||
| 391 | Ok(scan_dir(dir_cluster)? | ||
| 392 | .into_iter() | ||
| 393 | .filter(|i| i.name != "." && i.name != "..") | ||
| 394 | .map(|i| Entry { | ||
| 395 | name: i.name, | ||
| 396 | is_dir: i.attr & 0x10 != 0, | ||
| 397 | size: i.size as u64, | ||
| 398 | cluster: i.cluster, | ||
| 399 | }) | ||
| 400 | .collect()) | ||
| 401 | } | ||
| 402 | |||
| 403 | /// Find an entry by name (case-insensitive), returning the scan record | ||
| 404 | /// (which knows the entry's sector for in-place updates). | ||
| 405 | fn find_item(parent_dir_cluster: u32, name: &str) -> Result<DirItem, ()> { | ||
| 406 | for i in scan_dir(parent_dir_cluster)? { | ||
| 407 | if i.name.eq_ignore_ascii_case(name) { | ||
| 408 | return Ok(i); | ||
| 409 | } | ||
| 410 | } | ||
| 411 | Err(()) | ||
| 412 | } | ||
| 413 | |||
| 414 | /// Find an entry by name (case-insensitive). | ||
| 415 | pub fn find(parent_dir_cluster: u32, name: &str) -> Result<Entry, ()> { | ||
| 416 | find_item(parent_dir_cluster, name).map(|i| Entry { | ||
| 417 | name: i.name, | ||
| 418 | is_dir: i.attr & 0x10 != 0, | ||
| 419 | size: i.size as u64, | ||
| 420 | cluster: i.cluster, | ||
| 421 | }) | ||
| 422 | } | ||
| 423 | |||
| 424 | // --- file data -------------------------------------------------------------- | ||
| 425 | |||
| 426 | pub fn read_file( | ||
| 427 | parent_dir_cluster: u32, | ||
| 428 | name: &str, | ||
| 429 | buf: &mut [u8], | ||
| 430 | pos: u64, | ||
| 431 | ) -> Result<usize, ()> { | ||
| 432 | if buf.is_empty() { | ||
| 433 | return Ok(0); | ||
| 434 | } | ||
| 435 | let it = find(parent_dir_cluster, name)?; | ||
| 436 | if it.is_dir || it.cluster == 0 { | ||
| 437 | return Err(()); | ||
| 438 | } | ||
| 439 | if pos >= it.size { | ||
| 440 | return Ok(0); | ||
| 441 | } | ||
| 442 | let cs = cluster_size() as u64; | ||
| 443 | let mut cluster = it.cluster; | ||
| 444 | let mut skip = pos; | ||
| 445 | while skip >= cs { | ||
| 446 | cluster = next_cluster(cluster)?; | ||
| 447 | skip -= cs; | ||
| 448 | } | ||
| 449 | let mut cbuf = vec![0u8; cluster_size()]; | ||
| 450 | let mut remaining = buf.len() as u64; | ||
| 451 | if remaining > it.size - pos { | ||
| 452 | remaining = it.size - pos; | ||
| 453 | } | ||
| 454 | let mut out = 0usize; | ||
| 455 | while remaining > 0 { | ||
| 456 | read_cluster(cluster, &mut cbuf)?; | ||
| 457 | let from = skip as usize; | ||
| 458 | let n = ((cs as usize) - from).min(remaining as usize); | ||
| 459 | buf[out..out + n].copy_from_slice(&cbuf[from..from + n]); | ||
| 460 | out += n; | ||
| 461 | remaining -= n as u64; | ||
| 462 | if remaining > 0 { | ||
| 463 | cluster = next_cluster(cluster)?; | ||
| 464 | } | ||
| 465 | skip = 0; | ||
| 466 | } | ||
| 467 | Ok(out) | ||
| 468 | } | ||
| 469 | |||
| 470 | pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result<usize, ()> { | ||
| 471 | if buf.is_empty() { | ||
| 472 | return Ok(0); | ||
| 473 | } | ||
| 474 | let it = find_item(parent_dir_cluster, name)?; | ||
| 475 | if it.attr & 0x10 != 0 { | ||
| 476 | return Err(()); | ||
| 477 | } | ||
| 478 | let cs = cluster_size() as u64; | ||
| 479 | let end = pos + buf.len() as u64; | ||
| 480 | |||
| 481 | // Ensure a first cluster exists. | ||
| 482 | let mut cluster = it.cluster; | ||
| 483 | if cluster == 0 { | ||
| 484 | cluster = alloc_cluster()?; | ||
| 485 | update_entry(&it, Some(cluster), None)?; | ||
| 486 | } | ||
| 487 | // Walk to the cluster containing `pos`, extending the chain as needed. | ||
| 488 | let mut skip = pos; | ||
| 489 | let mut cur = cluster; | ||
| 490 | while skip >= cs { | ||
| 491 | match next_cluster(cur) { | ||
| 492 | Ok(n) => cur = n, | ||
| 493 | Err(_) => { | ||
| 494 | let n = alloc_cluster()?; | ||
| 495 | write_fat_entry(cur, n)?; | ||
| 496 | cur = n; | ||
| 497 | } | ||
| 498 | } | ||
| 499 | skip -= cs; | ||
| 500 | } | ||
| 501 | // Write the data. | ||
| 502 | let mut cbuf = vec![0u8; cluster_size()]; | ||
| 503 | let mut remaining = buf.len() as u64; | ||
| 504 | let mut in_off = 0usize; | ||
| 505 | while remaining > 0 { | ||
| 506 | read_cluster(cur, &mut cbuf)?; | ||
| 507 | let from = skip as usize; | ||
| 508 | let n = ((cs as usize) - from).min(remaining as usize); | ||
| 509 | cbuf[from..from + n].copy_from_slice(&buf[in_off..in_off + n]); | ||
| 510 | write_cluster(cur, &cbuf)?; | ||
| 511 | in_off += n; | ||
| 512 | remaining -= n as u64; | ||
| 513 | if remaining > 0 { | ||
| 514 | // Make sure the chain continues past this cluster. | ||
| 515 | match next_cluster(cur) { | ||
| 516 | Ok(n) => cur = n, | ||
| 517 | Err(_) => { | ||
| 518 | let n = alloc_cluster()?; | ||
| 519 | write_fat_entry(cur, n)?; | ||
| 520 | cur = n; | ||
| 521 | } | ||
| 522 | } | ||
| 523 | } | ||
| 524 | skip = 0; | ||
| 525 | } | ||
| 526 | if end > it.size as u64 { | ||
| 527 | update_entry(&it, None, Some(end as u32))?; | ||
| 528 | } | ||
| 529 | Ok(buf.len()) | ||
| 530 | } | ||
| 531 | |||
| 532 | pub fn set_len(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { | ||
| 533 | let it = find_item(parent_dir_cluster, name)?; | ||
| 534 | if it.attr & 0x10 != 0 { | ||
| 535 | return Err(()); | ||
| 536 | } | ||
| 537 | let cs = cluster_size() as u64; | ||
| 538 | let needed = if len == 0 { | ||
| 539 | 0 | ||
| 540 | } else { | ||
| 541 | ((len - 1) / cs + 1) as usize | ||
| 542 | }; | ||
| 543 | let have = chain_len(it.cluster); | ||
| 544 | if it.cluster == 0 { | ||
| 545 | if needed == 0 { | ||
| 546 | update_entry(&it, None, Some(0))?; | ||
| 547 | } else { | ||
| 548 | let first = alloc_cluster()?; | ||
| 549 | if needed > 1 { | ||
| 550 | extend_chain(first, needed - 1)?; | ||
| 551 | } | ||
| 552 | update_entry(&it, Some(first), Some(len as u32))?; | ||
| 553 | } | ||
| 554 | } else if needed < have { | ||
| 555 | truncate_chain(it.cluster, needed); | ||
| 556 | update_entry( | ||
| 557 | &it, | ||
| 558 | if needed == 0 { Some(0) } else { None }, | ||
| 559 | Some(len as u32), | ||
| 560 | )?; | ||
| 561 | } else if needed > have { | ||
| 562 | extend_chain(it.cluster, needed - have)?; | ||
| 563 | update_entry(&it, None, Some(len as u32))?; | ||
| 564 | } else { | ||
| 565 | update_entry(&it, None, Some(len as u32))?; | ||
| 566 | } | ||
| 567 | Ok(()) | ||
| 568 | } | ||
| 569 | |||
| 570 | fn chain_len(mut c: u32) -> usize { | ||
| 571 | let mut n = 0usize; | ||
| 572 | let mut guard = 0; | ||
| 573 | while c != 0 && !is_eoc(read_fat_entry(c)) && guard < 1_000_000 { | ||
| 574 | c = read_fat_entry(c); | ||
| 575 | n += 1; | ||
| 576 | guard += 1; | ||
| 577 | } | ||
| 578 | if c != 0 { | ||
| 579 | n += 1; | ||
| 580 | } | ||
| 581 | n | ||
| 582 | } | ||
| 583 | |||
| 584 | fn truncate_chain(mut c: u32, keep: usize) { | ||
| 585 | if keep == 0 { | ||
| 586 | free_chain(c); | ||
| 587 | return; | ||
| 588 | } | ||
| 589 | let mut idx = 0usize; | ||
| 590 | let mut guard = 0; | ||
| 591 | while idx + 1 < keep { | ||
| 592 | let n = read_fat_entry(c); | ||
| 593 | if is_eoc(n) { | ||
| 594 | return; | ||
| 595 | } | ||
| 596 | c = n; | ||
| 597 | idx += 1; | ||
| 598 | guard += 1; | ||
| 599 | if guard > 1_000_000 { | ||
| 600 | return; | ||
| 601 | } | ||
| 602 | } | ||
| 603 | let nxt = read_fat_entry(c); | ||
| 604 | if !is_eoc(nxt) { | ||
| 605 | free_chain(nxt); | ||
| 606 | let _ = write_fat_entry(c, EOC); | ||
| 607 | } | ||
| 608 | } | ||
| 609 | |||
| 610 | fn extend_chain(c: u32, add: usize) -> Result<(), ()> { | ||
| 611 | let mut last = c; | ||
| 612 | let mut guard = 0; | ||
| 613 | loop { | ||
| 614 | let n = read_fat_entry(last); | ||
| 615 | if is_eoc(n) { | ||
| 616 | break; | ||
| 617 | } | ||
| 618 | last = n; | ||
| 619 | guard += 1; | ||
| 620 | if guard > 1_000_000 { | ||
| 621 | return Err(()); | ||
| 622 | } | ||
| 623 | } | ||
| 624 | for _ in 0..add { | ||
| 625 | let n = alloc_cluster()?; | ||
| 626 | write_fat_entry(last, n)?; | ||
| 627 | last = n; | ||
| 628 | } | ||
| 629 | Ok(()) | ||
| 630 | } | ||
| 631 | |||
| 632 | /// Rewrite one field of a directory entry in place (its sector was cached | ||
| 633 | /// at scan time; the parent dir chain never moves while we hold it). | ||
| 634 | fn update_entry(it: &DirItem, cluster: Option<u32>, size: Option<u32>) -> Result<(), ()> { | ||
| 635 | let mut sec = [0u8; SECTOR]; | ||
| 636 | read_sector(it.sector, &mut sec)?; | ||
| 637 | let e = &mut sec[it.offset..it.offset + 32]; | ||
| 638 | if let Some(c) = cluster { | ||
| 639 | e[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes())); | ||
| 640 | e[26..28].copy_from_slice(&((c as u16).to_le_bytes())); | ||
| 641 | } | ||
| 642 | if let Some(s) = size { | ||
| 643 | e[28..32].copy_from_slice(&s.to_le_bytes()); | ||
| 644 | } | ||
| 645 | write_sector(it.sector, &sec) | ||
| 646 | } | ||
| 647 | |||
| 648 | // --- creation / removal ------------------------------------------------------ | ||
| 649 | |||
| 650 | /// Uppercased 8.3 short name for `name`; `needs_lfn` if the original name | ||
| 651 | /// can't round-trip through the short name (too long or lowercase). | ||
| 652 | fn to_8_3(name: &str) -> Option<([u8; 11], bool)> { | ||
| 653 | if name.is_empty() || name.contains('/') || name == "." || name == ".." { | ||
| 654 | return None; | ||
| 655 | } | ||
| 656 | let upper = name.to_ascii_uppercase(); | ||
| 657 | let (n, e) = match upper.rfind('.') { | ||
| 658 | Some(i) => (&upper[..i], Some(&upper[i + 1..])), | ||
| 659 | None => (upper.as_str(), None), | ||
| 660 | }; | ||
| 661 | if n.is_empty() { | ||
| 662 | return None; | ||
| 663 | } | ||
| 664 | let mut sfn = [b' '; 11]; | ||
| 665 | let nb = n.as_bytes(); | ||
| 666 | for i in 0..8 { | ||
| 667 | if i < nb.len() { | ||
| 668 | sfn[i] = nb[i]; | ||
| 669 | } | ||
| 670 | } | ||
| 671 | if let Some(ext) = e { | ||
| 672 | let eb = ext.as_bytes(); | ||
| 673 | for i in 0..3 { | ||
| 674 | if i < eb.len() { | ||
| 675 | sfn[8 + i] = eb[i]; | ||
| 676 | } | ||
| 677 | } | ||
| 678 | } | ||
| 679 | let needs = nb.len() > 8 || e.map(|x| x.len() > 3).unwrap_or(false) || name != upper; | ||
| 680 | Some((sfn, needs)) | ||
| 681 | } | ||
| 682 | |||
| 683 | /// Write the LFN entries (if any) + SFN entry for `name` into the parent | ||
| 684 | /// directory, extending its chain with a fresh zeroed cluster if needed. | ||
| 685 | fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> { | ||
| 686 | let (sfn, needs_lfn) = to_8_3(name).ok_or(())?; | ||
| 687 | let lfn_chars: Vec<u16> = name.encode_utf16().collect(); | ||
| 688 | let groups = if needs_lfn { | ||
| 689 | lfn_chars.len().div_ceil(13) | ||
| 690 | } else { | ||
| 691 | 0 | ||
| 692 | }; | ||
| 693 | let needed = groups + 1; | ||
| 694 | let cs = cluster_size(); | ||
| 695 | let mut buf = vec![0u8; cs]; | ||
| 696 | let mut cur: Option<u32> = Some(parent); | ||
| 697 | let mut guard = 0; | ||
| 698 | loop { | ||
| 699 | guard += 1; | ||
| 700 | if guard > 1_000_000 { | ||
| 701 | return Err(()); | ||
| 702 | } | ||
| 703 | let c = cur.ok_or(())?; | ||
| 704 | read_cluster(c, &mut buf)?; | ||
| 705 | let slots = cs / 32; | ||
| 706 | let mut run = 0usize; | ||
| 707 | let mut start = 0usize; | ||
| 708 | for i in 0..slots { | ||
| 709 | if buf[i * 32] == 0x00 || buf[i * 32] == 0xE5 { | ||
| 710 | run += 1; | ||
| 711 | if run == 1 { | ||
| 712 | start = i; | ||
| 713 | } | ||
| 714 | if run >= needed { | ||
| 715 | break; | ||
| 716 | } | ||
| 717 | } else { | ||
| 718 | run = 0; | ||
| 719 | } | ||
| 720 | } | ||
| 721 | if run >= needed { | ||
| 722 | if groups > 0 { | ||
| 723 | for g in 0..groups { | ||
| 724 | let seq = (groups - g) as u8 | if g == groups - 1 { 0x40 } else { 0 }; | ||
| 725 | let e = &mut buf[(start + g) * 32..(start + g) * 32 + 32]; | ||
| 726 | e.fill(0); | ||
| 727 | e[0] = seq; | ||
| 728 | e[11] = 0x0F; | ||
| 729 | e[13] = lfn_checksum(&sfn); | ||
| 730 | for k in 0..13 { | ||
| 731 | let ci = g * 13 + k; | ||
| 732 | let ch: u16 = if ci < lfn_chars.len() { | ||
| 733 | lfn_chars[ci] | ||
| 734 | } else if ci == lfn_chars.len() { | ||
| 735 | 0 | ||
| 736 | } else { | ||
| 737 | 0xFFFF | ||
| 738 | }; | ||
| 739 | let pos = if k < 5 { | ||
| 740 | 1 + k * 2 | ||
| 741 | } else if k < 11 { | ||
| 742 | 14 + (k - 5) * 2 | ||
| 743 | } else { | ||
| 744 | 28 + (k - 11) * 2 | ||
| 745 | }; | ||
| 746 | e[pos] = (ch & 0xFF) as u8; | ||
| 747 | e[pos + 1] = (ch >> 8) as u8; | ||
| 748 | } | ||
| 749 | } | ||
| 750 | } | ||
| 751 | let e = &mut buf[(start + groups) * 32..(start + groups) * 32 + 32]; | ||
| 752 | e.fill(0); | ||
| 753 | e[0..11].copy_from_slice(&sfn); | ||
| 754 | e[11] = if is_dir { 0x10 } else { 0x20 }; | ||
| 755 | e[20..22].copy_from_slice(&(((cluster >> 16) as u16).to_le_bytes())); | ||
| 756 | e[26..28].copy_from_slice(&((cluster as u16).to_le_bytes())); | ||
| 757 | write_cluster(c, &buf)?; | ||
| 758 | return Ok(()); | ||
| 759 | } | ||
| 760 | // Chain full: extend with a fresh zeroed cluster and retry. | ||
| 761 | match next_cluster(c) { | ||
| 762 | Ok(n) => cur = Some(n), | ||
| 763 | Err(_) => { | ||
| 764 | let n = alloc_cluster()?; | ||
| 765 | write_fat_entry(c, n)?; | ||
| 766 | cur = Some(n); | ||
| 767 | } | ||
| 768 | } | ||
| 769 | } | ||
| 770 | } | ||
| 771 | |||
| 772 | pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { | ||
| 773 | if find(parent_dir_cluster, name).is_ok() { | ||
| 774 | return Err(()); | ||
| 775 | } | ||
| 776 | let cluster = if is_dir { | ||
| 777 | let c = alloc_cluster()?; | ||
| 778 | // "." and ".." entries. | ||
| 779 | let cs = cluster_size(); | ||
| 780 | let mut buf = vec![0u8; cs]; | ||
| 781 | let dot = &mut buf[0..32]; | ||
| 782 | dot.fill(0); | ||
| 783 | dot[0] = b'.'; | ||
| 784 | dot[1..11].fill(b' '); // SFN is space-padded | ||
| 785 | dot[11] = 0x10; | ||
| 786 | dot[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes())); | ||
| 787 | dot[26..28].copy_from_slice(&((c as u16).to_le_bytes())); | ||
| 788 | let dotdot = &mut buf[32..64]; | ||
| 789 | dotdot.fill(0); | ||
| 790 | dotdot[0] = b'.'; | ||
| 791 | dotdot[1] = b'.'; | ||
| 792 | dotdot[2..11].fill(b' '); // SFN is space-padded | ||
| 793 | dotdot[11] = 0x10; | ||
| 794 | // ".." points at the parent; the root has cluster number 0. | ||
| 795 | let parent_cluster = if parent_dir_cluster == v().root_cluster { | ||
| 796 | 0 | ||
| 797 | } else { | ||
| 798 | parent_dir_cluster | ||
| 799 | }; | ||
| 800 | dotdot[20..22].copy_from_slice(&(((parent_cluster >> 16) as u16).to_le_bytes())); | ||
| 801 | dotdot[26..28].copy_from_slice(&((parent_cluster as u16).to_le_bytes())); | ||
| 802 | write_cluster(c, &buf)?; | ||
| 803 | c | ||
| 804 | } else { | ||
| 805 | 0 | ||
| 806 | }; | ||
| 807 | add_dir_entry(parent_dir_cluster, name, is_dir, cluster) | ||
| 808 | } | ||
| 809 | |||
| 810 | pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { | ||
| 811 | let it = find_item(parent_dir_cluster, name)?; | ||
| 812 | if it.attr & 0x10 != 0 { | ||
| 813 | // Only empty directories may be removed. | ||
| 814 | let kids = list_dir(it.cluster)?; | ||
| 815 | if !kids.is_empty() { | ||
| 816 | return Err(()); | ||
| 817 | } | ||
| 818 | free_chain(it.cluster); | ||
| 819 | } else if it.cluster != 0 { | ||
| 820 | free_chain(it.cluster); | ||
| 821 | } | ||
| 822 | // Mark the SFN and its leading LFN entries deleted. | ||
| 823 | let mut sec = [0u8; SECTOR]; | ||
| 824 | read_sector(it.sector, &mut sec)?; | ||
| 825 | let mut off = it.offset; | ||
| 826 | sec[off] = 0xE5; | ||
| 827 | while off >= 32 { | ||
| 828 | let prev = &sec[off - 32..off]; | ||
| 829 | if prev[11] == 0x0F { | ||
| 830 | sec[off - 32] = 0xE5; | ||
| 831 | off -= 32; | ||
| 832 | } else { | ||
| 833 | break; | ||
| 834 | } | ||
| 835 | } | ||
| 836 | write_sector(it.sector, &sec) | ||
| 837 | } | ||
| 838 | |||
| 839 | // --- mount ------------------------------------------------------------------- | ||
| 840 | |||
| 841 | /// Parse the BPB, cache the FAT and validate the volume. Returns true if | ||
| 842 | /// a usable FAT32 volume is mounted. | ||
| 843 | pub fn init() -> bool { | ||
| 844 | let mut bpb = [0u8; SECTOR]; | ||
| 845 | if read_sector(0, &mut bpb).is_err() { | ||
| 846 | crate::serial_println!("fat: no disk"); | ||
| 847 | return false; | ||
| 848 | } | ||
| 849 | let bytes_per_sector = u16::from_le_bytes([bpb[11], bpb[12]]); | ||
| 850 | if bytes_per_sector != 512 { | ||
| 851 | crate::serial_println!("fat: unsupported sector size {}", bytes_per_sector); | ||
| 852 | return false; | ||
| 853 | } | ||
| 854 | let spc = bpb[13]; | ||
| 855 | if spc == 0 || spc & (spc - 1) != 0 { | ||
| 856 | crate::serial_println!("fat: bad sectors/cluster {}", spc); | ||
| 857 | return false; | ||
| 858 | } | ||
| 859 | let reserved = u16::from_le_bytes([bpb[14], bpb[15]]); | ||
| 860 | let fat_count = bpb[16]; | ||
| 861 | let total16 = u16::from_le_bytes([bpb[19], bpb[20]]); | ||
| 862 | let total32 = u32::from_le_bytes([bpb[32], bpb[33], bpb[34], bpb[35]]); | ||
| 863 | let total = if total16 != 0 { | ||
| 864 | total16 as u32 | ||
| 865 | } else { | ||
| 866 | total32 | ||
| 867 | }; | ||
| 868 | let fat_size = u32::from_le_bytes([bpb[36], bpb[37], bpb[38], bpb[39]]); | ||
| 869 | let root_cluster = u32::from_le_bytes([bpb[44], bpb[45], bpb[46], bpb[47]]); | ||
| 870 | let fsinfo_sec = u16::from_le_bytes([bpb[48], bpb[49]]) as u32; | ||
| 871 | if fat_size == 0 || root_cluster < 2 || reserved == 0 || fat_count == 0 { | ||
| 872 | crate::serial_println!("fat: not FAT32 (root={} fat={})", root_cluster, fat_size); | ||
| 873 | return false; | ||
| 874 | } | ||
| 875 | let first_data = reserved as u32 + fat_count as u32 * fat_size; | ||
| 876 | if first_data >= total { | ||
| 877 | crate::serial_println!("fat: bad geometry"); | ||
| 878 | return false; | ||
| 879 | } | ||
| 880 | let cluster_count = (total - first_data) / spc as u32; | ||
| 881 | |||
| 882 | // Load the whole FAT into RAM (chunked through the 4 KiB DMA buffer). | ||
| 883 | let fat_bytes = fat_size as usize * SECTOR; | ||
| 884 | let mut fat = alloc::vec![0u8; fat_bytes]; | ||
| 885 | let mut lba = reserved as u64; | ||
| 886 | let mut done = 0usize; | ||
| 887 | while done < fat.len() { | ||
| 888 | let n = ((fat.len() - done) / SECTOR).min(8) as u32; | ||
| 889 | if ahci::read_sectors(lba, n).is_err() { | ||
| 890 | crate::serial_println!("fat: FAT read failed at lba {}", lba); | ||
| 891 | return false; | ||
| 892 | } | ||
| 893 | unsafe { | ||
| 894 | core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR); | ||
| 895 | } | ||
| 896 | lba += n as u64; | ||
| 897 | done += n as usize * SECTOR; | ||
| 898 | } | ||
| 899 | // Sanity: FAT[0] holds the media byte, FAT[1] should be an EOC. | ||
| 900 | let e0 = u32::from_le_bytes([fat[0], fat[1], fat[2], fat[3]]); | ||
| 901 | let e1 = u32::from_le_bytes([fat[4], fat[5], fat[6], fat[7]]); | ||
| 902 | if e0 & 0xFF != 0xF8 || e1 & EOC_MASK != EOC { | ||
| 903 | crate::serial_println!( | ||
| 904 | "fat: bad FAT markers ({:#x} {:#x})", | ||
| 905 | e0 & 0xFF, | ||
| 906 | e1 & EOC_MASK | ||
| 907 | ); | ||
| 908 | return false; | ||
| 909 | } | ||
| 910 | unsafe { | ||
| 911 | VOL = Some(Volume { | ||
| 912 | sectors_per_cluster: spc, | ||
| 913 | reserved, | ||
| 914 | fat_count, | ||
| 915 | fat_size, | ||
| 916 | root_cluster, | ||
| 917 | first_data, | ||
| 918 | cluster_count, | ||
| 919 | fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec), | ||
| 920 | fat, | ||
| 921 | }); | ||
| 922 | } | ||
| 923 | // Re-sync the FSInfo free-cluster count (may be stale from earlier | ||
| 924 | // sessions that predate FSInfo maintenance). | ||
| 925 | fsinfo_recount(); | ||
| 926 | let count = list_dir(root_cluster).map(|v| v.len()).unwrap_or(0); | ||
| 927 | crate::serial_println!( | ||
| 928 | "fat: FAT32 mounted — {} clusters ({} bytes each), root: {} entries", | ||
| 929 | cluster_count, | ||
| 930 | cluster_size(), | ||
| 931 | count | ||
| 932 | ); | ||
| 933 | true | ||
| 934 | } | ||
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 { | |||
| 51 | Regular(Vec<u8>), | 51 | Regular(Vec<u8>), |
| 52 | Dir(Vec<usize>), | 52 | Dir(Vec<usize>), |
| 53 | Char(CharDevice), | 53 | Char(CharDevice), |
| 54 | Disk(DiskInfo), | ||
| 55 | } | ||
| 56 | |||
| 57 | /// A node backed by the FAT32 volume (`/disk` mount). `parent` is the | ||
| 58 | /// directory cluster holding the entry (used to re-locate it on every | ||
| 59 | /// op, so it stays correct as the volume changes), `cluster` the entry's | ||
| 60 | /// own first cluster. | ||
| 61 | struct DiskInfo { | ||
| 62 | parent: u32, | ||
| 63 | name: String, | ||
| 64 | is_dir: bool, | ||
| 65 | size: u64, | ||
| 66 | cluster: u32, | ||
| 54 | } | 67 | } |
| 55 | 68 | ||
| 56 | struct Node { | 69 | struct Node { |
| @@ -161,6 +174,23 @@ pub fn init() -> (usize, usize) { | |||
| 161 | ), | 174 | ), |
| 162 | ); | 175 | ); |
| 163 | 176 | ||
| 177 | // Mount the FAT32 volume at /disk when one is present. | ||
| 178 | if crate::fat::is_mounted() { | ||
| 179 | let root_cl = crate::fat::root_cluster(); | ||
| 180 | add( | ||
| 181 | "disk", | ||
| 182 | Some(root), | ||
| 183 | FileType::Dir, | ||
| 184 | NodeData::Disk(DiskInfo { | ||
| 185 | parent: root_cl, | ||
| 186 | name: String::new(), | ||
| 187 | is_dir: true, | ||
| 188 | size: 0, | ||
| 189 | cluster: root_cl, | ||
| 190 | }), | ||
| 191 | ); | ||
| 192 | } | ||
| 193 | |||
| 164 | let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); | 194 | let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); |
| 165 | let files = nodes().len() - dirs; | 195 | let files = nodes().len() - dirs; |
| 166 | (dirs, files) | 196 | (dirs, files) |
| @@ -180,22 +210,52 @@ fn lookup(dir: usize, name: &str) -> Option<usize> { | |||
| 180 | None | 210 | None |
| 181 | } | 211 | } |
| 182 | 212 | ||
| 183 | /// Resolve an absolute path (supports `.` and `..`). | 213 | /// Resolve an absolute path (supports `.` and `..`). Paths under |
| 214 | /// `/disk` walk the mounted FAT32 volume through ephemeral Disk nodes. | ||
| 184 | pub fn resolve(path: &str) -> Option<usize> { | 215 | pub fn resolve(path: &str) -> Option<usize> { |
| 185 | if nodes().is_empty() { | 216 | if nodes().is_empty() { |
| 186 | return None; | 217 | return None; |
| 187 | } | 218 | } |
| 188 | let mut cur = 0usize; // root | 219 | let mut cur = 0usize; // root |
| 220 | // (cluster, parent, name) of directories descended into under /disk. | ||
| 221 | let mut dstack: Vec<(u32, u32, String)> = Vec::new(); | ||
| 189 | for seg in path.split('/') { | 222 | for seg in path.split('/') { |
| 190 | match seg { | 223 | match seg { |
| 191 | "" | "." => {} | 224 | "" | "." => {} |
| 192 | ".." => { | 225 | ".." => { |
| 193 | if let Some(p) = nodes()[cur].parent { | 226 | if let Some((cl, p, n)) = dstack.pop() { |
| 227 | cur = materialize_disk(DiskInfo { | ||
| 228 | parent: p, | ||
| 229 | name: n, | ||
| 230 | is_dir: true, | ||
| 231 | size: 0, | ||
| 232 | cluster: cl, | ||
| 233 | }); | ||
| 234 | } else if let Some(p) = nodes()[cur].parent { | ||
| 194 | cur = p; | 235 | cur = p; |
| 195 | } | 236 | } |
| 196 | } | 237 | } |
| 197 | name => { | 238 | name => { |
| 198 | cur = lookup(cur, name)?; | 239 | let disk_dir: Option<u32> = match &nodes()[cur].data { |
| 240 | NodeData::Disk(info) => Some(info.cluster), | ||
| 241 | _ => None, | ||
| 242 | }; | ||
| 243 | if let Some(dir_cluster) = disk_dir { | ||
| 244 | let e = crate::fat::find(dir_cluster, name).ok()?; | ||
| 245 | let d = DiskInfo { | ||
| 246 | parent: dir_cluster, | ||
| 247 | name: e.name.clone(), | ||
| 248 | is_dir: e.is_dir, | ||
| 249 | size: e.size, | ||
| 250 | cluster: e.cluster, | ||
| 251 | }; | ||
| 252 | if e.is_dir { | ||
| 253 | dstack.push((e.cluster, dir_cluster, e.name)); | ||
| 254 | } | ||
| 255 | cur = materialize_disk(d); | ||
| 256 | } else { | ||
| 257 | cur = lookup(cur, name)?; | ||
| 258 | } | ||
| 199 | } | 259 | } |
| 200 | } | 260 | } |
| 201 | } | 261 | } |
| @@ -209,12 +269,34 @@ pub fn kind(h: usize) -> FileType { | |||
| 209 | pub fn size(h: usize) -> u64 { | 269 | pub fn size(h: usize) -> u64 { |
| 210 | match &nodes()[h].data { | 270 | match &nodes()[h].data { |
| 211 | NodeData::Regular(v) => v.len() as u64, | 271 | NodeData::Regular(v) => v.len() as u64, |
| 272 | NodeData::Disk(i) => i.size, | ||
| 212 | _ => 0, | 273 | _ => 0, |
| 213 | } | 274 | } |
| 214 | } | 275 | } |
| 215 | 276 | ||
| 216 | /// Entries of a directory as (name, kind, size) — tombstoned nodes skipped. | 277 | /// Entries of a directory as (name, kind, size) — tombstoned nodes |
| 278 | /// skipped; disk directories are listed fresh from the FAT volume. | ||
| 217 | pub fn children(h: usize) -> Vec<(String, FileType, u64)> { | 279 | pub fn children(h: usize) -> Vec<(String, FileType, u64)> { |
| 280 | if let NodeData::Disk(i) = &nodes()[h].data { | ||
| 281 | let cl = i.cluster; | ||
| 282 | return crate::fat::list_dir(cl) | ||
| 283 | .map(|es| { | ||
| 284 | es.into_iter() | ||
| 285 | .map(|e| { | ||
| 286 | ( | ||
| 287 | e.name, | ||
| 288 | if e.is_dir { | ||
| 289 | FileType::Dir | ||
| 290 | } else { | ||
| 291 | FileType::Regular | ||
| 292 | }, | ||
| 293 | e.size, | ||
| 294 | ) | ||
| 295 | }) | ||
| 296 | .collect() | ||
| 297 | }) | ||
| 298 | .unwrap_or_default(); | ||
| 299 | } | ||
| 218 | let kids = match &nodes()[h].data { | 300 | let kids = match &nodes()[h].data { |
| 219 | NodeData::Dir(v) => v.clone(), | 301 | NodeData::Dir(v) => v.clone(), |
| 220 | _ => return Vec::new(), | 302 | _ => return Vec::new(), |
| @@ -275,6 +357,7 @@ pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result<usize, ()> { | |||
| 275 | buf[..n].copy_from_slice(&bytes[p..p + n]); | 357 | buf[..n].copy_from_slice(&bytes[p..p + n]); |
| 276 | Ok(n) | 358 | Ok(n) |
| 277 | } | 359 | } |
| 360 | NodeData::Disk(i) => crate::fat::read_file(i.parent, &i.name, buf, pos), | ||
| 278 | NodeData::Dir(_) => Err(()), | 361 | NodeData::Dir(_) => Err(()), |
| 279 | } | 362 | } |
| 280 | } | 363 | } |
| @@ -311,6 +394,7 @@ pub fn write(h: usize, buf: &[u8], pos: u64) -> Result<usize, ()> { | |||
| 311 | } | 394 | } |
| 312 | _ => Err(()), // read-only devices | 395 | _ => Err(()), // read-only devices |
| 313 | }, | 396 | }, |
| 397 | NodeData::Disk(i) => crate::fat::write_file(i.parent, &i.name, buf, pos), | ||
| 314 | NodeData::Dir(_) => Err(()), | 398 | NodeData::Dir(_) => Err(()), |
| 315 | } | 399 | } |
| 316 | } | 400 | } |
| @@ -322,6 +406,7 @@ pub fn set_len(h: usize, len: u64) -> Result<(), ()> { | |||
| 322 | v.resize(len as usize, 0); | 406 | v.resize(len as usize, 0); |
| 323 | Ok(()) | 407 | Ok(()) |
| 324 | } | 408 | } |
| 409 | NodeData::Disk(i) => crate::fat::set_len(i.parent, &i.name, len), | ||
| 325 | _ => Err(()), | 410 | _ => Err(()), |
| 326 | } | 411 | } |
| 327 | } | 412 | } |
| @@ -331,6 +416,30 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> { | |||
| 331 | if name.is_empty() || name.contains('/') || kind == FileType::Char { | 416 | if name.is_empty() || name.contains('/') || kind == FileType::Char { |
| 332 | return Err(()); | 417 | return Err(()); |
| 333 | } | 418 | } |
| 419 | // Disk directory: create through the FAT volume. | ||
| 420 | let disk_parent: Option<u32> = match &nodes()[parent].data { | ||
| 421 | NodeData::Disk(i) => { | ||
| 422 | if !i.is_dir { | ||
| 423 | return Err(()); | ||
| 424 | } | ||
| 425 | Some(i.cluster) | ||
| 426 | } | ||
| 427 | _ => None, | ||
| 428 | }; | ||
| 429 | if let Some(p) = disk_parent { | ||
| 430 | if crate::fat::find(p, name).is_ok() { | ||
| 431 | return Err(()); | ||
| 432 | } | ||
| 433 | crate::fat::create(p, name, kind == FileType::Dir)?; | ||
| 434 | let e = crate::fat::find(p, name)?; | ||
| 435 | return Ok(materialize_disk(DiskInfo { | ||
| 436 | parent: p, | ||
| 437 | name: e.name, | ||
| 438 | is_dir: e.is_dir, | ||
| 439 | size: e.size, | ||
| 440 | cluster: e.cluster, | ||
| 441 | })); | ||
| 442 | } | ||
| 334 | if lookup(parent, name).is_some() { | 443 | if lookup(parent, name).is_some() { |
| 335 | return Err(()); | 444 | return Err(()); |
| 336 | } | 445 | } |
| @@ -344,6 +453,15 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> { | |||
| 344 | 453 | ||
| 345 | /// Remove a regular file or an empty directory (tombstone). | 454 | /// Remove a regular file or an empty directory (tombstone). |
| 346 | pub fn remove(h: usize) -> Result<(), ()> { | 455 | pub fn remove(h: usize) -> Result<(), ()> { |
| 456 | // Disk nodes: delete through the FAT volume (dirs must be empty). | ||
| 457 | if let NodeData::Disk(i) = &nodes()[h].data { | ||
| 458 | if i.name.is_empty() { | ||
| 459 | return Err(()); // the /disk mount itself | ||
| 460 | } | ||
| 461 | let p = i.parent; | ||
| 462 | let n = i.name.clone(); | ||
| 463 | return crate::fat::remove(p, &n); | ||
| 464 | } | ||
| 347 | let parent = match nodes()[h].parent { | 465 | let parent = match nodes()[h].parent { |
| 348 | Some(p) => p, | 466 | Some(p) => p, |
| 349 | None => return Err(()), | 467 | None => return Err(()), |
| @@ -414,6 +532,44 @@ pub fn remove_path(path: &str) -> Result<(), ()> { | |||
| 414 | remove(h) | 532 | remove(h) |
| 415 | } | 533 | } |
| 416 | 534 | ||
| 535 | // --- disk mount ------------------------------------------------------------ | ||
| 536 | |||
| 537 | /// Round-robin pool of ephemeral disk nodes so a long-lived shell doesn't | ||
| 538 | /// grow the node table forever. | ||
| 539 | const DISK_SLOTS: usize = 24; | ||
| 540 | static mut DISK_POOL: *mut Vec<usize> = core::ptr::null_mut(); | ||
| 541 | static mut DISK_ROBIN: usize = 0; | ||
| 542 | |||
| 543 | fn materialize_disk(info: DiskInfo) -> usize { | ||
| 544 | unsafe { | ||
| 545 | if DISK_POOL.is_null() { | ||
| 546 | DISK_POOL = Box::into_raw(Box::new(Vec::new())); | ||
| 547 | } | ||
| 548 | let pool = &mut *DISK_POOL; | ||
| 549 | let idx = DISK_ROBIN % DISK_SLOTS; | ||
| 550 | DISK_ROBIN = DISK_ROBIN.wrapping_add(1); | ||
| 551 | let kind = if info.is_dir { | ||
| 552 | FileType::Dir | ||
| 553 | } else { | ||
| 554 | FileType::Regular | ||
| 555 | }; | ||
| 556 | if idx < pool.len() { | ||
| 557 | let h = pool[idx]; | ||
| 558 | let n = &mut nodes()[h]; | ||
| 559 | n.name = info.name.clone(); | ||
| 560 | n.kind = kind; | ||
| 561 | n.parent = None; | ||
| 562 | n.data = NodeData::Disk(info); | ||
| 563 | n.removed = false; | ||
| 564 | return h; | ||
| 565 | } | ||
| 566 | let name = info.name.clone(); | ||
| 567 | let h = add(&name, None, kind, NodeData::Disk(info)); | ||
| 568 | pool.push(h); | ||
| 569 | h | ||
| 570 | } | ||
| 571 | } | ||
| 572 | |||
| 417 | // --- character devices ---------------------------------------------------- | 573 | // --- character devices ---------------------------------------------------- |
| 418 | 574 | ||
| 419 | fn version_text() -> String { | 575 | 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 { | |||
| 167 | if SHADOW.is_null() { | 167 | if SHADOW.is_null() { |
| 168 | // No shadow (allocation failed): fall back to shifting the | 168 | // No shadow (allocation failed): fall back to shifting the |
| 169 | // display directly. | 169 | // display directly. |
| 170 | ptr::copy(self.base.add(n * row_bytes), self.base, total - n * row_bytes); | 170 | ptr::copy( |
| 171 | self.base.add(n * row_bytes), | ||
| 172 | self.base, | ||
| 173 | total - n * row_bytes, | ||
| 174 | ); | ||
| 171 | let last_row = self.base.add(total - n * row_bytes); | 175 | let last_row = self.base.add(total - n * row_bytes); |
| 172 | for i in 0..(n * row_words) { | 176 | for i in 0..(n * row_words) { |
| 173 | ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); | 177 | 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 @@ | |||
| 2 | 2 | ||
| 3 | extern crate alloc; | 3 | extern crate alloc; |
| 4 | 4 | ||
| 5 | pub mod ahci; | ||
| 6 | pub mod fat; | ||
| 5 | pub mod font; | 7 | pub mod font; |
| 6 | pub mod fs; | 8 | pub mod fs; |
| 7 | pub mod gop; | 9 | 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 { | |||
| 57 | inl(CONFIG_DATA) | 57 | inl(CONFIG_DATA) |
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | /// Write a dword to a device's config space (`reg` must be dword-aligned). | ||
| 61 | pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { | ||
| 62 | let addr = 0x8000_0000 | ||
| 63 | | ((bus as u32) << 16) | ||
| 64 | | ((device as u32) << 11) | ||
| 65 | | ((func as u32) << 8) | ||
| 66 | | ((reg as u32) & 0xFC); | ||
| 67 | outl(CONFIG_ADDRESS, addr); | ||
| 68 | outl(CONFIG_DATA, val) | ||
| 69 | } | ||
| 70 | |||
| 71 | /// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit | ||
| 72 | /// BARs. Returns None for I/O BARs or invalid values. | ||
| 73 | pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> { | ||
| 74 | let reg_off = 0x10 + bar_index * 4; | ||
| 75 | let lo = config_read(bus, device, func, reg_off); | ||
| 76 | if lo & 1 == 1 { | ||
| 77 | return None; // I/O BAR | ||
| 78 | } | ||
| 79 | let kind = (lo >> 1) & 0x3; | ||
| 80 | let hi = if kind == 2 { | ||
| 81 | config_read(bus, device, func, reg_off + 4) | ||
| 82 | } else { | ||
| 83 | 0 | ||
| 84 | }; | ||
| 85 | let base = ((hi as u64) << 32) | (lo as u64 & 0xFFFF_FFF0); | ||
| 86 | if base == 0 { | ||
| 87 | None | ||
| 88 | } else { | ||
| 89 | Some(base) | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 60 | /// Enumerate every device/function present on all buses. | 93 | /// Enumerate every device/function present on all buses. |
| 61 | pub fn probe() -> alloc::vec::Vec<DeviceInfo> { | 94 | pub fn probe() -> alloc::vec::Vec<DeviceInfo> { |
| 62 | let mut out = alloc::vec::Vec::new(); | 95 | 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() { | |||
| 432 | // appears promptly. | 432 | // appears promptly. |
| 433 | const TICK_LIMIT: u16 = 4; | 433 | const TICK_LIMIT: u16 = 4; |
| 434 | let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); | 434 | let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); |
| 435 | if PENDING_SCROLLS >= SCROLL_BATCH | 435 | if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { |
| 436 | || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT | ||
| 437 | { | ||
| 438 | sync_scroll(); | 436 | sync_scroll(); |
| 439 | } | 437 | } |
| 440 | } | 438 | } |
