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:
Matthew Jackson
2026-08-05 19:33:58 -07:00
parent 35c5eedc20
commit 1f70398774
2 changed files with 90 additions and 83 deletions
-83
View File
@@ -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()
);
}
+90
View File
@@ -734,3 +734,93 @@ fn write_and_mount_externally() {
"the OS mounted the image but read back different bytes" "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
);
}
}
}