Files
libfreemkv/src/verify.rs
T
MattJackson f1926c38dc v0.20.1: delete SectorReader, extract Disc::patch, doc/stub cleanup
WO-2 (delete SectorReader trait):
- The 0.18 trait split into SectorSource (read-only) and SectorSink
  (write-only) is final; the legacy SectorReader alias was a bridge.
- Renames every internal &mut dyn SectorReader (~25 sites) to
  &mut dyn SectorSource. The trait method capacity() becomes
  capacity_sectors() with a default of 0 (preserves SectorReader's
  default-0 behavior).
- Deletes the SectorReader trait, its blanket-to-Source bridge, and
  the FileSectorReader type alias. Adds explicit forwarding impls
  for Box<dyn SectorSource> and &mut dyn SectorSource so generic
  decorators like DecryptingSectorSource<S: SectorSource> compose.

WO-3a (extract Disc::patch):
- Moves Disc::patch (1230 lines) and bytes_bad_in_title from
  disc/mod.rs into disc/patch.rs as a split inherent impl. Zero
  behavior change — pure mechanical relocation. disc/mod.rs drops
  from 3,945 to 2,714 LOC.

WO-6 (partial):
- Deletes src/labels/png_filenames.rs — was a 72-LOC stub with
  detect() returning false, never wired into the PARSERS registry.

project docs doc drift fixes (audited 2026-05-13):
- JUMP_BASE_SECTORS: 256→1024 (64 MB base for UHD, not 8 MB)
- PASSN_DAMAGE_THRESHOLD_PCT: 12→6
- PASSN_SKIP_SECTORS_BASE: 64→32
- MAX_RANGE_SECS=180: replaced by proportional range_sectors × 25,
  capped at RANGE_BUDGET_CAP_SECS=1800.
2026-05-13 11:36:55 -07:00

261 lines
9.4 KiB
Rust

//! Disc sector verification — read every sector and classify health.
use crate::disc::{Chapter, DiscTitle};
use crate::progress::Progress;
use crate::sector::SectorSource;
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<SectorRange>,
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))
}
}
/// Verify all sectors in a title's extents.
/// Reads in batches for speed, falls back to single-sector on failure.
/// The progress callback returns false to request early stop.
pub fn verify_title(
reader: &mut dyn SectorSource,
title: &DiscTitle,
batch_sectors: u16,
on_progress: Option<&dyn Progress>,
) -> 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<SectorRange> = 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];
'outer: 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], false)
.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(cb) = on_progress {
let pp = crate::progress::PassProgress {
kind: crate::progress::PassKind::Verify,
work_done: sectors_done,
work_total: total_sectors,
bytes_good_total: (good + slow + recovered) * 2048,
bytes_unreadable_total: bad * 2048,
bytes_pending_total: 0,
bytes_total_disc: total_sectors * 2048,
disc_duration_secs: Some(title.duration_secs),
bytes_bad_in_main_title: 0,
main_title_duration_secs: Some(title.duration_secs),
main_title_size_bytes: Some(total_sectors * 2048),
};
if !cb.report(&pp) {
break 'outer;
}
}
} 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],
false,
)
.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 after brief pause
std::thread::sleep(std::time::Duration::from_secs(2));
if reader
.read_sectors(
sector_lba,
1,
&mut buf[sector_offset..sector_offset + 2048],
false,
)
.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(cb) = on_progress {
let pp = crate::progress::PassProgress {
kind: crate::progress::PassKind::Verify,
work_done: sectors_done,
work_total: total_sectors,
bytes_good_total: (good + slow + recovered) * 2048,
bytes_unreadable_total: bad * 2048,
bytes_pending_total: 0,
bytes_total_disc: total_sectors * 2048,
disc_duration_secs: Some(title.duration_secs),
bytes_bad_in_main_title: 0,
main_title_duration_secs: Some(title.duration_secs),
main_title_size_bytes: Some(total_sectors * 2048),
};
if !cb.report(&pp) {
break 'outer;
}
}
}
}
offset += count as u32;
byte_offset += bytes as u64;
}
}
VerifyResult {
total_sectors,
good,
slow,
recovered,
bad,
ranges,
elapsed_secs: start.elapsed().as_secs_f64(),
}
}