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/fat.rs | 934 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 934 insertions(+) create mode 100644 rsnix/src/fat.rs (limited to 'rsnix/src/fat.rs') 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 @@ +// --------------------------------------------------------------------------- +// 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 { + sectors_per_cluster: u8, + reserved: u16, + fat_count: u8, + 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 VOL: Option = None; + +pub fn is_mounted() -> bool { + unsafe { VOL.is_some() } +} + +pub fn root_cluster() -> u32 { + unsafe { VOL.as_ref() }.map(|v| v.root_cluster).unwrap_or(0) +} + +// --- 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 ---------------------------------------------------------- + +fn v() -> &'static mut Volume { + unsafe { VOL.as_mut() }.unwrap() +} + +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)?; + let mut done = false; + for (i, e) in buf.chunks_exact(32).enumerate() { + match e[0] { + 0x00 => { + done = true; + 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). + 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; + } + if ch != 0xFFFF { + lfn.push(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 { + utf16_to_string(&lfn) + } 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(); + } + } + } + } + if done { + break; + } + cur = next_cluster(c).ok(); + } + Ok(items) +} + +/// Public listing: no `.` / `..`. +pub fn list_dir(dir_cluster: u32) -> Result, ()> { + 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(parent_dir_cluster: u32, name: &str) -> Result { + 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( + parent_dir_cluster: u32, + name: &str, + buf: &mut [u8], + pos: u64, +) -> Result { + if buf.is_empty() { + return Ok(0); + } + let it = find(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(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result { + 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(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { + 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)) +} + +/// 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 (sfn, needs_lfn) = to_8_3(name).ok_or(())?; + 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 { + let seq = (groups - g) as u8 | if g == groups - 1 { 0x40 } else { 0 }; + let e = &mut buf[(start + g) * 32..(start + 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: 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(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { + if find(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(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { + 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)?; + 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 ------------------------------------------------------------------- + +/// Parse the BPB, cache the FAT and validate the volume. Returns true if +/// a usable FAT32 volume is mounted. +pub fn init() -> bool { + let mut bpb = [0u8; SECTOR]; + if read_sector(0, &mut bpb).is_err() { + crate::serial_println!("fat: no disk"); + return false; + } + 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; + } + let spc = bpb[13]; + if spc == 0 || spc & (spc - 1) != 0 { + crate::serial_println!("fat: bad sectors/cluster {}", spc); + return false; + } + 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 false; + } + let first_data = reserved as u32 + fat_count as u32 * fat_size; + if first_data >= total { + crate::serial_println!("fat: bad geometry"); + return false; + } + 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 false; + } + 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 false; + } + 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", + cluster_count, + cluster_size(), + count + ); + true +} -- cgit v1.2.3