From 92ed4f6668efccf6af73f45654405d333e6a318c Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 02:19:07 +0200 Subject: fat: mount every FAT32 volume under /disk//, fix multi-entry LFN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fat.rs: VOL: Option → VOLS: Vec; try_mount() returns a Volume per disk and init() mounts every valid FAT32 volume found. Volume UID = sanitized uppercase label + 8-hex volume serial (e.g. RSDOS-5BD1E55F): unique per format, stable across reboots, human-recognizable, machine-parseable (^[A-Z0-9_]+-[0-9A-F]{8}$); cloned volumes get a -N suffix. Public ops take a vol index and select it (select_vol also picks the backing AHCI disk), matching the ahci::select pattern - fs.rs: /disk is now a plain directory whose children are one Disk node per mounted volume, named by UID; DiskInfo carries vol - LFN fix: entries are stored last-first on disk — both the writer (head-entry first) and reader (append in disk order) were reversed and cancelled out for ≤13-char names; multi-entry names (>13 chars) read back mangled (e.g. 'from-kernel.txt' as 'xt'). Writer now places seq=1 closest to the SFN and seq=N|0x40 first; reader places chunks by sequence number and trims at the first NUL. Verified with a 25-char filename round trip: kernel ls/cat correct, mtools shows the full LFN, fsck clean on both volumes - test-disk.sh: test file renamed to disk-test-.txt (>13 chars, exercises multi-entry LFN); UID discovered from the boot log; QEMU stdio redirected to a log file + EXIT trap so an orphaned QEMU can never hang a piped caller (it previously did, 400s) --- rsnix/src/fat.rs | 201 +++++++++++++++++++++++++++++++++++++++---------------- rsnix/src/fs.rs | 76 ++++++++++++--------- 2 files changed, 188 insertions(+), 89 deletions(-) (limited to 'rsnix') diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs index 616a08f..c486dfe 100644 --- a/rsnix/src/fat.rs +++ b/rsnix/src/fat.rs @@ -34,6 +34,8 @@ pub struct Entry { } struct Volume { + disk: usize, // AHCI disk index this volume was mounted from + uid: String, // unique mount name: "LABEL-XXXXXXXX" sectors_per_cluster: u8, reserved: u16, fat_count: u8, @@ -45,14 +47,34 @@ struct Volume { fat: Vec, // FAT 1 cached in RAM } -static mut VOL: Option = None; +static mut VOLS: Vec = Vec::new(); +static mut ACTIVE_VOL: usize = 0; -pub fn is_mounted() -> bool { - unsafe { VOL.is_some() } +pub fn volume_count() -> usize { + unsafe { VOLS.len() } } -pub fn root_cluster() -> u32 { - unsafe { VOL.as_ref() }.map(|v| v.root_cluster).unwrap_or(0) +/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F"). +pub fn uid(vol: usize) -> &'static str { + unsafe { VOLS.get(vol) }.map(|v| v.uid.as_str()).unwrap_or("") +} + +pub fn root_cluster(vol: usize) -> u32 { + unsafe { VOLS.get(vol) }.map(|v| v.root_cluster).unwrap_or(0) +} + +/// Point all FAT operations at volume `vol`; also selects the backing +/// AHCI disk. Called by every public entry point (single-threaded +/// kernel, so a process-wide "current volume" is fine). +fn select_vol(vol: usize) -> Result<(), ()> { + let disk = unsafe { VOLS.get(vol) }.map(|v| v.disk).ok_or(())?; + unsafe { + ACTIVE_VOL = vol; + } + if !crate::ahci::select(disk) { + return Err(()); + } + Ok(()) } // --- sector I/O ----------------------------------------------------------- @@ -71,7 +93,7 @@ fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> { // --- cluster math ---------------------------------------------------------- fn v() -> &'static mut Volume { - unsafe { VOL.as_mut() }.unwrap() + unsafe { VOLS.get_mut(ACTIVE_VOL) }.unwrap() } fn spc() -> usize { @@ -336,7 +358,19 @@ fn scan_dir(dir_cluster: u32) -> Result, ()> { _ => { let attr = e[11]; if attr == 0x0F { - // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, 28-31). + // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, + // 28-31). Entries are stored last-first on disk; + // place each chunk by its sequence number so + // multi-entry names reassemble in the right order. + let seq = (e[0] & 0x1F) as usize; + if seq == 0 || seq > 20 { + continue; + } + let need = seq * 13; + if lfn.len() < need { + lfn.resize(need, 0); + } + let base = (seq - 1) * 13; for k in 0..13 { let pos = if k < 5 { 1 + k * 2 @@ -347,11 +381,9 @@ fn scan_dir(dir_cluster: u32) -> Result, ()> { }; let ch = u16::from_le_bytes([e[pos], e[pos + 1]]); if ch == 0x0000 { - break; - } - if ch != 0xFFFF { - lfn.push(ch); + break; // end of the name; rest is 0xFFFF padding } + lfn[base + k] = ch; } lfn_sum = e[13]; } else { @@ -359,7 +391,8 @@ fn scan_dir(dir_cluster: u32) -> Result, ()> { 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) + let end = lfn.iter().position(|&c| c == 0).unwrap_or(lfn.len()); + utf16_to_string(&lfn[..end]) } else { sfn_decode(e) }; @@ -387,7 +420,8 @@ fn scan_dir(dir_cluster: u32) -> Result, ()> { } /// Public listing: no `.` / `..`. -pub fn list_dir(dir_cluster: u32) -> Result, ()> { +pub fn list_dir(vol: usize, dir_cluster: u32) -> Result, ()> { + select_vol(vol)?; Ok(scan_dir(dir_cluster)? .into_iter() .filter(|i| i.name != "." && i.name != "..") @@ -412,7 +446,8 @@ fn find_item(parent_dir_cluster: u32, name: &str) -> Result { } /// Find an entry by name (case-insensitive). -pub fn find(parent_dir_cluster: u32, name: &str) -> Result { +pub fn find(vol: usize, parent_dir_cluster: u32, name: &str) -> Result { + select_vol(vol)?; find_item(parent_dir_cluster, name).map(|i| Entry { name: i.name, is_dir: i.attr & 0x10 != 0, @@ -424,15 +459,17 @@ pub fn find(parent_dir_cluster: u32, name: &str) -> Result { // --- file data -------------------------------------------------------------- pub fn read_file( + vol: usize, parent_dir_cluster: u32, name: &str, buf: &mut [u8], pos: u64, ) -> Result { + select_vol(vol)?; if buf.is_empty() { return Ok(0); } - let it = find(parent_dir_cluster, name)?; + let it = find(vol, parent_dir_cluster, name)?; if it.is_dir || it.cluster == 0 { return Err(()); } @@ -467,7 +504,8 @@ pub fn read_file( Ok(out) } -pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result { +pub fn write_file(vol: usize, parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result { + select_vol(vol)?; if buf.is_empty() { return Ok(0); } @@ -529,7 +567,8 @@ pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Ok(buf.len()) } -pub fn set_len(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { +pub fn set_len(vol: usize, parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { + select_vol(vol)?; let it = find_item(parent_dir_cluster, name)?; if it.attr & 0x10 != 0 { return Err(()); @@ -721,8 +760,11 @@ fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result< 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]; + // LFN entries are stored last-first on disk: the tail + // chunk (highest seq, LAST flag) goes first, and the + // head chunk (seq 1) lands immediately before the SFN. + let seq = (g + 1) as u8 | if g == groups - 1 { 0x40 } else { 0 }; + let e = &mut buf[(start + groups - 1 - g) * 32..(start + groups - 1 - g) * 32 + 32]; e.fill(0); e[0] = seq; e[11] = 0x0F; @@ -769,8 +811,9 @@ fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result< } } -pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { - if find(parent_dir_cluster, name).is_ok() { +pub fn create(vol: usize, parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { + select_vol(vol)?; + if find(vol, parent_dir_cluster, name).is_ok() { return Err(()); } let cluster = if is_dir { @@ -807,11 +850,12 @@ pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ( add_dir_entry(parent_dir_cluster, name, is_dir, cluster) } -pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { +pub fn remove(vol: usize, parent_dir_cluster: u32, name: &str) -> Result<(), ()> { + select_vol(vol)?; 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)?; + let kids = list_dir(vol, it.cluster)?; if !kids.is_empty() { return Err(()); } @@ -847,35 +891,59 @@ pub fn init() -> bool { crate::serial_println!("fat: no disk"); return false; } + let mut mounted = false; for d in 0..disks { if !crate::ahci::select(d) { continue; } - if try_mount() { - return true; + match try_mount(d) { + Some(volume) => { + let uid = volume.uid.clone(); + let vol = unsafe { + VOLS.push(volume); + VOLS.len() - 1 + }; + // Re-sync the FSInfo free-cluster count (may be stale + // from earlier sessions). + if select_vol(vol).is_ok() { + fsinfo_recount(); + } + let count = list_dir(vol, root_cluster(vol)) + .map(|es| es.len()) + .unwrap_or(0); + crate::serial_println!( + "fat: mounted \"{}\" at /disk/{} ({} clusters, {} B/cluster, root: {} entries)", + uid, + uid, + v().cluster_count, + cluster_size(), + count + ); + mounted = true; + } + None => crate::serial_println!("fat: no FAT32 volume on disk {}", d), } - crate::serial_println!("fat: no FAT32 volume on disk {}", d); } - false + mounted } /// Parse the BPB of the selected disk, cache the FAT and validate the -/// volume. -fn try_mount() -> bool { +/// volume. Returns the ready-to-mount volume or None. +fn try_mount(d: usize) -> Option { let mut bpb = [0u8; SECTOR]; if read_sector(0, &mut bpb).is_err() { crate::serial_println!("fat: no disk"); - return false; + return None; } 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; + return None; } let spc = bpb[13]; if spc == 0 || spc & (spc - 1) != 0 { crate::serial_println!("fat: bad sectors/cluster {}", spc); - return false; + return None; } let reserved = u16::from_le_bytes([bpb[14], bpb[15]]); let fat_count = bpb[16]; @@ -891,12 +959,12 @@ fn try_mount() -> bool { 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; + return None; } let first_data = reserved as u32 + fat_count as u32 * fat_size; if first_data >= total { crate::serial_println!("fat: bad geometry"); - return false; + return None; } let cluster_count = (total - first_data) / spc as u32; @@ -909,7 +977,7 @@ fn try_mount() -> bool { 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; + return None; } unsafe { core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR); @@ -926,30 +994,45 @@ fn try_mount() -> bool { e0 & 0xFF, e1 & EOC_MASK ); - return false; + return None; } - 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", + // Volume identity: sanitized label + volume serial → "RSDOS-5BD1E55F". + // Unique per format, stable across reboots, human-recognizable, and + // machine-parseable (^[A-Z0-9_]+-[0-9A-F]{8}$). + let serial = u32::from_le_bytes([bpb[67], bpb[68], bpb[69], bpb[70]]); + let mut label = String::new(); + for &c in &bpb[71..82] { + if c == 0 || c == b' ' { + continue; // strip padding/NULs + } + let ch = (c as char).to_ascii_uppercase(); + label.push(if ch.is_ascii_alphanumeric() { ch } else { '_' }); + } + if label.is_empty() { + label = String::from("DISK"); + } + // Deduplicate against already-mounted volumes (e.g. cloned images). + let mut uid = alloc::format!("{}-{:08X}", label, serial); + let mut n = 2u32; + loop { + let taken = unsafe { VOLS.iter() }.any(|v| v.uid == uid); + if !taken { + break; + } + uid = alloc::format!("{}-{:08X}-{}", label, serial, n); + n += 1; + } + Some(Volume { + disk: d, + uid, + sectors_per_cluster: spc, + reserved, + fat_count, + fat_size, + root_cluster, + first_data, cluster_count, - cluster_size(), - count - ); - true + fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec), + fat, + }) } diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index 6950171..4b76e07 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs @@ -59,6 +59,7 @@ enum NodeData { /// op, so it stays correct as the volume changes), `cluster` the entry's /// own first cluster. struct DiskInfo { + vol: usize, // index into fat::VOLS (which FAT32 volume this lives on) parent: u32, name: String, is_dir: bool, @@ -174,21 +175,32 @@ 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( + // Mount every FAT32 volume under /disk// (UID = label-serial). + let disks = crate::fat::volume_count(); + if disks > 0 { + let disk_dir = add( "disk", Some(root), FileType::Dir, - NodeData::Disk(DiskInfo { - parent: root_cl, - name: String::new(), - is_dir: true, - size: 0, - cluster: root_cl, - }), + NodeData::Dir(Vec::new()), ); + for vol in 0..disks { + let uid = String::from(crate::fat::uid(vol)); + let root_cl = crate::fat::root_cluster(vol); + add( + &uid, + Some(disk_dir), + FileType::Dir, + NodeData::Disk(DiskInfo { + vol, + 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(); @@ -217,14 +229,15 @@ pub fn resolve(path: &str) -> Option { 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(); + // (vol, cluster, parent, name) of directories descended into under /disk. + let mut dstack: Vec<(usize, u32, u32, String)> = Vec::new(); for seg in path.split('/') { match seg { "" | "." => {} ".." => { - if let Some((cl, p, n)) = dstack.pop() { + if let Some((vol, cl, p, n)) = dstack.pop() { cur = materialize_disk(DiskInfo { + vol, parent: p, name: n, is_dir: true, @@ -236,13 +249,14 @@ pub fn resolve(path: &str) -> Option { } } name => { - let disk_dir: Option = match &nodes()[cur].data { - NodeData::Disk(info) => Some(info.cluster), + let disk_ctx: Option<(usize, u32)> = match &nodes()[cur].data { + NodeData::Disk(info) => Some((info.vol, info.cluster)), _ => None, }; - if let Some(dir_cluster) = disk_dir { - let e = crate::fat::find(dir_cluster, name).ok()?; + if let Some((vol, dir_cluster)) = disk_ctx { + let e = crate::fat::find(vol, dir_cluster, name).ok()?; let d = DiskInfo { + vol, parent: dir_cluster, name: e.name.clone(), is_dir: e.is_dir, @@ -250,7 +264,7 @@ pub fn resolve(path: &str) -> Option { cluster: e.cluster, }; if e.is_dir { - dstack.push((e.cluster, dir_cluster, e.name)); + dstack.push((vol, e.cluster, dir_cluster, e.name)); } cur = materialize_disk(d); } else { @@ -279,7 +293,7 @@ pub fn size(h: usize) -> u64 { 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) + return crate::fat::list_dir(i.vol, cl) .map(|es| { es.into_iter() .map(|e| { @@ -357,7 +371,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::Disk(i) => crate::fat::read_file(i.vol, i.parent, &i.name, buf, pos), NodeData::Dir(_) => Err(()), } } @@ -394,7 +408,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::Disk(i) => crate::fat::write_file(i.vol, i.parent, &i.name, buf, pos), NodeData::Dir(_) => Err(()), } } @@ -406,7 +420,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), + NodeData::Disk(i) => crate::fat::set_len(i.vol, i.parent, &i.name, len), _ => Err(()), } } @@ -417,22 +431,23 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result { return Err(()); } // Disk directory: create through the FAT volume. - let disk_parent: Option = match &nodes()[parent].data { + let disk_parent: Option<(usize, u32)> = match &nodes()[parent].data { NodeData::Disk(i) => { if !i.is_dir { return Err(()); } - Some(i.cluster) + Some((i.vol, i.cluster)) } _ => None, }; - if let Some(p) = disk_parent { - if crate::fat::find(p, name).is_ok() { + if let Some((vol, p)) = disk_parent { + if crate::fat::find(vol, p, name).is_ok() { return Err(()); } - crate::fat::create(p, name, kind == FileType::Dir)?; - let e = crate::fat::find(p, name)?; + crate::fat::create(vol, p, name, kind == FileType::Dir)?; + let e = crate::fat::find(vol, p, name)?; return Ok(materialize_disk(DiskInfo { + vol, parent: p, name: e.name, is_dir: e.is_dir, @@ -458,9 +473,10 @@ pub fn remove(h: usize) -> Result<(), ()> { if i.name.is_empty() { return Err(()); // the /disk mount itself } + let vol = i.vol; let p = i.parent; let n = i.name.clone(); - return crate::fat::remove(p, &n); + return crate::fat::remove(vol, p, &n); } let parent = match nodes()[h].parent { Some(p) => p, -- cgit v1.2.3