diff options
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 34 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 8 | ||||
| -rw-r--r-- | rsnix/src/fat.rs | 201 | ||||
| -rw-r--r-- | rsnix/src/fs.rs | 76 | ||||
| -rwxr-xr-x | scripts/mkdisk.sh | 8 | ||||
| -rwxr-xr-x | scripts/test-disk.sh | 35 |
6 files changed, 241 insertions, 121 deletions
| @@ -168,19 +168,27 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: | |||
| 168 | geometry. Verified in QEMU: edit/type/navigate/insert/save/exit, exact | 168 | geometry. Verified in QEMU: edit/type/navigate/insert/save/exit, exact |
| 169 | bytes via `cat`/`hexdump`, syntax colours via framebuffer dump. | 169 | bytes via `cat`/`hexdump`, syntax colours via framebuffer dump. |
| 170 | ✅ **AHCI + FAT32 (persistent disk)** — a SATA AHCI driver (polling DMA, | 170 | ✅ **AHCI + FAT32 (persistent disk)** — a SATA AHCI driver (polling DMA, |
| 171 | no interrupts) plus a FAT32 filesystem, mounted as `/disk`; every shell | 171 | no interrupts) plus a FAT32 filesystem, mounted under `/disk/<UID>/`; |
| 172 | command works on disk paths: `ls`/`cat`/`hexdump`/`echo >`/`>>`/ | 172 | every shell command works on disk paths: `ls`/`cat`/`hexdump`/ |
| 173 | `mkdir`/`touch`/`rm`/`edit`. The driver scans **all** implemented | 173 | `echo >`/`>>`/`mkdir`/`touch`/`rm`/`edit`. The driver scans **all** |
| 174 | ports (CAP.NP/PI) and starts every port with a device; `fat::init` | 174 | implemented ports (CAP.NP/PI) and starts every port with a device; |
| 175 | tries each disk in port order and mounts the first valid FAT32 | 175 | `fat::init` mounts **every** valid FAT32 volume it finds, each under |
| 176 | volume — so the kernel finds the disk no matter which AHCI port the | 176 | `/disk/<UID>/` where UID = `<LABEL>-<8-hex-serial>` (e.g. |
| 177 | firmware/QEMU put it on (real hardware included). Whole-FAT cache with write-back to both | 177 | `RSDOS-5BD1E55F`) — sanitized uppercase label + volume serial, unique |
| 178 | FAT copies, checksum-validated LFN read/write, multi-cluster files, | 178 | per format, stable across reboots, human-recognizable and |
| 179 | space-padded `.`/`..` dot entries (`..` → cluster 0 under the root), | 179 | machine-parseable (`^[A-Z0-9_]+-[0-9A-F]{8}$`); cloned volumes get a |
| 180 | FSInfo free-count maintenance (fsck-clean after a session, verified | 180 | `-N` suffix. So the kernel finds the disks no matter which AHCI port |
| 181 | with `fsck.fat -n`). **Files survive reboot** — verified by | 181 | the firmware/QEMU put them on (real hardware included), and every |
| 182 | poweroff → `mdir`/`mtype` on the image and by a fresh boot seeing the | 182 | disk is visible at once (verified: `RSDOS-…` + `DATA2-…` mounted |
| 183 | prior session's files. | 183 | side by side, writes to both, fsck-clean). Whole-FAT cache per |
| 184 | volume with write-back to both FAT copies, checksum-validated LFN | ||
| 185 | read/write (multi-entry names >13 chars stored last-first on disk — | ||
| 186 | both the writer and reader were reversed and cancelled out; fixed), | ||
| 187 | multi-cluster files, space-padded `.`/`..` dot entries (`..` → | ||
| 188 | cluster 0 under the root), FSInfo free-count maintenance | ||
| 189 | (fsck-clean after a session, verified with `fsck.fat -n`). | ||
| 190 | **Files survive reboot** — verified by poweroff → `mdir`/`mtype` on | ||
| 191 | the image and by a fresh boot seeing the prior session's files. | ||
| 184 | 192 | ||
| 185 | QEMU setup: `-drive id=disk,if=none,format=raw,file=disk.img` + | 193 | QEMU setup: `-drive id=disk,if=none,format=raw,file=disk.img` + |
| 186 | `-device ide-hd,drive=disk,bus=ide.0` puts the data disk on AHCI port | 194 | `-device ide-hd,drive=disk,bus=ide.0` puts the data disk on AHCI port |
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 181d3e2..e6b7d2a 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -166,15 +166,11 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT | |||
| 166 | let devices = rsnix::pci::probe(); | 166 | let devices = rsnix::pci::probe(); |
| 167 | rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); | 167 | rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); |
| 168 | 168 | ||
| 169 | // Disk: AHCI SATA driver, then mount a FAT32 volume if one is attached. | 169 | // Disk: AHCI SATA driver, then mount every FAT32 volume under /disk/<UID>/. |
| 170 | if !rsnix::ahci::init() { | 170 | if !rsnix::ahci::init() { |
| 171 | rsnix::serial_println!("ahci: no disk attached"); | 171 | rsnix::serial_println!("ahci: no disk attached"); |
| 172 | } | 172 | } |
| 173 | if rsnix::fat::init() { | 173 | rsnix::fat::init(); // prints one line per mounted volume |
| 174 | rsnix::serial_println!("fat: volume mounted at /disk"); | ||
| 175 | } else { | ||
| 176 | rsnix::serial_println!("fat: no FAT32 volume"); | ||
| 177 | } | ||
| 178 | 174 | ||
| 179 | let (dirs, files) = rsnix::fs::init(); | 175 | let (dirs, files) = rsnix::fs::init(); |
| 180 | rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files); | 176 | rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files); |
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, |
diff --git a/scripts/mkdisk.sh b/scripts/mkdisk.sh index f9a8c09..6bdd50c 100755 --- a/scripts/mkdisk.sh +++ b/scripts/mkdisk.sh | |||
| @@ -55,10 +55,10 @@ fi | |||
| 55 | 55 | ||
| 56 | # mkfs.fat sizes are 1024-byte blocks; accept M/G/K suffixes too. | 56 | # mkfs.fat sizes are 1024-byte blocks; accept M/G/K suffixes too. |
| 57 | case "$SIZE" in | 57 | case "$SIZE" in |
| 58 | *G) SIZE_BLOCKS=$(( ${SIZE%G} * 1024 * 1024 )) ;; | 58 | *G) SIZE_BLOCKS=$((${SIZE%G} * 1024 * 1024)) ;; |
| 59 | *M) SIZE_BLOCKS=$(( ${SIZE%M} * 1024 )) ;; | 59 | *M) SIZE_BLOCKS=$((${SIZE%M} * 1024)) ;; |
| 60 | *K) SIZE_BLOCKS=$(( ${SIZE%K} )) ;; | 60 | *K) SIZE_BLOCKS=$((${SIZE%K})) ;; |
| 61 | *) SIZE_BLOCKS="$SIZE" ;; | 61 | *) SIZE_BLOCKS="$SIZE" ;; |
| 62 | esac | 62 | esac |
| 63 | if [ -z "$SIZE_BLOCKS" ] || [ "$SIZE_BLOCKS" -le 0 ] 2>/dev/null; then | 63 | if [ -z "$SIZE_BLOCKS" ] || [ "$SIZE_BLOCKS" -le 0 ] 2>/dev/null; then |
| 64 | echo "mkdisk: bad size '$SIZE' (use e.g. 64M or 65536)" >&2 | 64 | echo "mkdisk: bad size '$SIZE' (use e.g. 64M or 65536)" >&2 |
diff --git a/scripts/test-disk.sh b/scripts/test-disk.sh index f7be7c6..f7337ef 100755 --- a/scripts/test-disk.sh +++ b/scripts/test-disk.sh | |||
| @@ -31,8 +31,13 @@ ESP_DIR="$PROJECT_DIR/esp" | |||
| 31 | QPID=/tmp/rsdos-test-qpid | 31 | QPID=/tmp/rsdos-test-qpid |
| 32 | SOCK=/tmp/rsdos-test-qmon.sock | 32 | SOCK=/tmp/rsdos-test-qmon.sock |
| 33 | LOG=/tmp/rsdos-test-serial.log | 33 | LOG=/tmp/rsdos-test-serial.log |
| 34 | QLOG=/tmp/rsdos-test-qemu.log | ||
| 34 | STAMP="$(date +%s)" | 35 | STAMP="$(date +%s)" |
| 35 | 36 | ||
| 37 | # Never leave a QEMU behind (an orphan holding our stdout would hang any | ||
| 38 | # caller that pipes our output). | ||
| 39 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 40 | |||
| 36 | fail() { | 41 | fail() { |
| 37 | echo "FAIL: $1" | 42 | echo "FAIL: $1" |
| 38 | [ -f "$LOG" ] && tail -20 "$LOG" | 43 | [ -f "$LOG" ] && tail -20 "$LOG" |
| @@ -42,7 +47,7 @@ fail() { | |||
| 42 | # wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex. | 47 | # wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex. |
| 43 | wait_log() { | 48 | wait_log() { |
| 44 | local i=0 | 49 | local i=0 |
| 45 | while [ "$i" -lt $(( ${3:-60} * 10 )) ]; do | 50 | while [ "$i" -lt $((${3:-60} * 10)) ]; do |
| 46 | grep -q "$2" "$1" 2>/dev/null && return 0 | 51 | grep -q "$2" "$1" 2>/dev/null && return 0 |
| 47 | sleep 0.1 | 52 | sleep 0.1 |
| 48 | i=$((i + 1)) | 53 | i=$((i + 1)) |
| @@ -53,7 +58,7 @@ wait_log() { | |||
| 53 | # wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone. | 58 | # wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone. |
| 54 | wait_qemu_exit() { | 59 | wait_qemu_exit() { |
| 55 | local i=0 | 60 | local i=0 |
| 56 | while [ "$i" -lt $(( ${1:-30} * 10 )) ]; do | 61 | while [ "$i" -lt $((${1:-30} * 10)) ]; do |
| 57 | kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 | 62 | kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 |
| 58 | sleep 0.1 | 63 | sleep 0.1 |
| 59 | i=$((i + 1)) | 64 | i=$((i + 1)) |
| @@ -80,18 +85,29 @@ rm -f "$SOCK" "$LOG" | |||
| 80 | -device ide-hd,drive=esp,bus=ide.1 \ | 85 | -device ide-hd,drive=esp,bus=ide.1 \ |
| 81 | -serial file:"$LOG" \ | 86 | -serial file:"$LOG" \ |
| 82 | -monitor unix:"$SOCK",server,nowait \ | 87 | -monitor unix:"$SOCK",server,nowait \ |
| 83 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none & | 88 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ |
| 84 | echo $! > "$QPID" | 89 | >"$QLOG" 2>&1 & |
| 90 | echo $! >"$QPID" | ||
| 85 | 91 | ||
| 86 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | 92 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" |
| 87 | sleep 1 | 93 | sleep 1 |
| 88 | 94 | ||
| 95 | # Volumes mount under /disk/<UID>/ where UID = "LABEL-SERIAL" (e.g. | ||
| 96 | # RSDOS-5BD1E55F). Discover it from the boot log so the test works on | ||
| 97 | # any image without knowing the serial in advance. | ||
| 98 | VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)" | ||
| 99 | [ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)" | ||
| 100 | echo " mounted volume UID: $VOL_UID" | ||
| 101 | |||
| 89 | echo "==> 4/6 drive the shell (ls / mkdir / echo > / cat / poweroff)" | 102 | echo "==> 4/6 drive the shell (ls / mkdir / echo > / cat / poweroff)" |
| 103 | # The filename is deliberately >13 chars so the FAT LFN path gets | ||
| 104 | # multi-entry entries (they're stored last-first on disk). | ||
| 105 | TESTFILE="disk-test-$STAMP.txt" | ||
| 90 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | 106 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ |
| 91 | "ls /disk" \ | 107 | "ls /disk" \ |
| 92 | "mkdir /disk/sub" \ | 108 | "mkdir /disk/$VOL_UID/sub" \ |
| 93 | "echo disk-test-$STAMP > /disk/sub/hello.txt" \ | 109 | "echo disk-test-$STAMP > /disk/$VOL_UID/sub/$TESTFILE" \ |
| 94 | "cat /disk/sub/hello.txt" \ | 110 | "cat /disk/$VOL_UID/sub/$TESTFILE" \ |
| 95 | "poweroff" | 111 | "poweroff" |
| 96 | 112 | ||
| 97 | echo "==> 5/6 wait for shutdown" | 113 | echo "==> 5/6 wait for shutdown" |
| @@ -102,10 +118,11 @@ grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from se | |||
| 102 | if ! fsck.fat -n "$IMG" >/tmp/rsdos-fsck.out 2>&1; then | 118 | if ! fsck.fat -n "$IMG" >/tmp/rsdos-fsck.out 2>&1; then |
| 103 | fail "fsck.fat reports problems: $(cat /tmp/rsdos-fsck.out | tail -3)" | 119 | fail "fsck.fat reports problems: $(cat /tmp/rsdos-fsck.out | tail -3)" |
| 104 | fi | 120 | fi |
| 105 | MT="$(mtype -i "$IMG" "::/sub/hello.txt" 2>/dev/null || true)" | 121 | MT="$(mtype -i "$IMG" "::/sub/$TESTFILE" 2>/dev/null || true)" |
| 106 | [ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" | 122 | [ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" |
| 107 | 123 | ||
| 108 | echo "" | 124 | echo "" |
| 109 | echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" | 125 | echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" |
| 110 | echo " image: $IMG" | 126 | echo " image: $IMG" |
| 111 | echo " written: /disk/sub/hello.txt = \"disk-test-$STAMP\"" | 127 | echo " volume: /disk/$VOL_UID" |
| 128 | echo " written: /disk/$VOL_UID/sub/$TESTFILE = \"disk-test-$STAMP\"" | ||
