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 --- rsnix/src/fs.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 4 deletions(-) (limited to 'rsnix/src/fs.rs') 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 { -- cgit v1.2.3