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/src/fs.rs | |
| 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/fs.rs | 164 |
1 files changed, 160 insertions, 4 deletions
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 { |
