// --------------------------------------------------------------------------- // FAT32 — read/write a FAT32 volume over the AHCI sector interface. // // Mounts the volume's boot sector (BPB), caches the whole FAT in RAM, // follows cluster chains, and exposes file/directory operations used by // the VFS's Disk nodes: list, find, read, write, truncate, create (files // and dirs), remove. Long filenames are read back and written for names // that don't fit 8.3; short names are generated the standard way // (uppercased, 8+3, "~N" truncation). All timestamps are zero — this is // a toy kernel, not a backup tool. // // Single-threaded like the rest of the kernel; no caching beyond the FAT // itself, so every op re-reads directory sectors (cheap: the disk is // polled DMA at µs scale in QEMU). // --------------------------------------------------------------------------- extern crate alloc; use crate::ahci; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; const SECTOR: usize = 512; const EOC_MASK: u32 = 0x0FFF_FFFF; const EOC: u32 = 0x0FFF_FFFF; /// One directory-listed entry (or a resolved file). pub struct Entry { pub name: String, pub is_dir: bool, pub size: u64, pub cluster: u32, } struct Volume { disk: usize, // AHCI disk index this volume was mounted from uid: String, // unique mount name: "LABEL-XXXXXXXX" mounted: bool, // still backed by a present disk (false = tombstoned) sectors_per_cluster: u8, reserved: u16, fat_size: u32, // sectors per FAT root_cluster: u32, first_data: u32, // LBA of cluster 2 cluster_count: u32, fsinfo: Option, // sector number of the FSInfo structure fat: Vec, // FAT 1 cached in RAM } static mut VOLS: Vec = Vec::new(); static mut ACTIVE_VOL: usize = 0; /// Raw-pointer accessors for the volume statics. The kernel is /// single-threaded, so `&'static` refs are sound; the `addr_of_mut!` /// round-trip keeps `static_mut_refs` (warn today, deny in edition /// 2024) quiet. fn vols() -> &'static mut Vec { unsafe { &mut *core::ptr::addr_of_mut!(VOLS) } } fn active_vol() -> &'static mut usize { unsafe { &mut *core::ptr::addr_of_mut!(ACTIVE_VOL) } } pub fn volume_count() -> usize { vols().len() } /// Is volume `vol` currently mounted (its disk present and FAT cached)? pub fn is_mounted(vol: usize) -> bool { vols().get(vol).map(|v| v.mounted).unwrap_or(false) } /// Re-scan after a disk arrival/removal: unmount volumes whose disk left, /// mount every disk that has no mounted volume (which also re-tries /// mounts that failed at boot). Volume slots are tombstoned, never /// removed, so VFS disk handles keep their indices. Returns /// (mounted, unmounted). pub fn rescan() -> (usize, usize) { let disks = crate::ahci::disk_count(); let mut mounted = 0usize; let mut unmounted = 0usize; // Unmount volumes whose disk is gone. for i in 0..vols().len() { if vols()[i].mounted && !crate::ahci::is_up(vols()[i].disk) { vols()[i].mounted = false; crate::serial_println!("fat: unmounted \"{}\"", vols()[i].uid); unmounted += 1; } } // Mount every disk that has no mounted volume. for d in 0..disks { if !crate::ahci::select(d) { continue; } if vols().iter().any(|v| v.mounted && v.disk == d) { continue; } if let Some(volume) = try_mount(d) { let uid = volume.uid.clone(); let vol = match vols().iter().position(|v| !v.mounted && v.disk == d) { // Reuse the disk's own tombstoned slot (indices stay stable). Some(slot) => { vols()[slot] = volume; slot } None => { vols().push(volume); vols().len() - 1 } }; 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 += 1; } } (mounted, unmounted) } /// Unique mount name of volume `vol` ("RSDOS-5BD1E55F"). pub fn uid(vol: usize) -> &'static str { vols().get(vol).map(|v| v.uid.as_str()).unwrap_or("") } pub fn root_cluster(vol: usize) -> u32 { 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 = vols().get(vol).map(|v| v.disk).ok_or(())?; unsafe { ACTIVE_VOL = vol; } if !crate::ahci::select(disk) { return Err(()); } Ok(()) } // --- sector I/O ----------------------------------------------------------- fn read_sector(lba: u64, out: &mut [u8; SECTOR]) -> Result<(), ()> { ahci::read_sectors(lba, 1)?; unsafe { core::ptr::copy(ahci::buf(), out.as_mut_ptr(), SECTOR) }; Ok(()) } fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> { unsafe { core::ptr::copy(data.as_ptr(), ahci::buf(), SECTOR) }; ahci::write_sectors(lba, 1) } // --- cluster math ---------------------------------------------------------- /// Current volume's metadata — only ever reached with a selected volume /// (the fs layer calls `select_vol` first), so a missing slot is a kernel /// invariant violation, not a recoverable error. fn v() -> &'static mut Volume { let i = *active_vol(); match vols().get_mut(i) { Some(v) => v, None => { crate::serial_println!("fat: internal error — no volume at slot {}", i); crate::interrupts::halt(); } } } fn spc() -> usize { v().sectors_per_cluster as usize } fn cluster_size() -> usize { spc() * SECTOR } fn cluster_lba(c: u32) -> u64 { v().first_data as u64 + ((c as u64 - 2) * spc() as u64) } fn fat_off(c: u32) -> usize { c as usize * 4 } fn read_fat_entry(c: u32) -> u32 { let off = fat_off(c); u32::from_le_bytes([ v().fat[off], v().fat[off + 1], v().fat[off + 2], v().fat[off + 3], ]) & EOC_MASK } /// Update a FAT entry in the cache and write the affected sector back to /// both FAT copies. fn write_fat_entry(c: u32, val: u32) -> Result<(), ()> { let off = fat_off(c); v().fat[off..off + 4].copy_from_slice(&(val & EOC_MASK).to_le_bytes()); let sector_idx = off / SECTOR; let mut buf = [0u8; SECTOR]; buf.copy_from_slice(&v().fat[sector_idx * SECTOR..(sector_idx + 1) * SECTOR]); let lba1 = v().reserved as u64 + sector_idx as u64; let lba2 = lba1 + v().fat_size as u64; write_sector(lba1, &buf)?; write_sector(lba2, &buf) } fn is_eoc(c: u32) -> bool { c >= 0x0FFF_FFF8 } fn next_cluster(c: u32) -> Result { let n = read_fat_entry(c); if is_eoc(n) { Err(()) } else { Ok(n) } } /// Allocate a fresh cluster: mark it EOC in the FAT and zero its data. fn alloc_cluster() -> Result { let total = v().cluster_count + 2; for c in 2..total { if read_fat_entry(c) == 0 { write_fat_entry(c, EOC)?; let zero = vec![0u8; cluster_size()]; write_cluster(c, &zero)?; fsinfo_adjust(-1); return Ok(c); } } crate::serial_println!("fat: disk full"); Err(()) } fn free_chain(mut c: u32) { let mut freed = 0u64; let mut guard = 0; while !is_eoc(read_fat_entry(c)) && guard < 1_000_000 { let nxt = read_fat_entry(c); let _ = write_fat_entry(c, 0); c = nxt; freed += 1; guard += 1; } let _ = write_fat_entry(c, 0); freed += 1; if freed > 0 { fsinfo_adjust(freed as i64); } } /// Keep FSInfo.FSFreeCount in sync with cluster allocations/frees so /// fsck and other tools see an accurate free count. Falls back to a full /// recount when the stored value is the "unknown" marker. fn fsinfo_adjust(delta: i64) { let Some(s) = v().fsinfo else { return; }; let mut b = [0u8; SECTOR]; if read_sector(s as u64, &mut b).is_err() { return; } if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" { return; // not a valid FSInfo structure } let mut n = u32::from_le_bytes([b[488], b[489], b[490], b[491]]); if n == 0xFFFF_FFFF { n = free_clusters(); } let n = (n as i64 + delta).max(0) as u32; b[488..492].copy_from_slice(&n.to_le_bytes()); let _ = write_sector(s as u64, &b); } /// Rewrite FSInfo.FSFreeCount with the true free-cluster count. fn fsinfo_recount() { let Some(s) = v().fsinfo else { return; }; let mut b = [0u8; SECTOR]; if read_sector(s as u64, &mut b).is_err() { return; } if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" { return; } let n = free_clusters(); b[488..492].copy_from_slice(&n.to_le_bytes()); let _ = write_sector(s as u64, &b); } /// Count free clusters by scanning the cached FAT. fn free_clusters() -> u32 { let mut n = 0u32; for c in 2..v().cluster_count + 2 { if read_fat_entry(c) == 0 { n += 1; } } n } fn read_cluster(c: u32, buf: &mut [u8]) -> Result<(), ()> { if spc() <= 8 { ahci::read_sectors(cluster_lba(c), spc() as u32)?; unsafe { core::ptr::copy(ahci::buf(), buf.as_mut_ptr(), cluster_size()) }; } else { for s in 0..spc() { ahci::read_sectors(cluster_lba(c) + s as u64, 1)?; unsafe { core::ptr::copy(ahci::buf(), buf.as_mut_ptr().add(s * SECTOR), SECTOR); } } } Ok(()) } fn write_cluster(c: u32, buf: &[u8]) -> Result<(), ()> { if spc() <= 8 { unsafe { core::ptr::copy(buf.as_ptr(), ahci::buf(), cluster_size()) }; ahci::write_sectors(cluster_lba(c), spc() as u32) } else { for s in 0..spc() { unsafe { core::ptr::copy(buf.as_ptr().add(s * SECTOR), ahci::buf(), SECTOR); } ahci::write_sectors(cluster_lba(c) + s as u64, 1)?; } Ok(()) } } // --- directory scanning ---------------------------------------------------- /// Checksum of an SFN's 11 bytes; used to bind LFN entries to their SFN. fn lfn_checksum(sfn: &[u8; 11]) -> u8 { let mut s: u8 = 0; for &b in sfn { s = s.rotate_right(1).wrapping_add(b); } s } fn sfn_decode(e: &[u8]) -> String { let nts = e[12]; let mut s = String::new(); for &b in &e[0..8] { if b == b' ' || b == 0 { break; } let c = if nts & 0x08 != 0 { b.to_ascii_lowercase() } else { b }; s.push(c as char); } let mut ext = String::new(); for &b in &e[8..11] { if b == b' ' || b == 0 { break; } let c = if nts & 0x10 != 0 { b.to_ascii_lowercase() } else { b }; ext.push(c as char); } if !ext.is_empty() { s.push('.'); s.push_str(&ext); } if s.is_empty() { s.push('?'); } s } fn utf16_to_string(v: &[u16]) -> String { let mut s = String::new(); for &c in v { if c == 0 { break; } if let Some(ch) = char::from_u32(c as u32) { s.push(ch); } } s } struct DirItem { name: String, attr: u8, cluster: u32, size: u32, sector: u64, // LBA of the entry's sector (for in-place updates) offset: usize, } /// Scan a directory's cluster chain, reassembling long filenames. fn scan_dir(dir_cluster: u32) -> Result, ()> { let cs = cluster_size(); let mut buf = vec![0u8; cs]; let mut items = Vec::new(); let mut lfn: Vec = Vec::new(); let mut lfn_sum = 0u8; let mut cur: Option = Some(dir_cluster); let mut guard = 0; while let Some(c) = cur { guard += 1; if guard > 1_000_000 { return Err(()); // loop protection on a corrupt chain } read_cluster(c, &mut buf)?; for (i, e) in buf.chunks_exact(32).enumerate() { match e[0] { // 0x00 ends the entries in THIS cluster, not the whole // directory: a dir that spills into a second cluster keeps // its 0x00 marker in the first cluster, so we must keep // walking the chain to see the rest. 0x00 => break, 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN _ => { let attr = e[11]; if attr == 0x0F { // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, // 28-31). 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 } else if k < 11 { 14 + (k - 5) * 2 } else { 28 + (k - 11) * 2 }; let ch = u16::from_le_bytes([e[pos], e[pos + 1]]); if ch == 0x0000 { break; // end of the name; rest is 0xFFFF padding } lfn[base + k] = ch; } lfn_sum = e[13]; } else { // Short-name entry: bind any collected LFN. let mut sfn = [0u8; 11]; sfn.copy_from_slice(&e[0..11]); let name = if !lfn.is_empty() && lfn_checksum(&sfn) == lfn_sum { let end = lfn.iter().position(|&c| c == 0).unwrap_or(lfn.len()); utf16_to_string(&lfn[..end]) } else { sfn_decode(e) }; let cluster = u16::from_le_bytes([e[26], e[27]]) as u32 | ((u16::from_le_bytes([e[20], e[21]]) as u32) << 16); items.push(DirItem { name, attr, cluster, size: u32::from_le_bytes([e[28], e[29], e[30], e[31]]), sector: cluster_lba(c) + (i * 32 / SECTOR) as u64, offset: (i * 32) % SECTOR, }); lfn.clear(); } } } } cur = next_cluster(c).ok(); } Ok(items) } /// Public listing: no `.` / `..`. 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 != "..") .map(|i| Entry { name: i.name, is_dir: i.attr & 0x10 != 0, size: i.size as u64, cluster: i.cluster, }) .collect()) } /// Find an entry by name (case-insensitive), returning the scan record /// (which knows the entry's sector for in-place updates). fn find_item(parent_dir_cluster: u32, name: &str) -> Result { for i in scan_dir(parent_dir_cluster)? { if i.name.eq_ignore_ascii_case(name) { return Ok(i); } } Err(()) } /// Find an entry by name (case-insensitive). pub fn find(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, size: i.size as u64, cluster: i.cluster, }) } // --- 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(vol, parent_dir_cluster, name)?; if it.is_dir || it.cluster == 0 { return Err(()); } if pos >= it.size { return Ok(0); } let cs = cluster_size() as u64; let mut cluster = it.cluster; let mut skip = pos; while skip >= cs { cluster = next_cluster(cluster)?; skip -= cs; } let mut cbuf = vec![0u8; cluster_size()]; let mut remaining = buf.len() as u64; if remaining > it.size - pos { remaining = it.size - pos; } let mut out = 0usize; while remaining > 0 { read_cluster(cluster, &mut cbuf)?; let from = skip as usize; let n = ((cs as usize) - from).min(remaining as usize); buf[out..out + n].copy_from_slice(&cbuf[from..from + n]); out += n; remaining -= n as u64; if remaining > 0 { cluster = next_cluster(cluster)?; } skip = 0; } Ok(out) } pub fn write_file( vol: usize, parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64, ) -> Result { select_vol(vol)?; if buf.is_empty() { return Ok(0); } let it = find_item(parent_dir_cluster, name)?; if it.attr & 0x10 != 0 { return Err(()); } let cs = cluster_size() as u64; let end = pos + buf.len() as u64; // Ensure a first cluster exists. let mut cluster = it.cluster; if cluster == 0 { cluster = alloc_cluster()?; update_entry(&it, Some(cluster), None)?; } // Walk to the cluster containing `pos`, extending the chain as needed. let mut skip = pos; let mut cur = cluster; while skip >= cs { match next_cluster(cur) { Ok(n) => cur = n, Err(_) => { let n = alloc_cluster()?; write_fat_entry(cur, n)?; cur = n; } } skip -= cs; } // Write the data. let mut cbuf = vec![0u8; cluster_size()]; let mut remaining = buf.len() as u64; let mut in_off = 0usize; while remaining > 0 { read_cluster(cur, &mut cbuf)?; let from = skip as usize; let n = ((cs as usize) - from).min(remaining as usize); cbuf[from..from + n].copy_from_slice(&buf[in_off..in_off + n]); write_cluster(cur, &cbuf)?; in_off += n; remaining -= n as u64; if remaining > 0 { // Make sure the chain continues past this cluster. match next_cluster(cur) { Ok(n) => cur = n, Err(_) => { let n = alloc_cluster()?; write_fat_entry(cur, n)?; cur = n; } } } skip = 0; } if end > it.size as u64 { update_entry(&it, None, Some(end as u32))?; } Ok(buf.len()) } pub fn set_len(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(()); } let cs = cluster_size() as u64; let needed = if len == 0 { 0 } else { ((len - 1) / cs + 1) as usize }; let have = chain_len(it.cluster); if it.cluster == 0 { if needed == 0 { update_entry(&it, None, Some(0))?; } else { let first = alloc_cluster()?; if needed > 1 { extend_chain(first, needed - 1)?; } update_entry(&it, Some(first), Some(len as u32))?; } } else if needed < have { truncate_chain(it.cluster, needed); update_entry( &it, if needed == 0 { Some(0) } else { None }, Some(len as u32), )?; } else if needed > have { extend_chain(it.cluster, needed - have)?; update_entry(&it, None, Some(len as u32))?; } else { update_entry(&it, None, Some(len as u32))?; } Ok(()) } fn chain_len(mut c: u32) -> usize { let mut n = 0usize; let mut guard = 0; while c != 0 && !is_eoc(read_fat_entry(c)) && guard < 1_000_000 { c = read_fat_entry(c); n += 1; guard += 1; } if c != 0 { n += 1; } n } fn truncate_chain(mut c: u32, keep: usize) { if keep == 0 { free_chain(c); return; } let mut idx = 0usize; let mut guard = 0; while idx + 1 < keep { let n = read_fat_entry(c); if is_eoc(n) { return; } c = n; idx += 1; guard += 1; if guard > 1_000_000 { return; } } let nxt = read_fat_entry(c); if !is_eoc(nxt) { free_chain(nxt); let _ = write_fat_entry(c, EOC); } } fn extend_chain(c: u32, add: usize) -> Result<(), ()> { let mut last = c; let mut guard = 0; loop { let n = read_fat_entry(last); if is_eoc(n) { break; } last = n; guard += 1; if guard > 1_000_000 { return Err(()); } } for _ in 0..add { let n = alloc_cluster()?; write_fat_entry(last, n)?; last = n; } Ok(()) } /// Rewrite one field of a directory entry in place (its sector was cached /// at scan time; the parent dir chain never moves while we hold it). fn update_entry(it: &DirItem, cluster: Option, size: Option) -> Result<(), ()> { let mut sec = [0u8; SECTOR]; read_sector(it.sector, &mut sec)?; let e = &mut sec[it.offset..it.offset + 32]; if let Some(c) = cluster { e[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes())); e[26..28].copy_from_slice(&((c as u16).to_le_bytes())); } if let Some(s) = size { e[28..32].copy_from_slice(&s.to_le_bytes()); } write_sector(it.sector, &sec) } // --- creation / removal ------------------------------------------------------ /// Uppercased 8.3 short name for `name`; `needs_lfn` if the original name /// can't round-trip through the short name (too long or lowercase). fn to_8_3(name: &str) -> Option<([u8; 11], bool)> { if name.is_empty() || name.contains('/') || name == "." || name == ".." { return None; } let upper = name.to_ascii_uppercase(); let (n, e) = match upper.rfind('.') { Some(i) => (&upper[..i], Some(&upper[i + 1..])), None => (upper.as_str(), None), }; if n.is_empty() { return None; } let mut sfn = [b' '; 11]; let nb = n.as_bytes(); for i in 0..8 { if i < nb.len() { sfn[i] = nb[i]; } } if let Some(ext) = e { let eb = ext.as_bytes(); for i in 0..3 { if i < eb.len() { sfn[8 + i] = eb[i]; } } } let needs = nb.len() > 8 || e.map(|x| x.len() > 3).unwrap_or(false) || name != upper; Some((sfn, needs)) } /// Collect the raw short names already present in a directory. fn dir_short_names(parent: u32) -> Result, ()> { let cs = cluster_size(); let mut buf = vec![0u8; cs]; let mut out = Vec::new(); let mut cur: Option = Some(parent); let mut guard = 0; while let Some(c) = cur { guard += 1; if guard > 1_000_000 { return Err(()); // loop protection on a corrupt chain } read_cluster(c, &mut buf)?; for e in buf.chunks_exact(32) { match e[0] { // Same rule as scan_dir: 0x00 ends this cluster's entries // but the directory may continue in the next cluster. 0x00 => break, 0xE5 => {} _ => { if e[11] != 0x0F { let mut sfn = [0u8; 11]; sfn.copy_from_slice(&e[0..11]); out.push(sfn); } } } } cur = next_cluster(c).ok(); } Ok(out) } /// Make `base` unique within a directory: on collision, append a numeric /// ~N tail (DISK-TE~1, DISK-TE~2, …) — the standard FAT short-name /// disambiguation that fsck and other OSes expect. Without it, two files /// whose long names share an 8.3 prefix would get the same short name and /// fsck reports them as duplicate directory entries. fn unique_short_name(base: &[u8; 11], used: &[[u8; 11]]) -> [u8; 11] { if !used.contains(base) { return *base; } let mut name_len = 0; while name_len < 8 && base[name_len] != b' ' { name_len += 1; } for n in 1..=999u32 { let tail = alloc::format!("~{}", n); let tb = tail.as_bytes(); let keep = name_len.saturating_sub(tb.len()); let mut cand = [b' '; 11]; cand[..keep].copy_from_slice(&base[..keep]); cand[keep..keep + tb.len()].copy_from_slice(tb); cand[8..11].copy_from_slice(&base[8..11]); if !used.contains(&cand) { return cand; } } *base // pathological; keep the colliding name } /// Write the LFN entries (if any) + SFN entry for `name` into the parent /// directory, extending its chain with a fresh zeroed cluster if needed. fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> { let (base_sfn, mut needs_lfn) = to_8_3(name).ok_or(())?; // Short names must be unique within the directory; a plain truncation // collides when two long names share an 8.3 prefix. let sfn = unique_short_name(&base_sfn, &dir_short_names(parent)?); if sfn != base_sfn { needs_lfn = true; // keep the original name via an LFN entry } let lfn_chars: Vec = name.encode_utf16().collect(); let groups = if needs_lfn { lfn_chars.len().div_ceil(13) } else { 0 }; let needed = groups + 1; let cs = cluster_size(); let mut buf = vec![0u8; cs]; let mut cur: Option = Some(parent); let mut guard = 0; loop { guard += 1; if guard > 1_000_000 { return Err(()); } let c = cur.ok_or(())?; read_cluster(c, &mut buf)?; let slots = cs / 32; let mut run = 0usize; let mut start = 0usize; for i in 0..slots { if buf[i * 32] == 0x00 || buf[i * 32] == 0xE5 { run += 1; if run == 1 { start = i; } if run >= needed { break; } } else { run = 0; } } if run >= needed { if groups > 0 { for g in 0..groups { // 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; e[13] = lfn_checksum(&sfn); for k in 0..13 { let ci = g * 13 + k; let ch: u16 = if ci < lfn_chars.len() { lfn_chars[ci] } else if ci == lfn_chars.len() { 0 } else { 0xFFFF }; let pos = if k < 5 { 1 + k * 2 } else if k < 11 { 14 + (k - 5) * 2 } else { 28 + (k - 11) * 2 }; e[pos] = (ch & 0xFF) as u8; e[pos + 1] = (ch >> 8) as u8; } } } let e = &mut buf[(start + groups) * 32..(start + groups) * 32 + 32]; e.fill(0); e[0..11].copy_from_slice(&sfn); e[11] = if is_dir { 0x10 } else { 0x20 }; e[20..22].copy_from_slice(&(((cluster >> 16) as u16).to_le_bytes())); e[26..28].copy_from_slice(&((cluster as u16).to_le_bytes())); write_cluster(c, &buf)?; return Ok(()); } // Chain full: pack this cluster before extending. A mid-chain // 0x00 would look like the end of the directory to other FAT // readers (Windows, mtools stop at the first 0x00), hiding every // entry written into the new cluster. Marking the free slots as // 0xE5 (deleted) keeps them skippable while the chain walk finds // the continuation. for i in 0..slots { if buf[i * 32] == 0x00 { buf[i * 32] = 0xE5; } } write_cluster(c, &buf)?; // Chain full: extend with a fresh zeroed cluster and retry. match next_cluster(c) { Ok(n) => cur = Some(n), Err(_) => { let n = alloc_cluster()?; write_fat_entry(c, n)?; cur = Some(n); } } } } pub fn create(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 { let c = alloc_cluster()?; // "." and ".." entries. let cs = cluster_size(); let mut buf = vec![0u8; cs]; let dot = &mut buf[0..32]; dot.fill(0); dot[0] = b'.'; dot[1..11].fill(b' '); // SFN is space-padded dot[11] = 0x10; dot[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes())); dot[26..28].copy_from_slice(&((c as u16).to_le_bytes())); let dotdot = &mut buf[32..64]; dotdot.fill(0); dotdot[0] = b'.'; dotdot[1] = b'.'; dotdot[2..11].fill(b' '); // SFN is space-padded dotdot[11] = 0x10; // ".." points at the parent; the root has cluster number 0. let parent_cluster = if parent_dir_cluster == v().root_cluster { 0 } else { parent_dir_cluster }; dotdot[20..22].copy_from_slice(&(((parent_cluster >> 16) as u16).to_le_bytes())); dotdot[26..28].copy_from_slice(&((parent_cluster as u16).to_le_bytes())); write_cluster(c, &buf)?; c } else { 0 }; add_dir_entry(parent_dir_cluster, name, is_dir, cluster) } pub fn remove(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(vol, it.cluster)?; if !kids.is_empty() { return Err(()); } free_chain(it.cluster); } else if it.cluster != 0 { free_chain(it.cluster); } // Mark the SFN and its leading LFN entries deleted. let mut sec = [0u8; SECTOR]; read_sector(it.sector, &mut sec)?; let mut off = it.offset; sec[off] = 0xE5; while off >= 32 { let prev = &sec[off - 32..off]; if prev[11] == 0x0F { sec[off - 32] = 0xE5; off -= 32; } else { break; } } write_sector(it.sector, &sec) } // --- mount ------------------------------------------------------------------- /// Try to mount a FAT32 volume on every attached disk, in port order, and /// keep the first one that validates. Returns true if a usable FAT32 /// volume is mounted. pub fn init() -> bool { let disks = crate::ahci::disk_count(); if disks == 0 { crate::serial_println!("fat: no disk"); return false; } let mut mounted = false; for d in 0..disks { if !crate::ahci::select(d) { continue; } match try_mount(d) { Some(volume) => { let uid = volume.uid.clone(); vols().push(volume); let vol = 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), } } mounted } /// Parse the BPB of the selected disk, cache the FAT and validate the /// 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 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 None; } let spc = bpb[13]; if spc == 0 || spc & (spc - 1) != 0 { crate::serial_println!("fat: bad sectors/cluster {}", spc); return None; } let reserved = u16::from_le_bytes([bpb[14], bpb[15]]); let fat_count = bpb[16]; let total16 = u16::from_le_bytes([bpb[19], bpb[20]]); let total32 = u32::from_le_bytes([bpb[32], bpb[33], bpb[34], bpb[35]]); let total = if total16 != 0 { total16 as u32 } else { total32 }; let fat_size = u32::from_le_bytes([bpb[36], bpb[37], bpb[38], bpb[39]]); let root_cluster = u32::from_le_bytes([bpb[44], bpb[45], bpb[46], bpb[47]]); let fsinfo_sec = u16::from_le_bytes([bpb[48], bpb[49]]) as u32; if fat_size == 0 || root_cluster < 2 || reserved == 0 || fat_count == 0 { crate::serial_println!("fat: not FAT32 (root={} fat={})", root_cluster, fat_size); return None; } let first_data = reserved as u32 + fat_count as u32 * fat_size; if first_data >= total { crate::serial_println!("fat: bad geometry"); return None; } let cluster_count = (total - first_data) / spc as u32; // Load the whole FAT into RAM (chunked through the 4 KiB DMA buffer). let fat_bytes = fat_size as usize * SECTOR; let mut fat = alloc::vec![0u8; fat_bytes]; let mut lba = reserved as u64; let mut done = 0usize; while done < fat.len() { let n = ((fat.len() - done) / SECTOR).min(8) as u32; if ahci::read_sectors(lba, n).is_err() { crate::serial_println!("fat: FAT read failed at lba {}", lba); return None; } unsafe { core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR); } lba += n as u64; done += n as usize * SECTOR; } // Sanity: FAT[0] holds the media byte, FAT[1] should be an EOC. let e0 = u32::from_le_bytes([fat[0], fat[1], fat[2], fat[3]]); let e1 = u32::from_le_bytes([fat[4], fat[5], fat[6], fat[7]]); if e0 & 0xFF != 0xF8 || e1 & EOC_MASK != EOC { crate::serial_println!( "fat: bad FAT markers ({:#x} {:#x})", e0 & 0xFF, e1 & EOC_MASK ); return None; } // 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). // Tombstoned slots are skipped so a re-plugged disk keeps its name. let mut uid = alloc::format!("{}-{:08X}", label, serial); let mut n = 2u32; loop { let taken = vols().iter().any(|v| v.mounted && v.uid == uid); if !taken { break; } uid = alloc::format!("{}-{:08X}-{}", label, serial, n); n += 1; } Some(Volume { disk: d, uid, mounted: true, sectors_per_cluster: spc, reserved, fat_size, root_cluster, first_data, cluster_count, fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec), fat, }) }