diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 02:19:07 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 02:19:07 +0200 |
| commit | 92ed4f6668efccf6af73f45654405d333e6a318c (patch) | |
| tree | bb9b651dc36edcc90ae0289d7c4223d983454440 /rsnix | |
| parent | scripts: automate the data-disk workflow (mkdisk / sendkeys / test-disk) (diff) | |
| download | rs-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.rs | 201 | ||||
| -rw-r--r-- | rsnix/src/fs.rs | 76 |
2 files changed, 188 insertions, 89 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 | ||
| 36 | struct Volume { | 36 | struct 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 | ||
| 48 | static mut VOL: Option<Volume> = None; | 50 | static mut VOLS: Vec<Volume> = Vec::new(); |
| 51 | static mut ACTIVE_VOL: usize = 0; | ||
| 49 | 52 | ||
| 50 | pub fn is_mounted() -> bool { | 53 | pub fn volume_count() -> usize { |
| 51 | unsafe { VOL.is_some() } | 54 | unsafe { VOLS.len() } |
| 52 | } | 55 | } |
| 53 | 56 | ||
| 54 | pub 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) | 58 | pub fn uid(vol: usize) -> &'static str { |
| 59 | unsafe { VOLS.get(vol) }.map(|v| v.uid.as_str()).unwrap_or("") | ||
| 60 | } | ||
| 61 | |||
| 62 | pub 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). | ||
| 69 | fn 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 | ||
| 73 | fn v() -> &'static mut Volume { | 95 | fn v() -> &'static mut Volume { |
| 74 | unsafe { VOL.as_mut() }.unwrap() | 96 | unsafe { VOLS.get_mut(ACTIVE_VOL) }.unwrap() |
| 75 | } | 97 | } |
| 76 | 98 | ||
| 77 | fn spc() -> usize { | 99 | fn 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 `.` / `..`. |
| 390 | pub fn list_dir(dir_cluster: u32) -> Result<Vec<Entry>, ()> { | 423 | pub 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). |
| 415 | pub fn find(parent_dir_cluster: u32, name: &str) -> Result<Entry, ()> { | 449 | pub 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 | ||
| 426 | pub fn read_file( | 461 | pub 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 | ||
| 470 | pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result<usize, ()> { | 507 | pub 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 | ||
| 532 | pub fn set_len(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> { | 570 | pub 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 | ||
| 772 | pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> { | 814 | pub 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 | ||
| 810 | pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { | 853 | pub 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. |
| 864 | fn try_mount() -> bool { | 932 | fn 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 | } |
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index 6950171..4b76e07 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs | |||
| @@ -59,6 +59,7 @@ enum NodeData { | |||
| 59 | /// op, so it stays correct as the volume changes), `cluster` the entry's | 59 | /// op, so it stays correct as the volume changes), `cluster` the entry's |
| 60 | /// own first cluster. | 60 | /// own first cluster. |
| 61 | struct DiskInfo { | 61 | struct DiskInfo { |
| 62 | vol: usize, // index into fat::VOLS (which FAT32 volume this lives on) | ||
| 62 | parent: u32, | 63 | parent: u32, |
| 63 | name: String, | 64 | name: String, |
| 64 | is_dir: bool, | 65 | is_dir: bool, |
| @@ -174,21 +175,32 @@ pub fn init() -> (usize, usize) { | |||
| 174 | ), | 175 | ), |
| 175 | ); | 176 | ); |
| 176 | 177 | ||
| 177 | // Mount the FAT32 volume at /disk when one is present. | 178 | // Mount every FAT32 volume under /disk/<UID>/ (UID = label-serial). |
| 178 | if crate::fat::is_mounted() { | 179 | let disks = crate::fat::volume_count(); |
| 179 | let root_cl = crate::fat::root_cluster(); | 180 | if disks > 0 { |
| 180 | add( | 181 | let disk_dir = add( |
| 181 | "disk", | 182 | "disk", |
| 182 | Some(root), | 183 | Some(root), |
| 183 | FileType::Dir, | 184 | FileType::Dir, |
| 184 | NodeData::Disk(DiskInfo { | 185 | NodeData::Dir(Vec::new()), |
| 185 | parent: root_cl, | ||
| 186 | name: String::new(), | ||
| 187 | is_dir: true, | ||
| 188 | size: 0, | ||
| 189 | cluster: root_cl, | ||
| 190 | }), | ||
| 191 | ); | 186 | ); |
| 187 | for vol in 0..disks { | ||
| 188 | let uid = String::from(crate::fat::uid(vol)); | ||
| 189 | let root_cl = crate::fat::root_cluster(vol); | ||
| 190 | add( | ||
| 191 | &uid, | ||
| 192 | Some(disk_dir), | ||
| 193 | FileType::Dir, | ||
| 194 | NodeData::Disk(DiskInfo { | ||
| 195 | vol, | ||
| 196 | parent: root_cl, | ||
| 197 | name: String::new(), | ||
| 198 | is_dir: true, | ||
| 199 | size: 0, | ||
| 200 | cluster: root_cl, | ||
| 201 | }), | ||
| 202 | ); | ||
| 203 | } | ||
| 192 | } | 204 | } |
| 193 | 205 | ||
| 194 | let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); | 206 | let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); |
| @@ -217,14 +229,15 @@ pub fn resolve(path: &str) -> Option<usize> { | |||
| 217 | return None; | 229 | return None; |
| 218 | } | 230 | } |
| 219 | let mut cur = 0usize; // root | 231 | let mut cur = 0usize; // root |
| 220 | // (cluster, parent, name) of directories descended into under /disk. | 232 | // (vol, cluster, parent, name) of directories descended into under /disk. |
| 221 | let mut dstack: Vec<(u32, u32, String)> = Vec::new(); | 233 | let mut dstack: Vec<(usize, u32, u32, String)> = Vec::new(); |
| 222 | for seg in path.split('/') { | 234 | for seg in path.split('/') { |
| 223 | match seg { | 235 | match seg { |
| 224 | "" | "." => {} | 236 | "" | "." => {} |
| 225 | ".." => { | 237 | ".." => { |
| 226 | if let Some((cl, p, n)) = dstack.pop() { | 238 | if let Some((vol, cl, p, n)) = dstack.pop() { |
| 227 | cur = materialize_disk(DiskInfo { | 239 | cur = materialize_disk(DiskInfo { |
| 240 | vol, | ||
| 228 | parent: p, | 241 | parent: p, |
| 229 | name: n, | 242 | name: n, |
| 230 | is_dir: true, | 243 | is_dir: true, |
| @@ -236,13 +249,14 @@ pub fn resolve(path: &str) -> Option<usize> { | |||
| 236 | } | 249 | } |
| 237 | } | 250 | } |
| 238 | name => { | 251 | name => { |
| 239 | let disk_dir: Option<u32> = match &nodes()[cur].data { | 252 | let disk_ctx: Option<(usize, u32)> = match &nodes()[cur].data { |
| 240 | NodeData::Disk(info) => Some(info.cluster), | 253 | NodeData::Disk(info) => Some((info.vol, info.cluster)), |
| 241 | _ => None, | 254 | _ => None, |
| 242 | }; | 255 | }; |
| 243 | if let Some(dir_cluster) = disk_dir { | 256 | if let Some((vol, dir_cluster)) = disk_ctx { |
| 244 | let e = crate::fat::find(dir_cluster, name).ok()?; | 257 | let e = crate::fat::find(vol, dir_cluster, name).ok()?; |
| 245 | let d = DiskInfo { | 258 | let d = DiskInfo { |
| 259 | vol, | ||
| 246 | parent: dir_cluster, | 260 | parent: dir_cluster, |
| 247 | name: e.name.clone(), | 261 | name: e.name.clone(), |
| 248 | is_dir: e.is_dir, | 262 | is_dir: e.is_dir, |
| @@ -250,7 +264,7 @@ pub fn resolve(path: &str) -> Option<usize> { | |||
| 250 | cluster: e.cluster, | 264 | cluster: e.cluster, |
| 251 | }; | 265 | }; |
| 252 | if e.is_dir { | 266 | if e.is_dir { |
| 253 | dstack.push((e.cluster, dir_cluster, e.name)); | 267 | dstack.push((vol, e.cluster, dir_cluster, e.name)); |
| 254 | } | 268 | } |
| 255 | cur = materialize_disk(d); | 269 | cur = materialize_disk(d); |
| 256 | } else { | 270 | } else { |
| @@ -279,7 +293,7 @@ pub fn size(h: usize) -> u64 { | |||
| 279 | pub fn children(h: usize) -> Vec<(String, FileType, u64)> { | 293 | pub fn children(h: usize) -> Vec<(String, FileType, u64)> { |
| 280 | if let NodeData::Disk(i) = &nodes()[h].data { | 294 | if let NodeData::Disk(i) = &nodes()[h].data { |
| 281 | let cl = i.cluster; | 295 | let cl = i.cluster; |
| 282 | return crate::fat::list_dir(cl) | 296 | return crate::fat::list_dir(i.vol, cl) |
| 283 | .map(|es| { | 297 | .map(|es| { |
| 284 | es.into_iter() | 298 | es.into_iter() |
| 285 | .map(|e| { | 299 | .map(|e| { |
| @@ -357,7 +371,7 @@ pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result<usize, ()> { | |||
| 357 | buf[..n].copy_from_slice(&bytes[p..p + n]); | 371 | buf[..n].copy_from_slice(&bytes[p..p + n]); |
| 358 | Ok(n) | 372 | Ok(n) |
| 359 | } | 373 | } |
| 360 | NodeData::Disk(i) => crate::fat::read_file(i.parent, &i.name, buf, pos), | 374 | NodeData::Disk(i) => crate::fat::read_file(i.vol, i.parent, &i.name, buf, pos), |
| 361 | NodeData::Dir(_) => Err(()), | 375 | NodeData::Dir(_) => Err(()), |
| 362 | } | 376 | } |
| 363 | } | 377 | } |
| @@ -394,7 +408,7 @@ pub fn write(h: usize, buf: &[u8], pos: u64) -> Result<usize, ()> { | |||
| 394 | } | 408 | } |
| 395 | _ => Err(()), // read-only devices | 409 | _ => Err(()), // read-only devices |
| 396 | }, | 410 | }, |
| 397 | NodeData::Disk(i) => crate::fat::write_file(i.parent, &i.name, buf, pos), | 411 | NodeData::Disk(i) => crate::fat::write_file(i.vol, i.parent, &i.name, buf, pos), |
| 398 | NodeData::Dir(_) => Err(()), | 412 | NodeData::Dir(_) => Err(()), |
| 399 | } | 413 | } |
| 400 | } | 414 | } |
| @@ -406,7 +420,7 @@ pub fn set_len(h: usize, len: u64) -> Result<(), ()> { | |||
| 406 | v.resize(len as usize, 0); | 420 | v.resize(len as usize, 0); |
| 407 | Ok(()) | 421 | Ok(()) |
| 408 | } | 422 | } |
| 409 | NodeData::Disk(i) => crate::fat::set_len(i.parent, &i.name, len), | 423 | NodeData::Disk(i) => crate::fat::set_len(i.vol, i.parent, &i.name, len), |
| 410 | _ => Err(()), | 424 | _ => Err(()), |
| 411 | } | 425 | } |
| 412 | } | 426 | } |
| @@ -417,22 +431,23 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> { | |||
| 417 | return Err(()); | 431 | return Err(()); |
| 418 | } | 432 | } |
| 419 | // Disk directory: create through the FAT volume. | 433 | // Disk directory: create through the FAT volume. |
| 420 | let disk_parent: Option<u32> = match &nodes()[parent].data { | 434 | let disk_parent: Option<(usize, u32)> = match &nodes()[parent].data { |
| 421 | NodeData::Disk(i) => { | 435 | NodeData::Disk(i) => { |
| 422 | if !i.is_dir { | 436 | if !i.is_dir { |
| 423 | return Err(()); | 437 | return Err(()); |
| 424 | } | 438 | } |
| 425 | Some(i.cluster) | 439 | Some((i.vol, i.cluster)) |
| 426 | } | 440 | } |
| 427 | _ => None, | 441 | _ => None, |
| 428 | }; | 442 | }; |
| 429 | if let Some(p) = disk_parent { | 443 | if let Some((vol, p)) = disk_parent { |
| 430 | if crate::fat::find(p, name).is_ok() { | 444 | if crate::fat::find(vol, p, name).is_ok() { |
| 431 | return Err(()); | 445 | return Err(()); |
| 432 | } | 446 | } |
| 433 | crate::fat::create(p, name, kind == FileType::Dir)?; | 447 | crate::fat::create(vol, p, name, kind == FileType::Dir)?; |
| 434 | let e = crate::fat::find(p, name)?; | 448 | let e = crate::fat::find(vol, p, name)?; |
| 435 | return Ok(materialize_disk(DiskInfo { | 449 | return Ok(materialize_disk(DiskInfo { |
| 450 | vol, | ||
| 436 | parent: p, | 451 | parent: p, |
| 437 | name: e.name, | 452 | name: e.name, |
| 438 | is_dir: e.is_dir, | 453 | is_dir: e.is_dir, |
| @@ -458,9 +473,10 @@ pub fn remove(h: usize) -> Result<(), ()> { | |||
| 458 | if i.name.is_empty() { | 473 | if i.name.is_empty() { |
| 459 | return Err(()); // the /disk mount itself | 474 | return Err(()); // the /disk mount itself |
| 460 | } | 475 | } |
| 476 | let vol = i.vol; | ||
| 461 | let p = i.parent; | 477 | let p = i.parent; |
| 462 | let n = i.name.clone(); | 478 | let n = i.name.clone(); |
| 463 | return crate::fat::remove(p, &n); | 479 | return crate::fat::remove(vol, p, &n); |
| 464 | } | 480 | } |
| 465 | let parent = match nodes()[h].parent { | 481 | let parent = match nodes()[h].parent { |
| 466 | Some(p) => p, | 482 | Some(p) => p, |
