summaryrefslogtreecommitdiffstats
path: root/rsnix/src/fat.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 03:44:57 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 03:44:57 +0200
commit63d01e9d8eb6f3ccb6a96b7c577834276cf51add (patch)
treef490b9969a8f781f48c2f168dc3e6c71c41339a9 /rsnix/src/fat.rs
parentrustfmt: reflow fat.rs/fs.rs formatting (diff)
downloadrs-dos-63d01e9d8eb6f3ccb6a96b7c577834276cf51add.tar.gz
rs-dos-63d01e9d8eb6f3ccb6a96b7c577834276cf51add.zip
Clean up warnings, fix FAT32 multi-cluster dirs, prep for edition 2024
Warning cleanup (cargo check + clippy now 0 across the workspace): - Migrate all 21 static_mut_refs sites to &raw mut accessors (fat, gop, ps2, vt, ahci, interrupts, memory) so edition 2024 won't deny them - panic = "abort" profiles: bare cargo check/build now works without nightly/-Zbuild-std - Wire TICKS in the timer ISR (it was never incremented, so the vt scroll batching trigger was dead code) - Clippy: efi_main becomes unsafe extern "efiapi" with a # Safety doc, KeyBuf::new uses inline-const array repeat (no interior-mutable const), editor main loop is a while-let, PIT control byte regrouped - [workspace.lints.clippy] allows result_unit_err + needless_range_loop (deliberate kernel style); large_enum_variant allowed on LineInput - Drop now-stale static_mut_refs allow from the rsdos bin FAT32 correctness fixes (both pre-existing, exposed by the SFN fix): - Unique 8.3 short names: to_8_3() never disambiguated collisions, so files sharing a long-name prefix got identical SFNs and fsck reported duplicate directory entries. Add dir_short_names() + unique_short_name() (~N tails, Windows-style) and force LFN when the SFN changes. - Multi-cluster directories: scan_dir/dir_short_names stopped at the first 0x00 slot, hiding every entry written into a chained cluster (writes failed with 'cannot write', duplicate SFNs accumulated); add_dir_entry now packs the cluster (0xE5 deleted-markers) before extending so other FAT readers (mtools, Windows) see the continuation too. Other: delete dead fields (Disk.fis, Volume.fat_count), drop timer_irq/ keyboard_irq/rendered, refresh_and_exit is a documented unsafe fn, CONTEXT .md corrected (stable toolchain, IRQ input, 4-crate layout), .gitignore excludes .pi-subagents artifacts. Validated: cargo check/clippy/fmt clean, make test-disk PASS on fresh and reused images (incl. two directory-chain extensions and ~10 SFN tails), editor round-trip smoke test PASS.
Diffstat (limited to '')
-rw-r--r--rsnix/src/fat.rs130
1 files changed, 105 insertions, 25 deletions
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs
index 52aa641..13d19df 100644
--- a/rsnix/src/fat.rs
+++ b/rsnix/src/fat.rs
@@ -38,7 +38,6 @@ struct Volume {
38 uid: String, // unique mount name: "LABEL-XXXXXXXX" 38 uid: String, // unique mount name: "LABEL-XXXXXXXX"
39 sectors_per_cluster: u8, 39 sectors_per_cluster: u8,
40 reserved: u16, 40 reserved: u16,
41 fat_count: u8,
42 fat_size: u32, // sectors per FAT 41 fat_size: u32, // sectors per FAT
43 root_cluster: u32, 42 root_cluster: u32,
44 first_data: u32, // LBA of cluster 2 43 first_data: u32, // LBA of cluster 2
@@ -50,28 +49,36 @@ struct Volume {
50static mut VOLS: Vec<Volume> = Vec::new(); 49static mut VOLS: Vec<Volume> = Vec::new();
51static mut ACTIVE_VOL: usize = 0; 50static mut ACTIVE_VOL: usize = 0;
52 51
52/// Raw-pointer accessors for the volume statics. The kernel is
53/// single-threaded, so `&'static` refs are sound; the `addr_of_mut!`
54/// round-trip keeps `static_mut_refs` (warn today, deny in edition
55/// 2024) quiet.
56fn vols() -> &'static mut Vec<Volume> {
57 unsafe { &mut *core::ptr::addr_of_mut!(VOLS) }
58}
59
60fn active_vol() -> &'static mut usize {
61 unsafe { &mut *core::ptr::addr_of_mut!(ACTIVE_VOL) }
62}
63
53pub fn volume_count() -> usize { 64pub fn volume_count() -> usize {
54 unsafe { VOLS.len() } 65 vols().len()
55} 66}
56 67
57/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F"). 68/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F").
58pub fn uid(vol: usize) -> &'static str { 69pub fn uid(vol: usize) -> &'static str {
59 unsafe { VOLS.get(vol) } 70 vols().get(vol).map(|v| v.uid.as_str()).unwrap_or("")
60 .map(|v| v.uid.as_str())
61 .unwrap_or("")
62} 71}
63 72
64pub fn root_cluster(vol: usize) -> u32 { 73pub fn root_cluster(vol: usize) -> u32 {
65 unsafe { VOLS.get(vol) } 74 vols().get(vol).map(|v| v.root_cluster).unwrap_or(0)
66 .map(|v| v.root_cluster)
67 .unwrap_or(0)
68} 75}
69 76
70/// Point all FAT operations at volume `vol`; also selects the backing 77/// Point all FAT operations at volume `vol`; also selects the backing
71/// AHCI disk. Called by every public entry point (single-threaded 78/// AHCI disk. Called by every public entry point (single-threaded
72/// kernel, so a process-wide "current volume" is fine). 79/// kernel, so a process-wide "current volume" is fine).
73fn select_vol(vol: usize) -> Result<(), ()> { 80fn select_vol(vol: usize) -> Result<(), ()> {
74 let disk = unsafe { VOLS.get(vol) }.map(|v| v.disk).ok_or(())?; 81 let disk = vols().get(vol).map(|v| v.disk).ok_or(())?;
75 unsafe { 82 unsafe {
76 ACTIVE_VOL = vol; 83 ACTIVE_VOL = vol;
77 } 84 }
@@ -97,7 +104,7 @@ fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> {
97// --- cluster math ---------------------------------------------------------- 104// --- cluster math ----------------------------------------------------------
98 105
99fn v() -> &'static mut Volume { 106fn v() -> &'static mut Volume {
100 unsafe { VOLS.get_mut(ACTIVE_VOL) }.unwrap() 107 vols().get_mut(*active_vol()).unwrap()
101} 108}
102 109
103fn spc() -> usize { 110fn spc() -> usize {
@@ -351,13 +358,13 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
351 return Err(()); // loop protection on a corrupt chain 358 return Err(()); // loop protection on a corrupt chain
352 } 359 }
353 read_cluster(c, &mut buf)?; 360 read_cluster(c, &mut buf)?;
354 let mut done = false;
355 for (i, e) in buf.chunks_exact(32).enumerate() { 361 for (i, e) in buf.chunks_exact(32).enumerate() {
356 match e[0] { 362 match e[0] {
357 0x00 => { 363 // 0x00 ends the entries in THIS cluster, not the whole
358 done = true; 364 // directory: a dir that spills into a second cluster keeps
359 break; 365 // its 0x00 marker in the first cluster, so we must keep
360 } 366 // walking the chain to see the rest.
367 0x00 => break,
361 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN 368 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN
362 _ => { 369 _ => {
363 let attr = e[11]; 370 let attr = e[11];
@@ -415,9 +422,6 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
415 } 422 }
416 } 423 }
417 } 424 }
418 if done {
419 break;
420 }
421 cur = next_cluster(c).ok(); 425 cur = next_cluster(c).ok();
422 } 426 }
423 Ok(items) 427 Ok(items)
@@ -729,10 +733,77 @@ fn to_8_3(name: &str) -> Option<([u8; 11], bool)> {
729 Some((sfn, needs)) 733 Some((sfn, needs))
730} 734}
731 735
736/// Collect the raw short names already present in a directory.
737fn dir_short_names(parent: u32) -> Result<Vec<[u8; 11]>, ()> {
738 let cs = cluster_size();
739 let mut buf = vec![0u8; cs];
740 let mut out = Vec::new();
741 let mut cur: Option<u32> = Some(parent);
742 let mut guard = 0;
743 while let Some(c) = cur {
744 guard += 1;
745 if guard > 1_000_000 {
746 return Err(()); // loop protection on a corrupt chain
747 }
748 read_cluster(c, &mut buf)?;
749 for e in buf.chunks_exact(32) {
750 match e[0] {
751 // Same rule as scan_dir: 0x00 ends this cluster's entries
752 // but the directory may continue in the next cluster.
753 0x00 => break,
754 0xE5 => {}
755 _ => {
756 if e[11] != 0x0F {
757 let mut sfn = [0u8; 11];
758 sfn.copy_from_slice(&e[0..11]);
759 out.push(sfn);
760 }
761 }
762 }
763 }
764 cur = next_cluster(c).ok();
765 }
766 Ok(out)
767}
768
769/// Make `base` unique within a directory: on collision, append a numeric
770/// ~N tail (DISK-TE~1, DISK-TE~2, …) — the standard FAT short-name
771/// disambiguation that fsck and other OSes expect. Without it, two files
772/// whose long names share an 8.3 prefix would get the same short name and
773/// fsck reports them as duplicate directory entries.
774fn unique_short_name(base: &[u8; 11], used: &[[u8; 11]]) -> [u8; 11] {
775 if !used.contains(base) {
776 return *base;
777 }
778 let mut name_len = 0;
779 while name_len < 8 && base[name_len] != b' ' {
780 name_len += 1;
781 }
782 for n in 1..=999u32 {
783 let tail = alloc::format!("~{}", n);
784 let tb = tail.as_bytes();
785 let keep = name_len.saturating_sub(tb.len());
786 let mut cand = [b' '; 11];
787 cand[..keep].copy_from_slice(&base[..keep]);
788 cand[keep..keep + tb.len()].copy_from_slice(tb);
789 cand[8..11].copy_from_slice(&base[8..11]);
790 if !used.contains(&cand) {
791 return cand;
792 }
793 }
794 *base // pathological; keep the colliding name
795}
796
732/// Write the LFN entries (if any) + SFN entry for `name` into the parent 797/// Write the LFN entries (if any) + SFN entry for `name` into the parent
733/// directory, extending its chain with a fresh zeroed cluster if needed. 798/// directory, extending its chain with a fresh zeroed cluster if needed.
734fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> { 799fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> {
735 let (sfn, needs_lfn) = to_8_3(name).ok_or(())?; 800 let (base_sfn, mut needs_lfn) = to_8_3(name).ok_or(())?;
801 // Short names must be unique within the directory; a plain truncation
802 // collides when two long names share an 8.3 prefix.
803 let sfn = unique_short_name(&base_sfn, &dir_short_names(parent)?);
804 if sfn != base_sfn {
805 needs_lfn = true; // keep the original name via an LFN entry
806 }
736 let lfn_chars: Vec<u16> = name.encode_utf16().collect(); 807 let lfn_chars: Vec<u16> = name.encode_utf16().collect();
737 let groups = if needs_lfn { 808 let groups = if needs_lfn {
738 lfn_chars.len().div_ceil(13) 809 lfn_chars.len().div_ceil(13)
@@ -810,6 +881,18 @@ fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<
810 write_cluster(c, &buf)?; 881 write_cluster(c, &buf)?;
811 return Ok(()); 882 return Ok(());
812 } 883 }
884 // Chain full: pack this cluster before extending. A mid-chain
885 // 0x00 would look like the end of the directory to other FAT
886 // readers (Windows, mtools stop at the first 0x00), hiding every
887 // entry written into the new cluster. Marking the free slots as
888 // 0xE5 (deleted) keeps them skippable while the chain walk finds
889 // the continuation.
890 for i in 0..slots {
891 if buf[i * 32] == 0x00 {
892 buf[i * 32] = 0xE5;
893 }
894 }
895 write_cluster(c, &buf)?;
813 // Chain full: extend with a fresh zeroed cluster and retry. 896 // Chain full: extend with a fresh zeroed cluster and retry.
814 match next_cluster(c) { 897 match next_cluster(c) {
815 Ok(n) => cur = Some(n), 898 Ok(n) => cur = Some(n),
@@ -910,10 +993,8 @@ pub fn init() -> bool {
910 match try_mount(d) { 993 match try_mount(d) {
911 Some(volume) => { 994 Some(volume) => {
912 let uid = volume.uid.clone(); 995 let uid = volume.uid.clone();
913 let vol = unsafe { 996 vols().push(volume);
914 VOLS.push(volume); 997 let vol = vols().len() - 1;
915 VOLS.len() - 1
916 };
917 // Re-sync the FSInfo free-cluster count (may be stale 998 // Re-sync the FSInfo free-cluster count (may be stale
918 // from earlier sessions). 999 // from earlier sessions).
919 if select_vol(vol).is_ok() { 1000 if select_vol(vol).is_ok() {
@@ -1026,7 +1107,7 @@ fn try_mount(d: usize) -> Option<Volume> {
1026 let mut uid = alloc::format!("{}-{:08X}", label, serial); 1107 let mut uid = alloc::format!("{}-{:08X}", label, serial);
1027 let mut n = 2u32; 1108 let mut n = 2u32;
1028 loop { 1109 loop {
1029 let taken = unsafe { VOLS.iter() }.any(|v| v.uid == uid); 1110 let taken = vols().iter().any(|v| v.uid == uid);
1030 if !taken { 1111 if !taken {
1031 break; 1112 break;
1032 } 1113 }
@@ -1038,7 +1119,6 @@ fn try_mount(d: usize) -> Option<Volume> {
1038 uid, 1119 uid,
1039 sectors_per_cluster: spc, 1120 sectors_per_cluster: spc,
1040 reserved, 1121 reserved,
1041 fat_count,
1042 fat_size, 1122 fat_size,
1043 root_cluster, 1123 root_cluster,
1044 first_data, 1124 first_data,