From 1f70398774b288f667af845367048778616ba7af Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:33:58 -0700 Subject: [PATCH] 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. --- examples/iso_dump.rs | 83 --------------------------------------- src/dirimage/tests.rs | 90 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 83 deletions(-) delete mode 100644 examples/iso_dump.rs diff --git a/examples/iso_dump.rs b/examples/iso_dump.rs deleted file mode 100644 index e296bfd..0000000 --- a/examples/iso_dump.rs +++ /dev/null @@ -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 = std::env::args().collect(); - if args.len() < 3 { - eprintln!("Usage: iso_dump "); - 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() - ); -} diff --git a/src/dirimage/tests.rs b/src/dirimage/tests.rs index 33ba17b..2a7bff9 100644 --- a/src/dirimage/tests.rs +++ b/src/dirimage/tests.rs @@ -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 + ); + } + } +}