summaryrefslogtreecommitdiffstats
path: root/rsnix/src/fat.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 02:19:07 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 02:19:07 +0200
commit92ed4f6668efccf6af73f45654405d333e6a318c (patch)
treebb9b651dc36edcc90ae0289d7c4223d983454440 /rsnix/src/fat.rs
parentscripts: automate the data-disk workflow (mkdisk / sendkeys / test-disk) (diff)
downloadrs-dos-92ed4f6668efccf6af73f45654405d333e6a318c.tar.gz
rs-dos-92ed4f6668efccf6af73f45654405d333e6a318c.zip
fat: mount every FAT32 volume under /disk/<UID>/, fix multi-entry LFN
- fat.rs: VOL: Option<Volume> → VOLS: Vec<Volume>; 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)
Diffstat (limited to '')
-rw-r--r--rsnix/src/fat.rs201
1 files changed, 142 insertions, 59 deletions
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 {
34} 34}
35 35
36struct Volume { 36struct Volume {
37 disk: usize, // AHCI disk index this volume was mounted from
38 uid: String, // unique mount name: "LABEL-XXXXXXXX"
37 sectors_per_cluster: u8, 39 sectors_per_cluster: u8,
38 reserved: u16, 40 reserved: u16,
39 fat_count: u8, 41 fat_count: u8,
@@ -45,14 +47,34 @@ struct Volume {
45 fat: Vec<u8>, // FAT 1 cached in RAM 47 fat: Vec<u8>, // FAT 1 cached in RAM
46} 48}
47 49
48static mut VOL: Option<Volume> = None; 50static mut VOLS: Vec<Volume> = Vec::new();
51static mut ACTIVE_VOL: usize = 0;
49 52
50pub fn is_mounted() -> bool { 53pub fn volume_count() -> usize {
51 unsafe { VOL.is_some() } 54 unsafe { VOLS.len() }
52} 55}
53 56
54pub fn root_cluster() -> u32 { 57/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F").
55 unsafe { VOL.as_ref() }.map(|v| v.root_cluster).unwrap_or(0) 58pub fn uid(vol: usize) -> &'static str {
59 unsafe { VOLS.get(vol) }.map(|v| v.uid.as_str()).unwrap_or("")
60}
61
62pub fn root_cluster(vol: usize) -> u32 {
63 unsafe { VOLS.get(vol) }.map(|v| v.root_cluster).unwrap_or(0)
64}
65
66/// Point all FAT operations at volume `vol`; also selects the backing
67/// AHCI disk. Called by every public entry point (single-threaded
68/// kernel, so a process-wide "current volume" is fine).
69fn select_vol(vol: usize) -> Result<(), ()> {
70 let disk = unsafe { VOLS.get(vol) }.map(|v| v.disk).ok_or(())?;
71 unsafe {
72 ACTIVE_VOL = vol;
73 }
74 if !crate::ahci::select(disk) {
75 return Err(());
76 }
77 Ok(())
56} 78}
57 79
58// --- sector I/O ----------------------------------------------------------- 80// --- sector I/O -----------------------------------------------------------
@@ -71,7 +93,7 @@ fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> {
71// --- cluster math ---------------------------------------------------------- 93// --- cluster math ----------------------------------------------------------
72 94
73fn v() -> &'static mut Volume { 95fn v() -> &'static mut Volume {
74 unsafe { VOL.as_mut() }.unwrap() 96 unsafe { VOLS.get_mut(ACTIVE_VOL) }.unwrap()
75} 97}
76 98
77fn spc() -> usize { 99fn spc() -> usize {
@@ -336,7 +358,19 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
336 _ => { 358 _ => {
337 let attr = e[11]; 359 let attr = e[11];
338 if attr == 0x0F { 360 if attr == 0x0F {
339 // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, 28-31). 361 // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25,
362 // 28-31). Entries are stored last-first on disk;
363 // place each chunk by its sequence number so
364 // multi-entry names reassemble in the right order.
365 let seq = (e[0] & 0x1F) as usize;
366 if seq == 0 || seq > 20 {
367 continue;
368 }
369 let need = seq * 13;
370 if lfn.len() < need {
371 lfn.resize(need, 0);
372 }
373 let base = (seq - 1) * 13;
340 for k in 0..13 { 374 for k in 0..13 {
341 let pos = if k < 5 { 375 let pos = if k < 5 {
342 1 + k * 2 376 1 + k * 2
@@ -347,11 +381,9 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
347 }; 381 };
348 let ch = u16::from_le_bytes([e[pos], e[pos + 1]]); 382 let ch = u16::from_le_bytes([e[pos], e[pos + 1]]);
349 if ch == 0x0000 { 383 if ch == 0x0000 {
350 break; 384 break; // end of the name; rest is 0xFFFF padding
351 }
352 if ch != 0xFFFF {
353 lfn.push(ch);
354 } 385 }
386 lfn[base + k] = ch;
355 } 387 }
356 lfn_sum = e[13]; 388 lfn_sum = e[13];
357 } else { 389 } else {
@@ -359,7 +391,8 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
359 let mut sfn = [0u8; 11]; 391 let mut sfn = [0u8; 11];
360 sfn.copy_from_slice(&e[0..11]); 392 sfn.copy_from_slice(&e[0..11]);
361 let name = if !lfn.is_empty() && lfn_checksum(&sfn) == lfn_sum { 393 let name = if !lfn.is_empty() && lfn_checksum(&sfn) == lfn_sum {
362 utf16_to_string(&lfn) 394 let end = lfn.iter().position(|&c| c == 0).unwrap_or(lfn.len());
395 utf16_to_string(&lfn[..end])
363 } else { 396 } else {
364 sfn_decode(e) 397 sfn_decode(e)
365 }; 398 };
@@ -387,7 +420,8 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
387} 420}
388 421
389/// Public listing: no `.` / `..`. 422/// Public listing: no `.` / `..`.
390pub fn list_dir(dir_cluster: u32) -> Result<Vec<Entry>, ()> { 423pub fn list_dir(vol: usize, dir_cluster: u32) -> Result<Vec<Entry>, ()> {
424 select_vol(vol)?;
391 Ok(scan_dir(dir_cluster)? 425 Ok(scan_dir(dir_cluster)?
392 .into_iter() 426 .into_iter()
393 .filter(|i| i.name != "." && i.name != "..") 427 .filter(|i| i.name != "." && i.name != "..")
@@ -412,7 +446,8 @@ fn find_item(parent_dir_cluster: u32, name: &str) -> Result<DirItem, ()> {
412} 446}
413 447
414/// Find an entry by name (case-insensitive). 448/// Find an entry by name (case-insensitive).
415pub fn find(parent_dir_cluster: u32, name: &str) -> Result<Entry, ()> { 449pub fn find(vol: usize, parent_dir_cluster: u32, name: &str) -> Result<Entry, ()> {
450 select_vol(vol)?;
416 find_item(parent_dir_cluster, name).map(|i| Entry { 451 find_item(parent_dir_cluster, name).map(|i| Entry {
417 name: i.name, 452 name: i.name,
418 is_dir: i.attr & 0x10 != 0, 453 is_dir: i.attr & 0x10 != 0,
@@ -424,15 +459,17 @@ pub fn find(parent_dir_cluster: u32, name: &str) -> Result<Entry, ()> {
424// --- file data -------------------------------------------------------------- 459// --- file data --------------------------------------------------------------
425 460
426pub fn read_file( 461pub fn read_file(
462 vol: usize,
427 parent_dir_cluster: u32, 463 parent_dir_cluster: u32,
428 name: &str, 464 name: &str,
429 buf: &mut [u8], 465 buf: &mut [u8],
430 pos: u64, 466 pos: u64,
431) -> Result<usize, ()> { 467) -> Result<usize, ()> {
468 select_vol(vol)?;
432 if buf.is_empty() { 469 if buf.is_empty() {
433 return Ok(0); 470 return Ok(0);
434 } 471 }
435 let it = find(parent_dir_cluster, name)?; 472 let it = find(vol, parent_dir_cluster, name)?;
436 if it.is_dir || it.cluster == 0 { 473 if it.is_dir || it.cluster == 0 {
437 return Err(()); 474 return Err(());
438 } 475 }
@@ -467,7 +504,8 @@ pub fn read_file(
467 Ok(out) 504 Ok(out)
468} 505}
469 506
470pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result<usize, ()> { 507pub fn write_file(vol: usize, parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result<usize, ()> {
508 select_vol(vol)?;
471 if buf.is_empty() { 509 if buf.is_empty() {
472 return Ok(0); 510 return Ok(0);
473 } 511 }
@@ -529,7 +567,8 @@ pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) ->
529 Ok(buf.len()) 567 Ok(buf.len())
530} 568}
531 569
532pub fn set_len(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { 570pub fn set_len(vol: usize, parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> {
571 select_vol(vol)?;
533 let it = find_item(parent_dir_cluster, name)?; 572 let it = find_item(parent_dir_cluster, name)?;
534 if it.attr & 0x10 != 0 { 573 if it.attr & 0x10 != 0 {
535 return Err(()); 574 return Err(());
@@ -721,8 +760,11 @@ fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<
721 if run >= needed { 760 if run >= needed {
722 if groups > 0 { 761 if groups > 0 {
723 for g in 0..groups { 762 for g in 0..groups {
724 let seq = (groups - g) as u8 | if g == groups - 1 { 0x40 } else { 0 }; 763 // LFN entries are stored last-first on disk: the tail
725 let e = &mut buf[(start + g) * 32..(start + g) * 32 + 32]; 764 // chunk (highest seq, LAST flag) goes first, and the
765 // head chunk (seq 1) lands immediately before the SFN.
766 let seq = (g + 1) as u8 | if g == groups - 1 { 0x40 } else { 0 };
767 let e = &mut buf[(start + groups - 1 - g) * 32..(start + groups - 1 - g) * 32 + 32];
726 e.fill(0); 768 e.fill(0);
727 e[0] = seq; 769 e[0] = seq;
728 e[11] = 0x0F; 770 e[11] = 0x0F;
@@ -769,8 +811,9 @@ fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<
769 } 811 }
770} 812}
771 813
772pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { 814pub fn create(vol: usize, parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> {
773 if find(parent_dir_cluster, name).is_ok() { 815 select_vol(vol)?;
816 if find(vol, parent_dir_cluster, name).is_ok() {
774 return Err(()); 817 return Err(());
775 } 818 }
776 let cluster = if is_dir { 819 let cluster = if is_dir {
@@ -807,11 +850,12 @@ pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), (
807 add_dir_entry(parent_dir_cluster, name, is_dir, cluster) 850 add_dir_entry(parent_dir_cluster, name, is_dir, cluster)
808} 851}
809 852
810pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { 853pub fn remove(vol: usize, parent_dir_cluster: u32, name: &str) -> Result<(), ()> {
854 select_vol(vol)?;
811 let it = find_item(parent_dir_cluster, name)?; 855 let it = find_item(parent_dir_cluster, name)?;
812 if it.attr & 0x10 != 0 { 856 if it.attr & 0x10 != 0 {
813 // Only empty directories may be removed. 857 // Only empty directories may be removed.
814 let kids = list_dir(it.cluster)?; 858 let kids = list_dir(vol, it.cluster)?;
815 if !kids.is_empty() { 859 if !kids.is_empty() {
816 return Err(()); 860 return Err(());
817 } 861 }
@@ -847,35 +891,59 @@ pub fn init() -> bool {
847 crate::serial_println!("fat: no disk"); 891 crate::serial_println!("fat: no disk");
848 return false; 892 return false;
849 } 893 }
894 let mut mounted = false;
850 for d in 0..disks { 895 for d in 0..disks {
851 if !crate::ahci::select(d) { 896 if !crate::ahci::select(d) {
852 continue; 897 continue;
853 } 898 }
854 if try_mount() { 899 match try_mount(d) {
855 return true; 900 Some(volume) => {
901 let uid = volume.uid.clone();
902 let vol = unsafe {
903 VOLS.push(volume);
904 VOLS.len() - 1
905 };
906 // Re-sync the FSInfo free-cluster count (may be stale
907 // from earlier sessions).
908 if select_vol(vol).is_ok() {
909 fsinfo_recount();
910 }
911 let count = list_dir(vol, root_cluster(vol))
912 .map(|es| es.len())
913 .unwrap_or(0);
914 crate::serial_println!(
915 "fat: mounted \"{}\" at /disk/{} ({} clusters, {} B/cluster, root: {} entries)",
916 uid,
917 uid,
918 v().cluster_count,
919 cluster_size(),
920 count
921 );
922 mounted = true;
923 }
924 None => crate::serial_println!("fat: no FAT32 volume on disk {}", d),
856 } 925 }
857 crate::serial_println!("fat: no FAT32 volume on disk {}", d);
858 } 926 }
859 false 927 mounted
860} 928}
861 929
862/// Parse the BPB of the selected disk, cache the FAT and validate the 930/// Parse the BPB of the selected disk, cache the FAT and validate the
863/// volume. 931/// volume. Returns the ready-to-mount volume or None.
864fn try_mount() -> bool { 932fn try_mount(d: usize) -> Option<Volume> {
865 let mut bpb = [0u8; SECTOR]; 933 let mut bpb = [0u8; SECTOR];
866 if read_sector(0, &mut bpb).is_err() { 934 if read_sector(0, &mut bpb).is_err() {
867 crate::serial_println!("fat: no disk"); 935 crate::serial_println!("fat: no disk");
868 return false; 936 return None;
869 } 937 }
870 let bytes_per_sector = u16::from_le_bytes([bpb[11], bpb[12]]); 938 let bytes_per_sector = u16::from_le_bytes([bpb[11], bpb[12]]);
871 if bytes_per_sector != 512 { 939 if bytes_per_sector != 512 {
872 crate::serial_println!("fat: unsupported sector size {}", bytes_per_sector); 940 crate::serial_println!("fat: unsupported sector size {}", bytes_per_sector);
873 return false; 941 return None;
874 } 942 }
875 let spc = bpb[13]; 943 let spc = bpb[13];
876 if spc == 0 || spc & (spc - 1) != 0 { 944 if spc == 0 || spc & (spc - 1) != 0 {
877 crate::serial_println!("fat: bad sectors/cluster {}", spc); 945 crate::serial_println!("fat: bad sectors/cluster {}", spc);
878 return false; 946 return None;
879 } 947 }
880 let reserved = u16::from_le_bytes([bpb[14], bpb[15]]); 948 let reserved = u16::from_le_bytes([bpb[14], bpb[15]]);
881 let fat_count = bpb[16]; 949 let fat_count = bpb[16];
@@ -891,12 +959,12 @@ fn try_mount() -> bool {
891 let fsinfo_sec = u16::from_le_bytes([bpb[48], bpb[49]]) as u32; 959 let fsinfo_sec = u16::from_le_bytes([bpb[48], bpb[49]]) as u32;
892 if fat_size == 0 || root_cluster < 2 || reserved == 0 || fat_count == 0 { 960 if fat_size == 0 || root_cluster < 2 || reserved == 0 || fat_count == 0 {
893 crate::serial_println!("fat: not FAT32 (root={} fat={})", root_cluster, fat_size); 961 crate::serial_println!("fat: not FAT32 (root={} fat={})", root_cluster, fat_size);
894 return false; 962 return None;
895 } 963 }
896 let first_data = reserved as u32 + fat_count as u32 * fat_size; 964 let first_data = reserved as u32 + fat_count as u32 * fat_size;
897 if first_data >= total { 965 if first_data >= total {
898 crate::serial_println!("fat: bad geometry"); 966 crate::serial_println!("fat: bad geometry");
899 return false; 967 return None;
900 } 968 }
901 let cluster_count = (total - first_data) / spc as u32; 969 let cluster_count = (total - first_data) / spc as u32;
902 970
@@ -909,7 +977,7 @@ fn try_mount() -> bool {
909 let n = ((fat.len() - done) / SECTOR).min(8) as u32; 977 let n = ((fat.len() - done) / SECTOR).min(8) as u32;
910 if ahci::read_sectors(lba, n).is_err() { 978 if ahci::read_sectors(lba, n).is_err() {
911 crate::serial_println!("fat: FAT read failed at lba {}", lba); 979 crate::serial_println!("fat: FAT read failed at lba {}", lba);
912 return false; 980 return None;
913 } 981 }
914 unsafe { 982 unsafe {
915 core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR); 983 core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR);
@@ -926,30 +994,45 @@ fn try_mount() -> bool {
926 e0 & 0xFF, 994 e0 & 0xFF,
927 e1 & EOC_MASK 995 e1 & EOC_MASK
928 ); 996 );
929 return false; 997 return None;
930 } 998 }
931 unsafe { 999 // Volume identity: sanitized label + volume serial → "RSDOS-5BD1E55F".
932 VOL = Some(Volume { 1000 // Unique per format, stable across reboots, human-recognizable, and
933 sectors_per_cluster: spc, 1001 // machine-parseable (^[A-Z0-9_]+-[0-9A-F]{8}$).
934 reserved, 1002 let serial = u32::from_le_bytes([bpb[67], bpb[68], bpb[69], bpb[70]]);
935 fat_count, 1003 let mut label = String::new();
936 fat_size, 1004 for &c in &bpb[71..82] {
937 root_cluster, 1005 if c == 0 || c == b' ' {
938 first_data, 1006 continue; // strip padding/NULs
939 cluster_count, 1007 }
940 fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec), 1008 let ch = (c as char).to_ascii_uppercase();
941 fat, 1009 label.push(if ch.is_ascii_alphanumeric() { ch } else { '_' });
942 }); 1010 }
943 } 1011 if label.is_empty() {
944 // Re-sync the FSInfo free-cluster count (may be stale from earlier 1012 label = String::from("DISK");
945 // sessions that predate FSInfo maintenance). 1013 }
946 fsinfo_recount(); 1014 // Deduplicate against already-mounted volumes (e.g. cloned images).
947 let count = list_dir(root_cluster).map(|v| v.len()).unwrap_or(0); 1015 let mut uid = alloc::format!("{}-{:08X}", label, serial);
948 crate::serial_println!( 1016 let mut n = 2u32;
949 "fat: FAT32 mounted — {} clusters ({} bytes each), root: {} entries", 1017 loop {
1018 let taken = unsafe { VOLS.iter() }.any(|v| v.uid == uid);
1019 if !taken {
1020 break;
1021 }
1022 uid = alloc::format!("{}-{:08X}-{}", label, serial, n);
1023 n += 1;
1024 }
1025 Some(Volume {
1026 disk: d,
1027 uid,
1028 sectors_per_cluster: spc,
1029 reserved,
1030 fat_count,
1031 fat_size,
1032 root_cluster,
1033 first_data,
950 cluster_count, 1034 cluster_count,
951 cluster_size(), 1035 fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec),
952 count 1036 fat,
953 ); 1037 })
954 true
955} 1038}