1#[cfg(unix)]
34use nix::libc;
35use std::io::{self, Read, Write};
36use std::path::Path;
37use std::sync::{
38 atomic::{AtomicBool, Ordering},
39 mpsc, Arc, OnceLock,
40};
41use std::time::{Duration, Instant};
42
43const BLOCK_SIZE: usize = 4 * 1024 * 1024;
49
50const PROGRESS_INTERVAL: Duration = Duration::from_millis(400);
52
53static REAL_UID: OnceLock<u32> = OnceLock::new();
62
63pub fn set_real_uid(uid: u32) {
68 let _ = REAL_UID.set(uid);
69}
70
71#[cfg(unix)]
73fn real_uid() -> nix::unistd::Uid {
74 let raw = REAL_UID
75 .get()
76 .copied()
77 .unwrap_or_else(|| nix::unistd::getuid().as_raw());
78 nix::unistd::Uid::from_raw(raw)
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum FlashStage {
88 Starting,
90 Unmounting,
92 Writing,
94 Syncing,
96 Rereading,
98 Verifying,
100 Done,
102 Failed(String),
104}
105
106impl std::fmt::Display for FlashStage {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 FlashStage::Starting => write!(f, "Starting…"),
110 FlashStage::Unmounting => write!(f, "Unmounting partitions…"),
111 FlashStage::Writing => write!(f, "Writing image to device…"),
112 FlashStage::Syncing => write!(f, "Flushing write buffers…"),
113 FlashStage::Rereading => write!(f, "Refreshing partition table…"),
114 FlashStage::Verifying => write!(f, "Verifying written data…"),
115 FlashStage::Done => write!(f, "Flash complete!"),
116 FlashStage::Failed(m) => write!(f, "Failed: {m}"),
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
126pub enum FlashEvent {
127 Stage(FlashStage),
129 Progress {
131 bytes_written: u64,
132 total_bytes: u64,
133 speed_mb_s: f32,
134 },
135 Log(String),
137 Done,
139 Error(String),
141}
142
143pub fn run_pipeline(
159 image_path: &str,
160 device_path: &str,
161 tx: mpsc::Sender<FlashEvent>,
162 cancel: Arc<AtomicBool>,
163) {
164 if let Err(e) = flash_pipeline(image_path, device_path, &tx, cancel) {
165 let _ = tx.send(FlashEvent::Error(e));
166 }
167}
168
169fn send(tx: &mpsc::Sender<FlashEvent>, event: FlashEvent) {
174 let _ = tx.send(event);
176}
177
178fn flash_pipeline(
179 image_path: &str,
180 device_path: &str,
181 tx: &mpsc::Sender<FlashEvent>,
182 cancel: Arc<AtomicBool>,
183) -> Result<(), String> {
184 if !Path::new(image_path).is_file() {
186 return Err(format!("Image file not found: {image_path}"));
187 }
188
189 if !Path::new(device_path).exists() {
190 return Err(format!("Target device not found: {device_path}"));
191 }
192
193 #[cfg(target_os = "linux")]
195 reject_partition_node(device_path)?;
196
197 let image_size = std::fs::metadata(image_path)
198 .map_err(|e| format!("Cannot stat image: {e}"))?
199 .len();
200
201 if image_size == 0 {
202 return Err("Image file is empty".to_string());
203 }
204
205 send(tx, FlashEvent::Stage(FlashStage::Unmounting));
207 unmount_device(device_path, tx);
208
209 send(tx, FlashEvent::Stage(FlashStage::Writing));
211 send(
212 tx,
213 FlashEvent::Log(format!(
214 "Writing {image_size} bytes from {image_path} → {device_path}"
215 )),
216 );
217 write_image(image_path, device_path, image_size, tx, &cancel)?;
218
219 send(tx, FlashEvent::Stage(FlashStage::Syncing));
221 sync_device(device_path, tx);
222
223 send(tx, FlashEvent::Stage(FlashStage::Rereading));
225 reread_partition_table(device_path, tx);
226
227 send(tx, FlashEvent::Stage(FlashStage::Verifying));
229 verify(image_path, device_path, image_size, tx)?;
230
231 send(tx, FlashEvent::Done);
233 Ok(())
234}
235
236#[cfg(target_os = "linux")]
241fn reject_partition_node(device_path: &str) -> Result<(), String> {
242 let dev_name = Path::new(device_path)
243 .file_name()
244 .map(|n| n.to_string_lossy().to_string())
245 .unwrap_or_default();
246
247 let is_partition = {
248 let bytes = dev_name.as_bytes();
249 !bytes.is_empty() && bytes[bytes.len() - 1].is_ascii_digit() && {
250 let stem = dev_name.trim_end_matches(|c: char| c.is_ascii_digit());
251 stem.ends_with('p')
252 || (!stem.is_empty()
253 && !stem.ends_with(|c: char| c.is_ascii_digit())
254 && stem.chars().any(|c| c.is_ascii_alphabetic()))
255 }
256 };
257
258 if is_partition {
259 let whole = dev_name.trim_end_matches(|c: char| c.is_ascii_digit() || c == 'p');
260 return Err(format!(
261 "Refusing to write to partition node '{device_path}'. \
262 Select the whole-disk device (e.g. /dev/{whole}) instead."
263 ));
264 }
265
266 Ok(())
267}
268
269fn open_device_for_writing(device_path: &str) -> Result<std::fs::File, String> {
276 #[cfg(unix)]
277 {
278 use nix::unistd::seteuid;
279
280 let escalated = seteuid(nix::unistd::Uid::from_raw(0)).is_ok();
287
288 let result = std::fs::OpenOptions::new()
289 .write(true)
290 .open(device_path)
291 .map_err(|e| {
292 let raw = e.raw_os_error().unwrap_or(0);
293 if raw == libc::EACCES || raw == libc::EPERM {
294 if escalated {
295 format!(
296 "Permission denied opening '{device_path}'.\n\
297 Even with setuid-root the device refused access — \
298 check that the device exists and is not in use."
299 )
300 } else {
301 format!(
302 "Permission denied opening '{device_path}'.\n\
303 The binary is not installed setuid-root.\n\
304 Install with:\n \
305 sudo chown root:root /usr/bin/flashkraft\n \
306 sudo chmod u+s /usr/bin/flashkraft"
307 )
308 }
309 } else if raw == libc::EBUSY {
310 format!(
311 "Device '{device_path}' is busy. \
312 Ensure all partitions are unmounted before flashing."
313 )
314 } else {
315 format!("Cannot open device '{device_path}' for writing: {e}")
316 }
317 });
318
319 if escalated {
321 let _ = seteuid(real_uid());
322 }
323
324 result
325 }
326
327 #[cfg(not(unix))]
328 {
329 std::fs::OpenOptions::new()
330 .write(true)
331 .open(device_path)
332 .map_err(|e| {
333 let raw = e.raw_os_error().unwrap_or(0);
334 if raw == 5 || raw == 1314 {
336 format!(
337 "Access denied opening '{device_path}'.\n\
338 FlashKraft must be run as Administrator on Windows.\n\
339 Right-click the application and choose \
340 'Run as administrator'."
341 )
342 } else if raw == 32 {
343 format!(
345 "Device '{device_path}' is in use by another process.\n\
346 Close any applications using the drive and try again."
347 )
348 } else {
349 format!("Cannot open device '{device_path}' for writing: {e}")
350 }
351 })
352 }
353}
354
355fn unmount_device(device_path: &str, tx: &mpsc::Sender<FlashEvent>) {
360 let device_name = Path::new(device_path)
361 .file_name()
362 .map(|n| n.to_string_lossy().to_string())
363 .unwrap_or_default();
364
365 let partitions = find_mounted_partitions(&device_name, device_path);
366
367 if partitions.is_empty() {
368 send(tx, FlashEvent::Log("No mounted partitions found".into()));
369 } else {
370 for partition in &partitions {
371 send(tx, FlashEvent::Log(format!("Unmounting {partition}")));
372 do_unmount(partition, tx);
373 }
374 }
375}
376
377fn find_mounted_partitions(
385 #[cfg_attr(target_os = "windows", allow(unused_variables))] device_name: &str,
386 device_path: &str,
387) -> Vec<String> {
388 #[cfg(not(target_os = "windows"))]
389 {
390 let mounts = std::fs::read_to_string("/proc/mounts")
391 .or_else(|_| std::fs::read_to_string("/proc/self/mounts"))
392 .unwrap_or_default();
393
394 let mut partitions = Vec::new();
395 for line in mounts.lines() {
396 let dev = match line.split_whitespace().next() {
397 Some(d) => d,
398 None => continue,
399 };
400 if dev == device_path || is_partition_of(dev, device_name) {
401 partitions.push(dev.to_string());
402 }
403 }
404 partitions
405 }
406
407 #[cfg(target_os = "windows")]
408 {
409 windows::find_volumes_on_physical_drive(device_path)
410 }
411}
412
413#[cfg(not(target_os = "windows"))]
414fn is_partition_of(dev: &str, device_name: &str) -> bool {
415 let dev_base = Path::new(dev)
417 .file_name()
418 .map(|n| n.to_string_lossy())
419 .unwrap_or_default();
420
421 if !dev_base.starts_with(device_name) {
422 return false;
423 }
424 let suffix = &dev_base[device_name.len()..];
425 if suffix.is_empty() {
426 return false;
427 }
428 let first = suffix.chars().next().unwrap();
429 first.is_ascii_digit() || (first == 'p' && suffix.len() > 1)
430}
431
432fn do_unmount(partition: &str, tx: &mpsc::Sender<FlashEvent>) {
433 #[cfg(target_os = "linux")]
434 {
435 use nix::unistd::seteuid;
436 use std::ffi::CString;
437
438 let _ = seteuid(nix::unistd::Uid::from_raw(0));
440
441 if let Ok(c_path) = CString::new(partition) {
442 let ret = unsafe { libc::umount2(c_path.as_ptr(), libc::MNT_DETACH) };
443 if ret != 0 {
444 let err = std::io::Error::last_os_error();
445 send(
446 tx,
447 FlashEvent::Log(format!("Warning — could not unmount {partition}: {err}")),
448 );
449 }
450 }
451
452 let _ = seteuid(real_uid());
453 }
454
455 #[cfg(target_os = "macos")]
456 {
457 let out = std::process::Command::new("diskutil")
458 .args(["unmount", partition])
459 .output();
460 if let Ok(o) = out {
461 if !o.status.success() {
462 send(
463 tx,
464 FlashEvent::Log(format!("Warning — diskutil unmount {partition} failed")),
465 );
466 }
467 }
468 }
469
470 #[cfg(target_os = "windows")]
473 {
474 match windows::lock_and_dismount_volume(partition) {
475 Ok(()) => send(
476 tx,
477 FlashEvent::Log(format!("Dismounted volume {partition}")),
478 ),
479 Err(e) => send(
480 tx,
481 FlashEvent::Log(format!("Warning — could not dismount {partition}: {e}")),
482 ),
483 }
484 }
485}
486
487fn write_image(
492 image_path: &str,
493 device_path: &str,
494 image_size: u64,
495 tx: &mpsc::Sender<FlashEvent>,
496 cancel: &Arc<AtomicBool>,
497) -> Result<(), String> {
498 let image_file =
499 std::fs::File::open(image_path).map_err(|e| format!("Cannot open image: {e}"))?;
500
501 let device_file = open_device_for_writing(device_path)?;
502
503 let mut reader = io::BufReader::with_capacity(BLOCK_SIZE, image_file);
504 let mut writer = io::BufWriter::with_capacity(BLOCK_SIZE, device_file);
505 let mut buf = vec![0u8; BLOCK_SIZE];
506
507 let mut bytes_written: u64 = 0;
508 let start = Instant::now();
509 let mut last_report = Instant::now();
510
511 loop {
512 if cancel.load(Ordering::SeqCst) {
514 return Err("Flash operation cancelled by user".to_string());
515 }
516
517 let n = reader
518 .read(&mut buf)
519 .map_err(|e| format!("Read error on image: {e}"))?;
520
521 if n == 0 {
522 break; }
524
525 writer
526 .write_all(&buf[..n])
527 .map_err(|e| format!("Write error on device: {e}"))?;
528
529 bytes_written += n as u64;
530
531 let now = Instant::now();
532 if now.duration_since(last_report) >= PROGRESS_INTERVAL || bytes_written >= image_size {
533 let elapsed_s = now.duration_since(start).as_secs_f32();
534 let speed_mb_s = if elapsed_s > 0.001 {
535 (bytes_written as f32 / (1024.0 * 1024.0)) / elapsed_s
536 } else {
537 0.0
538 };
539
540 send(
541 tx,
542 FlashEvent::Progress {
543 bytes_written,
544 total_bytes: image_size,
545 speed_mb_s,
546 },
547 );
548 last_report = now;
549 }
550 }
551
552 writer
554 .flush()
555 .map_err(|e| format!("Buffer flush error: {e}"))?;
556
557 #[cfg_attr(not(unix), allow(unused_variables))]
559 let device_file = writer
560 .into_inner()
561 .map_err(|e| format!("BufWriter error: {e}"))?;
562
563 #[cfg(unix)]
567 {
568 use std::os::unix::io::AsRawFd;
569 let fd = device_file.as_raw_fd();
570 let ret = unsafe { libc::fsync(fd) };
571 if ret != 0 {
572 let err = std::io::Error::last_os_error();
573 return Err(format!(
574 "fsync failed on '{device_path}': {err} — \
575 data may not have been fully written to the device"
576 ));
577 }
578 }
579
580 let elapsed_s = start.elapsed().as_secs_f32();
582 let speed_mb_s = if elapsed_s > 0.001 {
583 (bytes_written as f32 / (1024.0 * 1024.0)) / elapsed_s
584 } else {
585 0.0
586 };
587 send(
588 tx,
589 FlashEvent::Progress {
590 bytes_written,
591 total_bytes: image_size,
592 speed_mb_s,
593 },
594 );
595
596 send(tx, FlashEvent::Log("Image write complete".into()));
597 Ok(())
598}
599
600fn sync_device(device_path: &str, tx: &mpsc::Sender<FlashEvent>) {
605 #[cfg(unix)]
606 if let Ok(f) = std::fs::OpenOptions::new().write(true).open(device_path) {
607 use std::os::unix::io::AsRawFd;
608 let fd = f.as_raw_fd();
609 #[cfg(target_os = "linux")]
610 unsafe {
611 libc::fdatasync(fd);
612 }
613 #[cfg(not(target_os = "linux"))]
614 unsafe {
615 libc::fsync(fd);
616 }
617 drop(f);
618 }
619
620 #[cfg(target_os = "linux")]
621 unsafe {
622 libc::sync();
623 }
624
625 #[cfg(target_os = "windows")]
628 {
629 match windows::flush_device_buffers(device_path) {
630 Ok(()) => {}
631 Err(e) => send(
632 tx,
633 FlashEvent::Log(format!(
634 "Warning — FlushFileBuffers on '{device_path}' failed: {e}"
635 )),
636 ),
637 }
638 }
639
640 send(tx, FlashEvent::Log("Write-back caches flushed".into()));
641}
642
643#[cfg(target_os = "linux")]
648fn reread_partition_table(device_path: &str, tx: &mpsc::Sender<FlashEvent>) {
649 use nix::ioctl_none;
650 use std::os::unix::io::AsRawFd;
651
652 ioctl_none!(blkrrpart, 0x12, 95);
653
654 std::thread::sleep(Duration::from_millis(500));
656
657 match std::fs::OpenOptions::new().write(true).open(device_path) {
658 Ok(f) => {
659 let result = unsafe { blkrrpart(f.as_raw_fd()) };
660 match result {
661 Ok(_) => send(
662 tx,
663 FlashEvent::Log("Kernel partition table refreshed".into()),
664 ),
665 Err(e) => send(
666 tx,
667 FlashEvent::Log(format!(
668 "Warning — BLKRRPART ioctl failed \
669 (device may not be partitioned): {e}"
670 )),
671 ),
672 }
673 }
674 Err(e) => send(
675 tx,
676 FlashEvent::Log(format!(
677 "Warning — could not open device for BLKRRPART: {e}"
678 )),
679 ),
680 }
681}
682
683#[cfg(target_os = "macos")]
684fn reread_partition_table(device_path: &str, tx: &mpsc::Sender<FlashEvent>) {
685 let _ = std::process::Command::new("diskutil")
686 .args(["rereadPartitionTable", device_path])
687 .output();
688 send(
689 tx,
690 FlashEvent::Log("Partition table refresh requested (macOS)".into()),
691 );
692}
693
694#[cfg(target_os = "windows")]
697fn reread_partition_table(device_path: &str, tx: &mpsc::Sender<FlashEvent>) {
698 std::thread::sleep(Duration::from_millis(500));
700
701 match windows::update_disk_properties(device_path) {
702 Ok(()) => send(
703 tx,
704 FlashEvent::Log("Partition table refreshed (IOCTL_DISK_UPDATE_PROPERTIES)".into()),
705 ),
706 Err(e) => send(
707 tx,
708 FlashEvent::Log(format!(
709 "Warning — IOCTL_DISK_UPDATE_PROPERTIES failed: {e}"
710 )),
711 ),
712 }
713}
714
715#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
716fn reread_partition_table(_device_path: &str, tx: &mpsc::Sender<FlashEvent>) {
717 send(
718 tx,
719 FlashEvent::Log("Partition table refresh not supported on this platform".into()),
720 );
721}
722
723fn verify(
728 image_path: &str,
729 device_path: &str,
730 image_size: u64,
731 tx: &mpsc::Sender<FlashEvent>,
732) -> Result<(), String> {
733 send(
734 tx,
735 FlashEvent::Log("Computing SHA-256 of source image".into()),
736 );
737 let image_hash = sha256_first_n_bytes(image_path, image_size)?;
738
739 send(
740 tx,
741 FlashEvent::Log(format!(
742 "Reading back {image_size} bytes from device for verification"
743 )),
744 );
745 let device_hash = sha256_first_n_bytes(device_path, image_size)?;
746
747 if image_hash != device_hash {
748 return Err(format!(
749 "Verification failed — data mismatch \
750 (image={image_hash} device={device_hash})"
751 ));
752 }
753
754 send(
755 tx,
756 FlashEvent::Log(format!("Verification passed ({image_hash})")),
757 );
758 Ok(())
759}
760
761fn sha256_first_n_bytes(path: &str, max_bytes: u64) -> Result<String, String> {
762 use sha2::{Digest, Sha256};
763
764 let file =
765 std::fs::File::open(path).map_err(|e| format!("Cannot open {path} for hashing: {e}"))?;
766
767 let mut hasher = Sha256::new();
768 let mut reader = io::BufReader::with_capacity(BLOCK_SIZE, file);
769 let mut buf = vec![0u8; BLOCK_SIZE];
770 let mut remaining = max_bytes;
771
772 while remaining > 0 {
773 let to_read = (remaining as usize).min(buf.len());
774 let n = reader
775 .read(&mut buf[..to_read])
776 .map_err(|e| format!("Read error while hashing {path}: {e}"))?;
777 if n == 0 {
778 break;
779 }
780 hasher.update(&buf[..n]);
781 remaining -= n as u64;
782 }
783
784 Ok(format!("{:x}", hasher.finalize()))
785}
786
787#[cfg(target_os = "windows")]
805mod windows {
806 use windows_sys::Win32::{
809 Foundation::{
810 CloseHandle, FALSE, GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
811 },
812 Storage::FileSystem::{
813 CreateFileW, FlushFileBuffers, FILE_FLAG_WRITE_THROUGH, FILE_SHARE_READ,
814 FILE_SHARE_WRITE, OPEN_EXISTING,
815 },
816 System::{
817 Ioctl::{FSCTL_DISMOUNT_VOLUME, FSCTL_LOCK_VOLUME, IOCTL_DISK_UPDATE_PROPERTIES},
818 IO::DeviceIoControl,
819 },
820 };
821
822 fn to_wide(s: &str) -> Vec<u16> {
826 use std::os::windows::ffi::OsStrExt;
827 std::ffi::OsStr::new(s)
828 .encode_wide()
829 .chain(std::iter::once(0))
830 .collect()
831 }
832
833 fn open_device_handle(path: &str, access: u32) -> Result<HANDLE, String> {
838 let wide = to_wide(path);
839 let handle = unsafe {
840 CreateFileW(
841 wide.as_ptr(),
842 access,
843 FILE_SHARE_READ | FILE_SHARE_WRITE,
844 std::ptr::null(),
845 OPEN_EXISTING,
846 FILE_FLAG_WRITE_THROUGH,
847 std::ptr::null_mut(),
848 )
849 };
850 if handle == INVALID_HANDLE_VALUE {
851 Err(format!(
852 "Cannot open device '{}': {}",
853 path,
854 std::io::Error::last_os_error()
855 ))
856 } else {
857 Ok(handle)
858 }
859 }
860
861 fn device_ioctl(handle: HANDLE, code: u32) -> Result<(), String> {
865 let mut bytes_returned: u32 = 0;
866 let ok = unsafe {
867 DeviceIoControl(
868 handle,
869 code,
870 std::ptr::null(), 0,
872 std::ptr::null_mut(), 0,
874 &mut bytes_returned,
875 std::ptr::null_mut(), )
877 };
878 if ok == FALSE {
879 Err(format!("{}", std::io::Error::last_os_error()))
880 } else {
881 Ok(())
882 }
883 }
884
885 pub fn find_volumes_on_physical_drive(physical_drive: &str) -> Vec<String> {
900 use windows_sys::Win32::{
901 Storage::FileSystem::GetLogicalDriveStringsW,
902 System::Ioctl::{IOCTL_STORAGE_GET_DEVICE_NUMBER, STORAGE_DEVICE_NUMBER},
903 };
904
905 let target_index: u32 = physical_drive
907 .to_ascii_lowercase()
908 .trim_start_matches(r"\\.\physicaldrive")
909 .parse()
910 .unwrap_or(u32::MAX);
911
912 let mut buf = vec![0u16; 512];
914 let len = unsafe { GetLogicalDriveStringsW(buf.len() as u32, buf.as_mut_ptr()) };
915 if len == 0 || len > buf.len() as u32 {
916 return Vec::new();
917 }
918
919 let drive_letters: Vec<String> = buf[..len as usize]
921 .split(|&c| c == 0)
922 .filter(|s| !s.is_empty())
923 .map(|s| {
924 let letter: String = std::char::from_u32(s[0] as u32)
927 .map(|c| c.to_string())
928 .unwrap_or_default();
929 format!(r"\\.\{}:", letter)
930 })
931 .collect();
932
933 let mut matching = Vec::new();
934
935 for vol_path in &drive_letters {
936 let wide = to_wide(vol_path);
937 let handle = unsafe {
938 CreateFileW(
939 wide.as_ptr(),
940 GENERIC_READ,
941 FILE_SHARE_READ | FILE_SHARE_WRITE,
942 std::ptr::null(),
943 OPEN_EXISTING,
944 0,
945 std::ptr::null_mut(),
946 )
947 };
948 if handle == INVALID_HANDLE_VALUE {
949 continue;
950 }
951
952 let mut dev_num = STORAGE_DEVICE_NUMBER {
953 DeviceType: 0,
954 DeviceNumber: u32::MAX,
955 PartitionNumber: 0,
956 };
957 let mut bytes_returned: u32 = 0;
958
959 let ok = unsafe {
960 DeviceIoControl(
961 handle,
962 IOCTL_STORAGE_GET_DEVICE_NUMBER,
963 std::ptr::null(),
964 0,
965 &mut dev_num as *mut _ as *mut _,
966 std::mem::size_of::<STORAGE_DEVICE_NUMBER>() as u32,
967 &mut bytes_returned,
968 std::ptr::null_mut(),
969 )
970 };
971
972 unsafe { CloseHandle(handle) };
973
974 if ok != FALSE && dev_num.DeviceNumber == target_index {
975 matching.push(vol_path.clone());
976 }
977 }
978
979 matching
980 }
981
982 pub fn lock_and_dismount_volume(volume_path: &str) -> Result<(), String> {
994 let handle = open_device_handle(volume_path, GENERIC_READ | GENERIC_WRITE)?;
995
996 let lock_result = device_ioctl(handle, FSCTL_LOCK_VOLUME);
999 if let Err(ref e) = lock_result {
1000 eprintln!(
1002 "[flash] FSCTL_LOCK_VOLUME on '{volume_path}' failed ({e}); \
1003 attempting dismount anyway"
1004 );
1005 }
1006
1007 let dismount_result = device_ioctl(handle, FSCTL_DISMOUNT_VOLUME);
1009
1010 unsafe { CloseHandle(handle) };
1011
1012 lock_result.and(dismount_result)
1013 }
1014
1015 pub fn flush_device_buffers(device_path: &str) -> Result<(), String> {
1018 let handle = open_device_handle(device_path, GENERIC_WRITE)?;
1019 let ok = unsafe { FlushFileBuffers(handle) };
1020 unsafe { CloseHandle(handle) };
1021 if ok == FALSE {
1022 Err(format!("{}", std::io::Error::last_os_error()))
1023 } else {
1024 Ok(())
1025 }
1026 }
1027
1028 pub fn update_disk_properties(device_path: &str) -> Result<(), String> {
1031 let handle = open_device_handle(device_path, GENERIC_READ | GENERIC_WRITE)?;
1032 let result = device_ioctl(handle, IOCTL_DISK_UPDATE_PROPERTIES);
1033 unsafe { CloseHandle(handle) };
1034 result
1035 }
1036
1037 #[cfg(test)]
1040 mod tests {
1041 use super::*;
1042
1043 #[test]
1045 fn test_to_wide_null_terminated() {
1046 let wide = to_wide("ABC");
1047 assert_eq!(wide.last(), Some(&0u16), "must be null-terminated");
1048 assert_eq!(&wide[..3], &[b'A' as u16, b'B' as u16, b'C' as u16]);
1049 }
1050
1051 #[test]
1053 fn test_to_wide_empty() {
1054 let wide = to_wide("");
1055 assert_eq!(wide, vec![0u16]);
1056 }
1057
1058 #[test]
1060 fn test_open_device_handle_bad_path_returns_error() {
1061 let result = open_device_handle(r"\\.\NonExistentDevice999", GENERIC_READ);
1062 assert!(result.is_err(), "expected error for nonexistent device");
1063 }
1064
1065 #[test]
1067 fn test_flush_device_buffers_bad_path() {
1068 let result = flush_device_buffers(r"\\.\PhysicalDrive999");
1069 assert!(result.is_err());
1070 }
1071
1072 #[test]
1074 fn test_update_disk_properties_bad_path() {
1075 let result = update_disk_properties(r"\\.\PhysicalDrive999");
1076 assert!(result.is_err());
1077 }
1078
1079 #[test]
1081 fn test_lock_and_dismount_bad_path() {
1082 let result = lock_and_dismount_volume(r"\\.\Z99:");
1083 assert!(result.is_err());
1084 }
1085
1086 #[test]
1089 fn test_find_volumes_bad_path_no_panic() {
1090 let result = find_volumes_on_physical_drive("not-a-valid-path");
1091 let _ = result;
1093 }
1094
1095 #[test]
1098 fn test_find_volumes_nonexistent_drive_returns_empty() {
1099 let result = find_volumes_on_physical_drive(r"\\.\PhysicalDrive999");
1100 assert!(
1101 result.is_empty(),
1102 "expected no volumes for PhysicalDrive999"
1103 );
1104 }
1105 }
1106}
1107
1108#[cfg(test)]
1113mod tests {
1114 use super::*;
1115 use std::io::Write;
1116 use std::sync::mpsc;
1117
1118 fn make_channel() -> (mpsc::Sender<FlashEvent>, mpsc::Receiver<FlashEvent>) {
1119 mpsc::channel()
1120 }
1121
1122 fn drain(rx: &mpsc::Receiver<FlashEvent>) -> Vec<FlashEvent> {
1123 let mut events = Vec::new();
1124 while let Ok(e) = rx.try_recv() {
1125 events.push(e);
1126 }
1127 events
1128 }
1129
1130 fn has_stage(events: &[FlashEvent], stage: &FlashStage) -> bool {
1131 events
1132 .iter()
1133 .any(|e| matches!(e, FlashEvent::Stage(s) if s == stage))
1134 }
1135
1136 fn find_error(events: &[FlashEvent]) -> Option<&str> {
1137 events.iter().find_map(|e| {
1138 if let FlashEvent::Error(msg) = e {
1139 Some(msg.as_str())
1140 } else {
1141 None
1142 }
1143 })
1144 }
1145
1146 #[test]
1149 fn test_set_real_uid_stores_value() {
1150 set_real_uid(1000);
1153 }
1154
1155 #[test]
1158 #[cfg(not(target_os = "windows"))]
1159 fn test_is_partition_of_sda() {
1160 assert!(is_partition_of("/dev/sda1", "sda"));
1161 assert!(is_partition_of("/dev/sda2", "sda"));
1162 assert!(!is_partition_of("/dev/sdb1", "sda"));
1163 assert!(!is_partition_of("/dev/sda", "sda"));
1164 }
1165
1166 #[test]
1167 #[cfg(not(target_os = "windows"))]
1168 fn test_is_partition_of_nvme() {
1169 assert!(is_partition_of("/dev/nvme0n1p1", "nvme0n1"));
1170 assert!(is_partition_of("/dev/nvme0n1p2", "nvme0n1"));
1171 assert!(!is_partition_of("/dev/nvme0n1", "nvme0n1"));
1172 }
1173
1174 #[test]
1175 #[cfg(not(target_os = "windows"))]
1176 fn test_is_partition_of_mmcblk() {
1177 assert!(is_partition_of("/dev/mmcblk0p1", "mmcblk0"));
1178 assert!(!is_partition_of("/dev/mmcblk0", "mmcblk0"));
1179 }
1180
1181 #[test]
1182 #[cfg(not(target_os = "windows"))]
1183 fn test_is_partition_of_no_false_prefix_match() {
1184 assert!(!is_partition_of("/dev/sda1", "sd"));
1185 }
1186
1187 #[test]
1190 #[cfg(target_os = "linux")]
1191 fn test_reject_partition_node_sda1() {
1192 let dir = std::env::temp_dir();
1193 let img = dir.join("fk_reject_img.bin");
1194 std::fs::write(&img, vec![0u8; 1024]).unwrap();
1195
1196 let result = reject_partition_node("/dev/sda1");
1197 assert!(result.is_err());
1198 assert!(result.unwrap_err().contains("Refusing"));
1199
1200 let _ = std::fs::remove_file(img);
1201 }
1202
1203 #[test]
1204 #[cfg(target_os = "linux")]
1205 fn test_reject_partition_node_nvme() {
1206 let result = reject_partition_node("/dev/nvme0n1p1");
1207 assert!(result.is_err());
1208 assert!(result.unwrap_err().contains("Refusing"));
1209 }
1210
1211 #[test]
1212 #[cfg(target_os = "linux")]
1213 fn test_reject_partition_node_accepts_whole_disk() {
1214 let result = reject_partition_node("/dev/sdb");
1217 assert!(result.is_ok(), "whole-disk node should not be rejected");
1218 }
1219
1220 #[test]
1223 fn test_find_mounted_partitions_parses_proc_mounts_format() {
1224 let result = find_mounted_partitions("sda", "/dev/sda");
1227 let _ = result; }
1229
1230 #[test]
1233 fn test_sha256_full_file() {
1234 use sha2::{Digest, Sha256};
1235
1236 let dir = std::env::temp_dir();
1237 let path = dir.join("fk_sha256_full.bin");
1238 let data: Vec<u8> = (0u8..=255u8).cycle().take(4096).collect();
1239 std::fs::write(&path, &data).unwrap();
1240
1241 let result = sha256_first_n_bytes(path.to_str().unwrap(), data.len() as u64).unwrap();
1242 let expected = format!("{:x}", Sha256::digest(&data));
1243 assert_eq!(result, expected);
1244
1245 let _ = std::fs::remove_file(path);
1246 }
1247
1248 #[test]
1249 fn test_sha256_partial() {
1250 use sha2::{Digest, Sha256};
1251
1252 let dir = std::env::temp_dir();
1253 let path = dir.join("fk_sha256_partial.bin");
1254 let data: Vec<u8> = (0u8..=255u8).cycle().take(8192).collect();
1255 std::fs::write(&path, &data).unwrap();
1256
1257 let n = 4096u64;
1258 let result = sha256_first_n_bytes(path.to_str().unwrap(), n).unwrap();
1259 let expected = format!("{:x}", Sha256::digest(&data[..n as usize]));
1260 assert_eq!(result, expected);
1261
1262 let _ = std::fs::remove_file(path);
1263 }
1264
1265 #[test]
1266 fn test_sha256_nonexistent_returns_error() {
1267 let result = sha256_first_n_bytes("/nonexistent/path.bin", 1024);
1268 assert!(result.is_err());
1269 assert!(result.unwrap_err().contains("Cannot open"));
1270 }
1271
1272 #[test]
1273 fn test_sha256_empty_read_is_hash_of_empty() {
1274 use sha2::{Digest, Sha256};
1275
1276 let dir = std::env::temp_dir();
1277 let path = dir.join("fk_sha256_empty.bin");
1278 std::fs::write(&path, b"hello world extended data").unwrap();
1279
1280 let result = sha256_first_n_bytes(path.to_str().unwrap(), 0).unwrap();
1282 let expected = format!("{:x}", Sha256::digest(b""));
1283 assert_eq!(result, expected);
1284
1285 let _ = std::fs::remove_file(path);
1286 }
1287
1288 #[test]
1291 fn test_write_image_to_temp_file() {
1292 let dir = std::env::temp_dir();
1293 let img_path = dir.join("fk_write_img.bin");
1294 let dev_path = dir.join("fk_write_dev.bin");
1295
1296 let image_size: u64 = 2 * 1024 * 1024; {
1298 let mut f = std::fs::File::create(&img_path).unwrap();
1299 let block: Vec<u8> = (0u8..=255u8).cycle().take(BLOCK_SIZE).collect();
1300 let mut rem = image_size;
1301 while rem > 0 {
1302 let n = rem.min(BLOCK_SIZE as u64) as usize;
1303 f.write_all(&block[..n]).unwrap();
1304 rem -= n as u64;
1305 }
1306 }
1307 std::fs::File::create(&dev_path).unwrap();
1308
1309 let (tx, rx) = make_channel();
1310 let cancel = Arc::new(AtomicBool::new(false));
1311
1312 let result = write_image(
1313 img_path.to_str().unwrap(),
1314 dev_path.to_str().unwrap(),
1315 image_size,
1316 &tx,
1317 &cancel,
1318 );
1319
1320 assert!(result.is_ok(), "write_image failed: {result:?}");
1321
1322 let written = std::fs::read(&dev_path).unwrap();
1323 let original = std::fs::read(&img_path).unwrap();
1324 assert_eq!(written, original, "written data must match image exactly");
1325
1326 let events = drain(&rx);
1327 let has_progress = events
1328 .iter()
1329 .any(|e| matches!(e, FlashEvent::Progress { .. }));
1330 assert!(has_progress, "must emit at least one Progress event");
1331
1332 let _ = std::fs::remove_file(img_path);
1333 let _ = std::fs::remove_file(dev_path);
1334 }
1335
1336 #[test]
1337 fn test_write_image_cancelled_mid_write() {
1338 let dir = std::env::temp_dir();
1339 let img_path = dir.join("fk_cancel_img.bin");
1340 let dev_path = dir.join("fk_cancel_dev.bin");
1341
1342 let image_size: u64 = 8 * 1024 * 1024; {
1345 let mut f = std::fs::File::create(&img_path).unwrap();
1346 let block = vec![0xAAu8; BLOCK_SIZE];
1347 let mut rem = image_size;
1348 while rem > 0 {
1349 let n = rem.min(BLOCK_SIZE as u64) as usize;
1350 f.write_all(&block[..n]).unwrap();
1351 rem -= n as u64;
1352 }
1353 }
1354 std::fs::File::create(&dev_path).unwrap();
1355
1356 let (tx, _rx) = make_channel();
1357 let cancel = Arc::new(AtomicBool::new(true)); let result = write_image(
1360 img_path.to_str().unwrap(),
1361 dev_path.to_str().unwrap(),
1362 image_size,
1363 &tx,
1364 &cancel,
1365 );
1366
1367 assert!(result.is_err());
1368 assert!(
1369 result.unwrap_err().contains("cancelled"),
1370 "error should mention cancellation"
1371 );
1372
1373 let _ = std::fs::remove_file(img_path);
1374 let _ = std::fs::remove_file(dev_path);
1375 }
1376
1377 #[test]
1378 fn test_write_image_missing_image_returns_error() {
1379 let dir = std::env::temp_dir();
1380 let dev_path = dir.join("fk_noimg_dev.bin");
1381 std::fs::File::create(&dev_path).unwrap();
1382
1383 let (tx, _rx) = make_channel();
1384 let cancel = Arc::new(AtomicBool::new(false));
1385
1386 let result = write_image(
1387 "/nonexistent/image.img",
1388 dev_path.to_str().unwrap(),
1389 1024,
1390 &tx,
1391 &cancel,
1392 );
1393
1394 assert!(result.is_err());
1395 assert!(result.unwrap_err().contains("Cannot open image"));
1396
1397 let _ = std::fs::remove_file(dev_path);
1398 }
1399
1400 #[test]
1403 fn test_verify_matching_files() {
1404 let dir = std::env::temp_dir();
1405 let img = dir.join("fk_verify_img.bin");
1406 let dev = dir.join("fk_verify_dev.bin");
1407 let data = vec![0xBBu8; 64 * 1024];
1408 std::fs::write(&img, &data).unwrap();
1409 std::fs::write(&dev, &data).unwrap();
1410
1411 let (tx, _rx) = make_channel();
1412 let result = verify(
1413 img.to_str().unwrap(),
1414 dev.to_str().unwrap(),
1415 data.len() as u64,
1416 &tx,
1417 );
1418 assert!(result.is_ok());
1419
1420 let _ = std::fs::remove_file(img);
1421 let _ = std::fs::remove_file(dev);
1422 }
1423
1424 #[test]
1425 fn test_verify_mismatch_returns_error() {
1426 let dir = std::env::temp_dir();
1427 let img = dir.join("fk_mismatch_img.bin");
1428 let dev = dir.join("fk_mismatch_dev.bin");
1429 std::fs::write(&img, vec![0x00u8; 64 * 1024]).unwrap();
1430 std::fs::write(&dev, vec![0xFFu8; 64 * 1024]).unwrap();
1431
1432 let (tx, _rx) = make_channel();
1433 let result = verify(img.to_str().unwrap(), dev.to_str().unwrap(), 64 * 1024, &tx);
1434 assert!(result.is_err());
1435 assert!(result.unwrap_err().contains("Verification failed"));
1436
1437 let _ = std::fs::remove_file(img);
1438 let _ = std::fs::remove_file(dev);
1439 }
1440
1441 #[test]
1442 fn test_verify_only_checks_image_size_bytes() {
1443 let dir = std::env::temp_dir();
1444 let img = dir.join("fk_trunc_img.bin");
1445 let dev = dir.join("fk_trunc_dev.bin");
1446 let image_data = vec![0xCCu8; 32 * 1024];
1447 let mut device_data = image_data.clone();
1448 device_data.extend_from_slice(&[0xDDu8; 32 * 1024]);
1449 std::fs::write(&img, &image_data).unwrap();
1450 std::fs::write(&dev, &device_data).unwrap();
1451
1452 let (tx, _rx) = make_channel();
1453 let result = verify(
1454 img.to_str().unwrap(),
1455 dev.to_str().unwrap(),
1456 image_data.len() as u64,
1457 &tx,
1458 );
1459 assert!(
1460 result.is_ok(),
1461 "should pass when first N bytes match: {result:?}"
1462 );
1463
1464 let _ = std::fs::remove_file(img);
1465 let _ = std::fs::remove_file(dev);
1466 }
1467
1468 #[test]
1471 fn test_pipeline_rejects_missing_image() {
1472 let (tx, rx) = make_channel();
1473 let cancel = Arc::new(AtomicBool::new(false));
1474 run_pipeline("/nonexistent/image.iso", "/dev/null", tx, cancel);
1475 let events = drain(&rx);
1476 let err = find_error(&events);
1477 assert!(err.is_some(), "must emit an Error event");
1478 assert!(err.unwrap().contains("Image file not found"), "err={err:?}");
1479 }
1480
1481 #[test]
1482 fn test_pipeline_rejects_empty_image() {
1483 let dir = std::env::temp_dir();
1484 let empty = dir.join("fk_empty.img");
1485 std::fs::write(&empty, b"").unwrap();
1486
1487 let (tx, rx) = make_channel();
1488 let cancel = Arc::new(AtomicBool::new(false));
1489 run_pipeline(empty.to_str().unwrap(), "/dev/null", tx, cancel);
1490
1491 let events = drain(&rx);
1492 let err = find_error(&events);
1493 assert!(err.is_some());
1494 assert!(err.unwrap().contains("empty"), "err={err:?}");
1495
1496 let _ = std::fs::remove_file(empty);
1497 }
1498
1499 #[test]
1500 fn test_pipeline_rejects_missing_device() {
1501 let dir = std::env::temp_dir();
1502 let img = dir.join("fk_nodev_img.bin");
1503 std::fs::write(&img, vec![0u8; 1024]).unwrap();
1504
1505 let (tx, rx) = make_channel();
1506 let cancel = Arc::new(AtomicBool::new(false));
1507 run_pipeline(img.to_str().unwrap(), "/nonexistent/device", tx, cancel);
1508
1509 let events = drain(&rx);
1510 let err = find_error(&events);
1511 assert!(err.is_some());
1512 assert!(
1513 err.unwrap().contains("Target device not found"),
1514 "err={err:?}"
1515 );
1516
1517 let _ = std::fs::remove_file(img);
1518 }
1519
1520 #[test]
1522 fn test_pipeline_end_to_end_temp_files() {
1523 let dir = std::env::temp_dir();
1524 let img = dir.join("fk_e2e_img.bin");
1525 let dev = dir.join("fk_e2e_dev.bin");
1526
1527 let image_data: Vec<u8> = (0u8..=255u8).cycle().take(1024 * 1024).collect();
1528 std::fs::write(&img, &image_data).unwrap();
1529 std::fs::File::create(&dev).unwrap();
1530
1531 let (tx, rx) = make_channel();
1532 let cancel = Arc::new(AtomicBool::new(false));
1533 run_pipeline(img.to_str().unwrap(), dev.to_str().unwrap(), tx, cancel);
1534
1535 let events = drain(&rx);
1536
1537 let has_progress = events
1539 .iter()
1540 .any(|e| matches!(e, FlashEvent::Progress { .. }));
1541 assert!(has_progress, "must emit Progress events");
1542
1543 assert!(
1545 has_stage(&events, &FlashStage::Unmounting),
1546 "must emit Unmounting stage"
1547 );
1548 assert!(
1549 has_stage(&events, &FlashStage::Writing),
1550 "must emit Writing stage"
1551 );
1552 assert!(
1553 has_stage(&events, &FlashStage::Syncing),
1554 "must emit Syncing stage"
1555 );
1556
1557 let has_done = events.iter().any(|e| matches!(e, FlashEvent::Done));
1560 let has_error = events.iter().any(|e| matches!(e, FlashEvent::Error(_)));
1561 assert!(
1562 has_done || has_error,
1563 "pipeline must end with Done or Error"
1564 );
1565
1566 if has_done {
1567 let written = std::fs::read(&dev).unwrap();
1568 assert_eq!(written, image_data, "written data must match image");
1569 } else if let Some(err_msg) = find_error(&events) {
1570 assert!(
1572 !err_msg.contains("Cannot open")
1573 && !err_msg.contains("Verification failed")
1574 && !err_msg.contains("Write error"),
1575 "unexpected error: {err_msg}"
1576 );
1577 }
1578
1579 let _ = std::fs::remove_file(img);
1580 let _ = std::fs::remove_file(dev);
1581 }
1582
1583 #[test]
1586 fn test_flash_stage_display() {
1587 assert!(FlashStage::Writing.to_string().contains("Writing"));
1588 assert!(FlashStage::Syncing.to_string().contains("Flushing"));
1589 assert!(FlashStage::Done.to_string().contains("complete"));
1590 assert!(FlashStage::Failed("oops".into())
1591 .to_string()
1592 .contains("oops"));
1593 }
1594
1595 #[test]
1598 fn test_flash_stage_eq() {
1599 assert_eq!(FlashStage::Writing, FlashStage::Writing);
1600 assert_ne!(FlashStage::Writing, FlashStage::Syncing);
1601 assert_eq!(
1602 FlashStage::Failed("x".into()),
1603 FlashStage::Failed("x".into())
1604 );
1605 assert_ne!(
1606 FlashStage::Failed("x".into()),
1607 FlashStage::Failed("y".into())
1608 );
1609 }
1610
1611 #[test]
1614 fn test_flash_event_clone() {
1615 let events = vec![
1616 FlashEvent::Stage(FlashStage::Writing),
1617 FlashEvent::Progress {
1618 bytes_written: 1024,
1619 total_bytes: 4096,
1620 speed_mb_s: 12.5,
1621 },
1622 FlashEvent::Log("hello".into()),
1623 FlashEvent::Done,
1624 FlashEvent::Error("boom".into()),
1625 ];
1626 for e in &events {
1627 let _ = e.clone(); }
1629 }
1630
1631 #[test]
1636 fn test_find_mounted_partitions_nonexistent_device_returns_empty() {
1637 #[cfg(target_os = "windows")]
1639 let result = find_mounted_partitions("PhysicalDrive999", r"\\.\PhysicalDrive999");
1640 #[cfg(not(target_os = "windows"))]
1641 let result = find_mounted_partitions("sdzzz", "/dev/sdzzz");
1642
1643 let _ = result;
1645 }
1646
1647 #[test]
1650 fn test_find_mounted_partitions_empty_name_no_panic() {
1651 let result = find_mounted_partitions("", "");
1652 let _ = result;
1653 }
1654
1655 #[test]
1660 fn test_is_partition_of_windows_style_paths() {
1661 assert!(!is_partition_of(r"\\.\PhysicalDrive0", "PhysicalDrive0"));
1663 assert!(!is_partition_of(r"\\.\PhysicalDrive1", "PhysicalDrive0"));
1664 }
1665
1666 #[test]
1671 fn test_pipeline_emits_syncing_stage() {
1672 let dir = std::env::temp_dir();
1673 let img = dir.join("fk_sync_stage_img.bin");
1674 let dev = dir.join("fk_sync_stage_dev.bin");
1675
1676 let data: Vec<u8> = (0u8..=255).cycle().take(512 * 1024).collect();
1677 std::fs::write(&img, &data).unwrap();
1678 std::fs::File::create(&dev).unwrap();
1679
1680 let (tx, rx) = make_channel();
1681 let cancel = Arc::new(AtomicBool::new(false));
1682 run_pipeline(img.to_str().unwrap(), dev.to_str().unwrap(), tx, cancel);
1683
1684 let events = drain(&rx);
1685 assert!(
1686 has_stage(&events, &FlashStage::Syncing),
1687 "Syncing stage must be emitted on every platform"
1688 );
1689
1690 let _ = std::fs::remove_file(&img);
1691 let _ = std::fs::remove_file(&dev);
1692 }
1693
1694 #[test]
1696 fn test_pipeline_emits_rereading_stage() {
1697 let dir = std::env::temp_dir();
1698 let img = dir.join("fk_reread_stage_img.bin");
1699 let dev = dir.join("fk_reread_stage_dev.bin");
1700
1701 let data: Vec<u8> = vec![0xABu8; 256 * 1024];
1702 std::fs::write(&img, &data).unwrap();
1703 std::fs::File::create(&dev).unwrap();
1704
1705 let (tx, rx) = make_channel();
1706 let cancel = Arc::new(AtomicBool::new(false));
1707 run_pipeline(img.to_str().unwrap(), dev.to_str().unwrap(), tx, cancel);
1708
1709 let events = drain(&rx);
1710 assert!(
1711 has_stage(&events, &FlashStage::Rereading),
1712 "Rereading stage must be emitted on every platform"
1713 );
1714
1715 let _ = std::fs::remove_file(&img);
1716 let _ = std::fs::remove_file(&dev);
1717 }
1718
1719 #[test]
1721 fn test_pipeline_emits_verifying_stage() {
1722 let dir = std::env::temp_dir();
1723 let img = dir.join("fk_verify_stage_img.bin");
1724 let dev = dir.join("fk_verify_stage_dev.bin");
1725
1726 let data: Vec<u8> = vec![0xCDu8; 256 * 1024];
1727 std::fs::write(&img, &data).unwrap();
1728 std::fs::File::create(&dev).unwrap();
1729
1730 let (tx, rx) = make_channel();
1731 let cancel = Arc::new(AtomicBool::new(false));
1732 run_pipeline(img.to_str().unwrap(), dev.to_str().unwrap(), tx, cancel);
1733
1734 let events = drain(&rx);
1735 assert!(
1736 has_stage(&events, &FlashStage::Verifying),
1737 "Verifying stage must be emitted on every platform"
1738 );
1739
1740 let _ = std::fs::remove_file(&img);
1741 let _ = std::fs::remove_file(&dev);
1742 }
1743
1744 #[test]
1749 fn test_open_device_for_writing_nonexistent_mentions_path() {
1750 let bad = if cfg!(target_os = "windows") {
1751 r"\\.\PhysicalDrive999".to_string()
1752 } else {
1753 "/nonexistent/fk_bad_device".to_string()
1754 };
1755
1756 let dir = std::env::temp_dir();
1758 let img = dir.join("fk_open_err_img.bin");
1759 std::fs::write(&img, vec![1u8; 512]).unwrap();
1760
1761 let (tx, _rx) = make_channel();
1762 let cancel = Arc::new(AtomicBool::new(false));
1763 let result = write_image(img.to_str().unwrap(), &bad, 512, &tx, &cancel);
1764
1765 assert!(result.is_err(), "must fail for nonexistent device");
1766 assert!(
1768 result.as_ref().unwrap_err().contains("PhysicalDrive999")
1769 || result.as_ref().unwrap_err().contains("fk_bad_device")
1770 || result.as_ref().unwrap_err().contains("Cannot open"),
1771 "error should reference the bad path: {:?}",
1772 result
1773 );
1774
1775 let _ = std::fs::remove_file(&img);
1776 }
1777
1778 #[test]
1783 fn test_sync_device_emits_log() {
1784 let dir = std::env::temp_dir();
1785 let dev = dir.join("fk_sync_log_dev.bin");
1786 std::fs::File::create(&dev).unwrap();
1787
1788 let (tx, rx) = make_channel();
1789 sync_device(dev.to_str().unwrap(), &tx);
1790
1791 let events = drain(&rx);
1792 let has_flush_log = events.iter().any(|e| {
1793 if let FlashEvent::Log(msg) = e {
1794 let lower = msg.to_lowercase();
1795 lower.contains("flush") || lower.contains("cache")
1796 } else {
1797 false
1798 }
1799 });
1800 assert!(
1801 has_flush_log,
1802 "sync_device must emit a flush/cache log event"
1803 );
1804
1805 let _ = std::fs::remove_file(&dev);
1806 }
1807
1808 #[test]
1813 fn test_reread_partition_table_emits_log() {
1814 let dir = std::env::temp_dir();
1815 let dev = dir.join("fk_reread_log_dev.bin");
1816 std::fs::File::create(&dev).unwrap();
1817
1818 let (tx, rx) = make_channel();
1819 reread_partition_table(dev.to_str().unwrap(), &tx);
1820
1821 let events = drain(&rx);
1822 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
1823 assert!(
1824 has_log,
1825 "reread_partition_table must emit at least one Log event"
1826 );
1827
1828 let _ = std::fs::remove_file(&dev);
1829 }
1830
1831 #[test]
1836 fn test_unmount_device_no_partitions_emits_log() {
1837 let dir = std::env::temp_dir();
1838 let dev = dir.join("fk_unmount_log_dev.bin");
1839 std::fs::File::create(&dev).unwrap();
1840
1841 let path_str = dev.to_str().unwrap();
1842 let (tx, rx) = make_channel();
1843 unmount_device(path_str, &tx);
1844
1845 let events = drain(&rx);
1846 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
1848 assert!(has_log, "unmount_device must emit at least one Log event");
1849
1850 let _ = std::fs::remove_file(&dev);
1851 }
1852
1853 #[test]
1858 fn test_pipeline_stage_ordering() {
1859 let dir = std::env::temp_dir();
1860 let img = dir.join("fk_order_img.bin");
1861 let dev = dir.join("fk_order_dev.bin");
1862
1863 let data: Vec<u8> = (0u8..=255).cycle().take(256 * 1024).collect();
1864 std::fs::write(&img, &data).unwrap();
1865 std::fs::File::create(&dev).unwrap();
1866
1867 let (tx, rx) = make_channel();
1868 let cancel = Arc::new(AtomicBool::new(false));
1869 run_pipeline(img.to_str().unwrap(), dev.to_str().unwrap(), tx, cancel);
1870
1871 let events = drain(&rx);
1872
1873 let stages: Vec<&FlashStage> = events
1875 .iter()
1876 .filter_map(|e| {
1877 if let FlashEvent::Stage(s) = e {
1878 Some(s)
1879 } else {
1880 None
1881 }
1882 })
1883 .collect();
1884
1885 let pos = |target: &FlashStage| {
1887 stages
1888 .iter()
1889 .position(|s| *s == target)
1890 .unwrap_or(usize::MAX)
1891 };
1892
1893 let unmounting = pos(&FlashStage::Unmounting);
1894 let writing = pos(&FlashStage::Writing);
1895 let syncing = pos(&FlashStage::Syncing);
1896 let rereading = pos(&FlashStage::Rereading);
1897 let verifying = pos(&FlashStage::Verifying);
1898
1899 assert!(unmounting < writing, "Unmounting must precede Writing");
1900 assert!(writing < syncing, "Writing must precede Syncing");
1901 assert!(syncing < rereading, "Syncing must precede Rereading");
1902 assert!(rereading < verifying, "Rereading must precede Verifying");
1903
1904 let _ = std::fs::remove_file(&img);
1905 let _ = std::fs::remove_file(&dev);
1906 }
1907
1908 #[test]
1913 #[cfg(target_os = "linux")]
1914 fn test_find_mounted_partitions_linux_no_panic() {
1915 let result = find_mounted_partitions("sda", "/dev/sda");
1917 let _ = result;
1918 }
1919
1920 #[test]
1924 #[cfg(target_os = "linux")]
1925 fn test_find_mounted_partitions_linux_reads_proc_mounts() {
1926 let content = std::fs::read_to_string("/proc/mounts").unwrap_or_default();
1929 if !content.is_empty() {
1931 if let Some(line) = content.lines().find(|l| l.starts_with("/dev/")) {
1934 if let Some(dev) = line.split_whitespace().next() {
1935 let name = std::path::Path::new(dev)
1936 .file_name()
1937 .map(|n| n.to_string_lossy().to_string())
1938 .unwrap_or_default();
1939 let _ = find_mounted_partitions(&name, dev);
1940 }
1941 }
1942 }
1943 }
1944
1945 #[test]
1948 #[cfg(target_os = "linux")]
1949 fn test_do_unmount_not_mounted_emits_warning() {
1950 let (tx, rx) = make_channel();
1951 do_unmount("/dev/fk_nonexistent_part", &tx);
1952 let events = drain(&rx);
1953 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
1956 assert!(has_log, "do_unmount must emit a Log event on failure");
1957 }
1958
1959 #[test]
1964 #[cfg(target_os = "macos")]
1965 fn test_do_unmount_macos_bad_path_emits_warning() {
1966 let (tx, rx) = make_channel();
1967 do_unmount("/dev/fk_nonexistent_part", &tx);
1968 let events = drain(&rx);
1969 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
1970 assert!(has_log, "do_unmount must emit a Log event on failure");
1971 }
1972
1973 #[test]
1976 #[cfg(target_os = "macos")]
1977 fn test_find_mounted_partitions_macos_no_panic() {
1978 let result = find_mounted_partitions("disk2", "/dev/disk2");
1979 let _ = result;
1980 }
1981
1982 #[test]
1985 #[cfg(target_os = "macos")]
1986 fn test_reread_partition_table_macos_emits_log() {
1987 let dir = std::env::temp_dir();
1988 let dev = dir.join("fk_macos_reread_dev.bin");
1989 std::fs::File::create(&dev).unwrap();
1990
1991 let (tx, rx) = make_channel();
1992 reread_partition_table(dev.to_str().unwrap(), &tx);
1993
1994 let events = drain(&rx);
1995 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
1996 assert!(has_log, "reread_partition_table must emit a log on macOS");
1997
1998 let _ = std::fs::remove_file(&dev);
1999 }
2000
2001 #[test]
2007 #[cfg(target_os = "windows")]
2008 fn test_find_mounted_partitions_windows_nonexistent() {
2009 let result = find_mounted_partitions("PhysicalDrive999", r"\\.\PhysicalDrive999");
2010 assert!(
2011 result.is_empty(),
2012 "nonexistent physical drive should have no volumes"
2013 );
2014 }
2015
2016 #[test]
2019 #[cfg(target_os = "windows")]
2020 fn test_do_unmount_windows_bad_volume_emits_log() {
2021 let (tx, rx) = make_channel();
2022 do_unmount(r"\\.\Z99:", &tx);
2023 let events = drain(&rx);
2024 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
2025 assert!(has_log, "do_unmount on bad volume must emit a Log event");
2026 }
2027
2028 #[test]
2031 #[cfg(target_os = "windows")]
2032 fn test_sync_device_windows_bad_path_no_panic() {
2033 let (tx, rx) = make_channel();
2034 sync_device(r"\\.\PhysicalDrive999", &tx);
2035 let events = drain(&rx);
2036 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
2039 assert!(has_log, "sync_device must emit a Log event on Windows");
2040 }
2041
2042 #[test]
2045 #[cfg(target_os = "windows")]
2046 fn test_reread_partition_table_windows_bad_path_no_panic() {
2047 let (tx, rx) = make_channel();
2048 reread_partition_table(r"\\.\PhysicalDrive999", &tx);
2049 let events = drain(&rx);
2050 let has_log = events.iter().any(|e| matches!(e, FlashEvent::Log(_)));
2051 assert!(
2052 has_log,
2053 "reread_partition_table must emit a Log event on Windows"
2054 );
2055 }
2056
2057 #[test]
2060 #[cfg(target_os = "windows")]
2061 fn test_open_device_for_writing_windows_access_denied_message() {
2062 let dir = std::env::temp_dir();
2063 let img = dir.join("fk_win_open_img.bin");
2064 std::fs::write(&img, vec![1u8; 512]).unwrap();
2065
2066 let (tx, _rx) = make_channel();
2067 let cancel = Arc::new(AtomicBool::new(false));
2068 let result = write_image(
2069 img.to_str().unwrap(),
2070 r"\\.\PhysicalDrive999",
2071 512,
2072 &tx,
2073 &cancel,
2074 );
2075
2076 assert!(result.is_err());
2077 let msg = result.unwrap_err();
2078 assert!(
2080 msg.contains("PhysicalDrive999")
2081 || msg.contains("Access denied")
2082 || msg.contains("Cannot open"),
2083 "error must be descriptive: {msg}"
2084 );
2085
2086 let _ = std::fs::remove_file(&img);
2087 }
2088}