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"
|
"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