From 0596307c19f2495b5cdd4717ade4dc24d8b748c8 Mon Sep 17 00:00:00 2001 From: Matt Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:01:02 +0000 Subject: [PATCH] Add verify module: sector-by-sector disc health check --- src/lib.rs | 1 + src/verify.rs | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 src/verify.rs diff --git a/src/lib.rs b/src/lib.rs index d5d4b7c..a1a281b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,6 +90,7 @@ pub(crate) mod platform; pub mod profile; pub mod scsi; pub mod sector; +pub mod verify; pub(crate) mod speed; pub(crate) mod udf; diff --git a/src/verify.rs b/src/verify.rs new file mode 100644 index 0000000..cf7a8ad --- /dev/null +++ b/src/verify.rs @@ -0,0 +1,218 @@ +//! Disc sector verification — read every sector and classify health. + +use crate::disc::{Chapter, DiscTitle, Extent}; +use crate::sector::SectorReader; +use std::time::Instant; + +/// Health status of a single sector read. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SectorStatus { + /// Read succeeded on first attempt, under 500ms + Good, + /// Read succeeded but took longer than expected (>500ms per sector) + Slow, + /// Read failed initially but succeeded on retry + Recovered, + /// Read failed all attempts — unrecoverable + Bad, +} + +/// A contiguous range of bad or slow sectors. +#[derive(Debug, Clone)] +pub struct SectorRange { + pub start_lba: u32, + pub count: u32, + pub status: SectorStatus, + /// Byte offset from start of title + pub byte_offset: u64, +} + +/// Result of a disc verification scan. +#[derive(Debug, Clone)] +pub struct VerifyResult { + pub total_sectors: u64, + pub good: u64, + pub slow: u64, + pub recovered: u64, + pub bad: u64, + /// Contiguous ranges of non-good sectors + pub ranges: Vec, + pub elapsed_secs: f64, +} + +impl VerifyResult { + /// Percentage of sectors that are fully readable (good + slow + recovered). + pub fn readable_pct(&self) -> f64 { + if self.total_sectors == 0 { + return 100.0; + } + (self.total_sectors - self.bad) as f64 / self.total_sectors as f64 * 100.0 + } + + /// True if every sector read successfully. + pub fn is_perfect(&self) -> bool { + self.bad == 0 && self.recovered == 0 && self.slow == 0 + } + + /// Map a bad sector range to a chapter timestamp. + pub fn chapter_at_offset(chapters: &[Chapter], byte_offset: u64, duration_secs: f64, total_bytes: u64) -> Option<(usize, f64)> { + if total_bytes == 0 || chapters.is_empty() { + return None; + } + let time_secs = byte_offset as f64 / total_bytes as f64 * duration_secs; + let mut chapter_idx = 0; + for (i, ch) in chapters.iter().enumerate() { + if ch.time_secs <= time_secs { + chapter_idx = i; + } else { + break; + } + } + Some((chapter_idx + 1, time_secs)) + } +} + +/// Progress callback: (sectors_done, total_sectors, current_status) +pub type ProgressFn = Box; + +/// Verify all sectors in a title's extents. +/// Reads in batches for speed, falls back to single-sector on failure. +pub fn verify_title( + reader: &mut dyn SectorReader, + title: &DiscTitle, + batch_sectors: u16, + mut on_progress: Option, +) -> VerifyResult { + let start = Instant::now(); + let mut good: u64 = 0; + let mut slow: u64 = 0; + let mut recovered: u64 = 0; + let mut bad: u64 = 0; + let mut ranges: Vec = Vec::new(); + let mut sectors_done: u64 = 0; + let mut byte_offset: u64 = 0; + + let total_sectors: u64 = title.extents.iter().map(|e| e.sector_count as u64).sum(); + let mut buf = vec![0u8; batch_sectors as usize * 2048]; + + for ext in &title.extents { + let mut offset: u32 = 0; + while offset < ext.sector_count { + let remaining = ext.sector_count - offset; + let count = remaining.min(batch_sectors as u32) as u16; + let lba = ext.start_lba + offset; + let bytes = count as usize * 2048; + + let batch_start = Instant::now(); + let batch_ok = reader + .read_sectors(lba, count, &mut buf[..bytes]) + .is_ok(); + let batch_ms = batch_start.elapsed().as_millis(); + + if batch_ok { + // Batch succeeded — classify by speed + let status = if batch_ms > count as u128 * 500 { + SectorStatus::Slow + } else { + SectorStatus::Good + }; + + match status { + SectorStatus::Good => good += count as u64, + SectorStatus::Slow => { + slow += count as u64; + ranges.push(SectorRange { + start_lba: lba, + count: count as u32, + status: SectorStatus::Slow, + byte_offset, + }); + } + _ => {} + } + + sectors_done += count as u64; + if let Some(ref mut cb) = on_progress { + cb(sectors_done, total_sectors, status); + } + } else { + // Batch failed — test each sector individually + for i in 0..count { + let sector_lba = lba + i as u32; + let sector_offset = i as usize * 2048; + let sector_byte_offset = byte_offset + i as u64 * 2048; + + let s1 = Instant::now(); + let first_ok = reader + .read_sectors(sector_lba, 1, &mut buf[sector_offset..sector_offset + 2048]) + .is_ok(); + let s1_ms = s1.elapsed().as_millis(); + + let status = if first_ok && s1_ms <= 500 { + good += 1; + SectorStatus::Good + } else if first_ok { + slow += 1; + SectorStatus::Slow + } else { + // Retry once more + std::thread::sleep(std::time::Duration::from_secs(2)); + if reader + .read_sectors(sector_lba, 1, &mut buf[sector_offset..sector_offset + 2048]) + .is_ok() + { + recovered += 1; + SectorStatus::Recovered + } else { + bad += 1; + SectorStatus::Bad + } + }; + + if status != SectorStatus::Good { + // Merge with previous range if contiguous and same status + if let Some(last) = ranges.last_mut() { + if last.status == status + && last.start_lba + last.count == sector_lba + { + last.count += 1; + } else { + ranges.push(SectorRange { + start_lba: sector_lba, + count: 1, + status, + byte_offset: sector_byte_offset, + }); + } + } else { + ranges.push(SectorRange { + start_lba: sector_lba, + count: 1, + status, + byte_offset: sector_byte_offset, + }); + } + } + + sectors_done += 1; + if let Some(ref mut cb) = on_progress { + cb(sectors_done, total_sectors, status); + } + } + } + + offset += count as u32; + byte_offset += bytes as u64; + } + } + + VerifyResult { + total_sectors, + good, + slow, + recovered, + bad, + ranges, + elapsed_secs: start.elapsed().as_secs_f64(), + } +}