use libfreemkv::ContentFormat;
use libfreemkv::Disc;
use libfreemkv::DiscFormat;
use libfreemkv::disc::CopyOptions;
use libfreemkv::disc::DiscRegion;
use libfreemkv::disc::mapfile::{Mapfile, SectorStatus};
use libfreemkv::error::{Error, Result};
use libfreemkv::scsi;
use libfreemkv::{ScsiSense, SectorSource};
use std::sync::{Arc, Mutex};
const SECTOR_SIZE: usize = 2048;
#[derive(Debug, Clone, Copy)]
enum ScriptStep {
Ok,
Err { sense_key: u8, asc: u8, ascq: u8 },
}
struct ScriptedSectorReader {
capacity: u32,
script: std::collections::HashMap<u32, Vec<ScriptStep>>,
attempt_idx: Mutex<std::collections::HashMap<u32, usize>>,
trace: Arc<Mutex<Vec<(u32, u16, bool)>>>,
}
impl ScriptedSectorReader {
fn new(capacity: u32) -> (Self, Arc<Mutex<Vec<(u32, u16, bool)>>>) {
let trace = Arc::new(Mutex::new(Vec::new()));
(
Self {
capacity,
script: std::collections::HashMap::new(),
attempt_idx: Mutex::new(std::collections::HashMap::new()),
trace: trace.clone(),
},
trace,
)
}
fn always(&mut self, lba: u32, step: ScriptStep) {
self.script.insert(lba, vec![step]);
}
#[allow(dead_code)]
fn sequence(&mut self, lba: u32, steps: Vec<ScriptStep>) {
self.script.insert(lba, steps);
}
fn step_for(&self, lba: u32) -> ScriptStep {
let v = match self.script.get(&lba) {
Some(v) => v,
None => return ScriptStep::Ok,
};
let mut idx = self.attempt_idx.lock().unwrap();
let i = idx.entry(lba).or_insert(0);
let step = v[(*i).min(v.len() - 1)];
*i += 1;
step
}
}
impl SectorSource for ScriptedSectorReader {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let mut failure: Option<(u8, u8, u8)> = None;
for offset in 0..count as u32 {
match self.step_for(lba + offset) {
ScriptStep::Ok => {}
ScriptStep::Err {
sense_key,
asc,
ascq,
} => {
failure = Some((sense_key, asc, ascq));
break;
}
}
}
let ok = failure.is_none();
self.trace.lock().unwrap().push((lba, count, ok));
if let Some((sense_key, asc, ascq)) = failure {
return Err(Error::ScsiError {
opcode: scsi::SCSI_READ_10,
status: scsi::SCSI_STATUS_CHECK_CONDITION,
sense: Some(ScsiSense {
sense_key,
asc,
ascq,
}),
});
}
for (i, chunk) in buf.chunks_mut(SECTOR_SIZE).enumerate() {
chunk.fill(((lba + i as u32) & 0xff) as u8);
}
Ok(buf.len())
}
fn capacity_sectors(&self) -> u32 {
self.capacity
}
}
fn synthetic_disc(capacity_sectors: u32) -> Disc {
Disc {
volume_id: String::new(),
meta_title: None,
format: DiscFormat::BluRay,
capacity_sectors,
capacity_bytes: capacity_sectors as u64 * SECTOR_SIZE as u64,
layers: 1,
titles: Vec::new(),
region: DiscRegion::Free,
aacs: None,
css: None,
encrypted: false,
aacs_error: None,
content_format: ContentFormat::BdTs,
}
}
fn prep_iso_and_mapfile(
iso_path: &std::path::Path,
total_bytes: u64,
finished_ranges: &[(u64, u64)],
nontrimmed_ranges: &[(u64, u64)],
) {
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(iso_path)
.unwrap();
f.set_len(total_bytes).unwrap();
f.seek(SeekFrom::Start(0)).unwrap();
f.write_all(&[]).unwrap();
let map_path = libfreemkv::disc::mapfile_path_for(iso_path);
let mut mf = Mapfile::create(&map_path, total_bytes, "test").unwrap();
for &(pos, size) in finished_ranges {
mf.record(pos, size, SectorStatus::Finished).unwrap();
}
for &(pos, size) in nontrimmed_ranges {
mf.record(pos, size, SectorStatus::NonTrimmed).unwrap();
}
}
#[derive(Debug, PartialEq, Eq)]
struct Golden {
bytes_good: u64,
bytes_unreadable: u64,
bytes_pending: u64,
wedged_exit: bool,
max_reads: usize,
}
fn run_profile(
profile_name: &str,
capacity_sectors: u32,
nontrimmed: &[(u64, u64)],
finished: &[(u64, u64)],
scripted: ScriptedSectorReader,
trace: Arc<Mutex<Vec<(u32, u16, bool)>>>,
) -> (
libfreemkv::disc::CopyResult,
libfreemkv::disc::mapfile::MapStats,
usize,
) {
let total_bytes: u64 = capacity_sectors as u64 * SECTOR_SIZE as u64;
let disc = synthetic_disc(capacity_sectors);
let tmp = tempfile::NamedTempFile::new().unwrap();
let iso_path = tmp.path().to_path_buf();
drop(tmp);
prep_iso_and_mapfile(&iso_path, total_bytes, finished, nontrimmed);
let opts = CopyOptions {
decrypt: false,
multipass: true,
..Default::default()
};
let mut reader = scripted;
let pr = disc
.copy(&mut reader, &iso_path, &opts)
.unwrap_or_else(|e| panic!("[{profile_name}] disc.copy returned Err: {e:?}"));
let map_path = libfreemkv::disc::mapfile_path_for(&iso_path);
let map = Mapfile::load(&map_path).unwrap();
let stats = map.stats();
let trace_len = trace.lock().unwrap().len();
let _ = std::fs::remove_file(&iso_path);
let _ = std::fs::remove_file(&map_path);
(pr, stats, trace_len)
}
#[test]
fn profile_01_clean_all_recoverable() {
let capacity_sectors: u32 = 256;
let (reader, trace) = ScriptedSectorReader::new(capacity_sectors);
let nontrimmed = [(100 * 2048, 16 * 2048)]; let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"01_clean",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
let expected = Golden {
bytes_good: capacity_sectors as u64 * 2048,
bytes_unreadable: 0,
bytes_pending: 0,
wedged_exit: false,
max_reads: 8, };
assert_eq!(stats.bytes_good, expected.bytes_good, "01_clean bytes_good");
assert_eq!(
stats.bytes_unreadable, expected.bytes_unreadable,
"01_clean bytes_unreadable"
);
assert_eq!(
stats.bytes_pending, expected.bytes_pending,
"01_clean bytes_pending"
);
assert!(!pr.halted, "01_clean halted");
assert!(
trace_len <= expected.max_reads,
"01_clean trace_len={trace_len} exceeds bound {}",
expected.max_reads
);
}
#[test]
fn profile_02_all_medium_error() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
for lba in 100..116 {
reader.always(
lba,
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
);
}
let nontrimmed = [(100 * 2048, 16 * 2048)];
let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"02_all_medium",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
assert_eq!(
stats.bytes_good,
(capacity_sectors as u64 - 16) * 2048,
"02_all_medium bytes_good"
);
assert_eq!(
stats.bytes_unreadable, 0,
"02_all_medium bytes_unreadable (must NOT be marked terminal in one pass)"
);
assert_eq!(
stats.bytes_pending,
16 * 2048,
"02_all_medium bytes_pending (NonTrimmed retained across passes)"
);
assert!(!pr.halted, "02_all_medium halted");
assert!(
trace_len <= 80,
"02_all_medium trace_len={trace_len} exceeds 80"
);
}
#[test]
fn profile_03_alternating_good_bad() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
for lba in (100..116).step_by(2) {
reader.always(
lba,
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
);
}
let nontrimmed = [(100 * 2048, 16 * 2048)];
let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"03_alternating",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
let good_total = stats.bytes_good;
let baseline_good = (capacity_sectors as u64 - 16) * 2048;
let middle_recovered = good_total - baseline_good;
assert!(
middle_recovered >= 6 * 2048,
"03_alternating recovered only {middle_recovered} bytes of 8 good sectors"
);
assert!(
middle_recovered <= 9 * 2048,
"03_alternating recovered MORE than scripted good sectors: {middle_recovered}"
);
assert_eq!(stats.bytes_unreadable, 0, "03_alternating bytes_unreadable");
assert!(
stats.bytes_pending > 0,
"03_alternating expected NonTrimmed remainder, got bytes_pending=0"
);
assert!(!pr.halted, "03_alternating halted");
assert!(
trace_len <= 120,
"03_alternating trace_len={trace_len} exceeds 120"
);
}
#[test]
fn profile_04_edge_bad_good_middle() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
for lba in 100..104 {
reader.always(
lba,
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
);
}
for lba in 112..116 {
reader.always(
lba,
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
);
}
let nontrimmed = [(100 * 2048, 16 * 2048)];
let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"04_edge_bad",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
let middle_recovered = stats.bytes_good - (capacity_sectors as u64 - 16) * 2048;
assert!(
middle_recovered >= 6 * 2048,
"04_edge_bad recovered only {middle_recovered} bytes of 8 good middle sectors"
);
assert_eq!(stats.bytes_unreadable, 0, "04_edge_bad bytes_unreadable");
assert!(
stats.bytes_pending > 0,
"04_edge_bad bytes_pending expected > 0"
);
assert!(!pr.halted, "04_edge_bad halted");
assert!(
trace_len <= 120,
"04_edge_bad trace_len={trace_len} exceeds 120"
);
}
#[test]
fn profile_05_single_bad_sector() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
reader.always(
108,
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
);
let nontrimmed = [(100 * 2048, 16 * 2048)];
let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"05_single_bad",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
assert_eq!(
stats.bytes_good,
(capacity_sectors as u64 - 1) * 2048,
"05_single_bad bytes_good"
);
assert_eq!(stats.bytes_unreadable, 0, "05_single_bad bytes_unreadable");
assert_eq!(stats.bytes_pending, 2048, "05_single_bad bytes_pending");
assert!(!pr.halted, "05_single_bad halted");
assert!(
trace_len <= 80,
"05_single_bad trace_len={trace_len} exceeds 80"
);
}
#[test]
fn profile_06_deep_pit() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
for lba in 108..116 {
reader.always(
lba,
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
);
}
let nontrimmed = [(100 * 2048, 24 * 2048)];
let finished = [
(0, 100 * 2048),
(124 * 2048, (capacity_sectors as u64 - 124) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"06_deep_pit",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
let recovered_in_range = stats.bytes_good - (capacity_sectors as u64 - 24) * 2048;
assert!(
recovered_in_range >= 14 * 2048,
"06_deep_pit recovered only {recovered_in_range} bytes of 16 good sectors"
);
assert_eq!(stats.bytes_unreadable, 0, "06_deep_pit bytes_unreadable");
assert!(
stats.bytes_pending > 0,
"06_deep_pit bytes_pending expected > 0"
);
assert!(!pr.halted, "06_deep_pit halted");
assert!(
trace_len <= 120,
"06_deep_pit trace_len={trace_len} exceeds 120"
);
}
#[test]
fn profile_07_medium_then_good() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
for lba in 105..110 {
reader.sequence(
lba,
vec![
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
ScriptStep::Ok,
],
);
}
let nontrimmed = [(100 * 2048, 16 * 2048)];
let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"07_medium_then_good",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
assert_eq!(
stats.bytes_good,
capacity_sectors as u64 * 2048,
"07_medium_then_good bytes_good — cache-prime should consume \
the failing script steps so the real read sees Ok"
);
assert_eq!(
stats.bytes_unreadable, 0,
"07_medium_then_good bytes_unreadable"
);
assert_eq!(stats.bytes_pending, 0, "07_medium_then_good bytes_pending");
assert!(!pr.halted, "07_medium_then_good halted");
assert!(
trace_len <= 100,
"07_medium_then_good trace_len={trace_len} exceeds 100"
);
}
#[test]
fn profile_08_batch_fail_singles_ok() {
let capacity_sectors: u32 = 256;
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
reader.sequence(
108,
vec![
ScriptStep::Err {
sense_key: scsi::SENSE_KEY_MEDIUM_ERROR,
asc: 0x11,
ascq: 0x00,
},
ScriptStep::Ok,
],
);
let nontrimmed = [(100 * 2048, 16 * 2048)];
let finished = [
(0, 100 * 2048),
(116 * 2048, (capacity_sectors as u64 - 116) * 2048),
];
let (pr, stats, trace_len) = run_profile(
"08_batch_fail",
capacity_sectors,
&nontrimmed,
&finished,
reader,
trace,
);
assert_eq!(
stats.bytes_good,
capacity_sectors as u64 * 2048,
"08_batch_fail bytes_good — second attempt should recover"
);
assert_eq!(stats.bytes_unreadable, 0, "08_batch_fail bytes_unreadable");
assert_eq!(stats.bytes_pending, 0, "08_batch_fail bytes_pending");
assert!(!pr.halted, "08_batch_fail halted");
assert!(
trace_len <= 80,
"08_batch_fail trace_len={trace_len} exceeds 80"
);
}