// --------------------------------------------------------------------------- // 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 }