Two opt-in scan diagnostics for the DVD decrypted-image defect
dvd_placement_invariant_on_a_real_folder checks, per title set, the sum ifo.rs relies on: file_start_lba(VTS_nn_0.IFO) + vtstt_vobs must land on VTS_nn_1.VOB. It reports all 13 sets correct on a real DVD folder, which is what excluded placement as the cause. dump_titles_for_an_image prints every title a scan produces with the numbers canonical_title_order sorts on. It is what showed the real shape: the same disc scans to 38 titles as a CSS image and 10 once decrypted, with identical capacity and a byte-complete image. Both are #[ignore]d and read their target from the environment, so they cost the gate nothing and are there for whoever picks the defect up.
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
// Minimal ISO dumper — find exact stall point
|
||||
use libfreemkv::Drive;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("Usage: iso_dump <device> <output>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut drive = Drive::open(Path::new(&args[1])).unwrap();
|
||||
drive.wait_ready().unwrap();
|
||||
let _ = drive.init();
|
||||
let _ = drive.probe_disc();
|
||||
|
||||
// AACS handshake — required to read past the protected area
|
||||
eprint!("Scanning disc... ");
|
||||
let _ = libfreemkv::Disc::scan(&mut drive, &libfreemkv::ScanOptions::default());
|
||||
eprintln!("OK");
|
||||
|
||||
let cap = drive.read_capacity().unwrap();
|
||||
let batch = libfreemkv::disc::detect_max_batch_sectors(drive.device_path());
|
||||
|
||||
eprintln!("Device: {} | {} sectors | batch {}", args[1], cap, batch);
|
||||
|
||||
let file = std::fs::File::create(&args[2]).unwrap();
|
||||
let mut w = BufWriter::with_capacity(4 * 1024 * 1024, file);
|
||||
let mut buf = vec![0u8; batch as usize * 2048];
|
||||
let mut lba: u32 = 0;
|
||||
let start = Instant::now();
|
||||
let mut last = Instant::now();
|
||||
let mut bytes: u64 = 0;
|
||||
let mut last_bytes: u64 = 0;
|
||||
|
||||
while lba < cap {
|
||||
let count = ((cap - lba) as u16).min(batch);
|
||||
let n = count as usize * 2048;
|
||||
|
||||
// Tiny yield between reads — test if pacing prevents firmware throttle
|
||||
std::thread::yield_now();
|
||||
let t0 = Instant::now();
|
||||
let ok = drive.read(lba, count, &mut buf[..n], true).is_ok();
|
||||
let read_ms = t0.elapsed().as_millis();
|
||||
|
||||
// Flag slow reads
|
||||
if read_ms > 2000 {
|
||||
eprintln!("\n SLOW READ: LBA {} took {}ms (ok={})", lba, read_ms, ok);
|
||||
}
|
||||
|
||||
if !ok {
|
||||
buf[..n].fill(0);
|
||||
}
|
||||
w.write_all(&buf[..n]).unwrap();
|
||||
lba += count as u32;
|
||||
bytes += n as u64;
|
||||
|
||||
if last.elapsed().as_millis() >= 1000 {
|
||||
let delta = bytes - last_bytes;
|
||||
let speed = delta as f64 / last.elapsed().as_secs_f64() / 1_048_576.0;
|
||||
let avg = bytes as f64 / start.elapsed().as_secs_f64() / 1_048_576.0;
|
||||
let pct = bytes as f64 / (cap as f64 * 2048.0) * 100.0;
|
||||
eprint!(
|
||||
"\r {:.1}% LBA {} | {:.0} MB/s (avg {:.0}) | {:.1} GB ",
|
||||
pct,
|
||||
lba,
|
||||
speed,
|
||||
avg,
|
||||
bytes as f64 / 1e9
|
||||
);
|
||||
last_bytes = bytes;
|
||||
last = Instant::now();
|
||||
}
|
||||
}
|
||||
w.flush().unwrap();
|
||||
eprintln!(
|
||||
"\nDone: {:.1} GB in {:.0}s",
|
||||
bytes as f64 / 1e9,
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
@@ -734,3 +734,93 @@ fn write_and_mount_externally() {
|
||||
"the OS mounted the image but read back different bytes"
|
||||
);
|
||||
}
|
||||
|
||||
/// Diagnostic (opt-in): verify the DVD placement invariant for every title set
|
||||
/// in a REAL folder. `ifo.rs` derives a title's extents as
|
||||
/// `file_start_lba(VTS_nn_0.IFO) + vtstt_vobs`, so that sum must land exactly on
|
||||
/// `VTS_nn_1.VOB` or the mux reads the wrong sectors for that title.
|
||||
///
|
||||
/// Run with: `FMKV_DVD_FOLDER=/path/to/tree cargo test --lib
|
||||
/// dvd_placement_invariant_on_a_real_folder -- --ignored --nocapture`
|
||||
#[test]
|
||||
#[ignore = "diagnostic: needs FMKV_DVD_FOLDER pointing at a real VIDEO_TS tree"]
|
||||
fn dvd_placement_invariant_on_a_real_folder() {
|
||||
let Ok(dir) = std::env::var("FMKV_DVD_FOLDER") else {
|
||||
return;
|
||||
};
|
||||
let mut img = DirImage::open(std::path::Path::new(&dir)).expect("open");
|
||||
let fs = udf::read_filesystem(&mut img).expect("udf");
|
||||
let mut bad = 0;
|
||||
for n in 1..=25u32 {
|
||||
let ifo = format!("/VIDEO_TS/VTS_{n:02}_0.IFO");
|
||||
let vob = format!("/VIDEO_TS/VTS_{n:02}_1.VOB");
|
||||
let (Ok(ifo_lba), Ok(vob_lba)) = (
|
||||
fs.file_start_lba(&mut img, &ifo),
|
||||
fs.file_start_lba(&mut img, &vob),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let head = fs
|
||||
.read_file_prefix(&mut img, &ifo, 0xC8)
|
||||
.unwrap_or_default();
|
||||
if head.len() < 0xC8 {
|
||||
println!("VTS {n:02}: IFO shorter than 0xC8");
|
||||
continue;
|
||||
}
|
||||
let vtstt = u32::from_be_bytes([head[0xC4], head[0xC5], head[0xC6], head[0xC7]]);
|
||||
let want = ifo_lba + vtstt;
|
||||
if want != vob_lba {
|
||||
bad += 1;
|
||||
}
|
||||
println!(
|
||||
"VTS {n:02}: ifo={ifo_lba} vtstt={vtstt} want={want} vob={vob_lba} {}",
|
||||
if want == vob_lba { "ok" } else { "MISMATCH" }
|
||||
);
|
||||
}
|
||||
println!("{bad} title set(s) misplaced");
|
||||
assert_eq!(
|
||||
bad, 0,
|
||||
"the placement invariant must hold for every title set"
|
||||
);
|
||||
}
|
||||
|
||||
/// Diagnostic (opt-in): dump every title an image scan produces, with the
|
||||
/// numbers `canonical_title_order` actually sorts on.
|
||||
///
|
||||
/// Run: `FMKV_IMAGE=/path/to.iso cargo test --lib dump_titles_for_an_image
|
||||
/// -- --ignored --nocapture`
|
||||
#[test]
|
||||
#[ignore = "diagnostic: needs FMKV_IMAGE"]
|
||||
fn dump_titles_for_an_image() {
|
||||
let Ok(path) = std::env::var("FMKV_IMAGE") else {
|
||||
return;
|
||||
};
|
||||
let (disc, _r) = crate::session::scan_iso(
|
||||
std::path::Path::new(&path),
|
||||
crate::disc::ScanOptions::default(),
|
||||
)
|
||||
.expect("scan");
|
||||
println!(
|
||||
"capacity_bytes={} format={:?} css_error={:?} titles={}",
|
||||
disc.capacity_bytes,
|
||||
disc.format,
|
||||
disc.css.as_ref().map(|c| format!("{:?}", c.crack_span)),
|
||||
disc.titles.len()
|
||||
);
|
||||
for (i, t) in disc.titles.iter().enumerate() {
|
||||
println!(
|
||||
" [{i}] playlist={:<16} dur={:>8.2}s size={:>12} extents={} streams={}",
|
||||
t.playlist,
|
||||
t.duration_secs,
|
||||
t.size_bytes,
|
||||
t.extents.len(),
|
||||
t.streams.len()
|
||||
);
|
||||
for e in t.extents.iter().take(3) {
|
||||
println!(
|
||||
" extent lba={} sectors={}",
|
||||
e.start_lba, e.sector_count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user