audit: lock DTS rate table, fix sniff overflow-scan, cover decrypt loss
Round-10 findings from the 10-phase release audit: - A finder claimed the DTS SFREQ→rate table was wrong at 11/12; verified it against ffmpeg's avpriv_dca_sample_rates (12k/24k/48k/96k/192k at 11-15) — the table is CORRECT. Added a test that locks the full table so it can't be mis-"fixed". - sniff_video_codec advanced 3 bytes after a matched start code, re-reading the code byte as an overlapping start code; skip the full 4-byte marker. - Guard the HD-DVD next_id title counter with saturating_add so a crafted disc with >65536 clips can't overflow (panic in debug). - Add a test that an undecryptable unit (DecryptFailed) is zero-filled and counted as loss through ExtractResult (complete=false, bytes_lost>0) — the recovery-seam consolidation folded that bucket into bytes_unreadable.
This commit is contained in:
+66
-1
@@ -832,8 +832,11 @@ mod tests {
|
||||
|
||||
struct MemDisc {
|
||||
sectors: HashMap<u32, [u8; 2048]>,
|
||||
/// Absolute LBAs that fail to read (bad-sector fixture).
|
||||
/// Absolute LBAs that fail to read (bad-sector fixture → DiscRead).
|
||||
bad: std::collections::HashSet<u32>,
|
||||
/// Absolute LBAs whose read fails to DECRYPT (no/wrong key fixture →
|
||||
/// DecryptFailed), exercising the undecryptable-unit loss path.
|
||||
decrypt_fail: std::collections::HashSet<u32>,
|
||||
}
|
||||
|
||||
impl MemDisc {
|
||||
@@ -841,6 +844,7 @@ mod tests {
|
||||
Self {
|
||||
sectors: HashMap::new(),
|
||||
bad: std::collections::HashSet::new(),
|
||||
decrypt_fail: std::collections::HashSet::new(),
|
||||
}
|
||||
}
|
||||
fn put(&mut self, lba: u32, data: [u8; 2048]) {
|
||||
@@ -872,6 +876,9 @@ mod tests {
|
||||
sense: None,
|
||||
});
|
||||
}
|
||||
if self.decrypt_fail.contains(&(lba + i)) {
|
||||
return Err(Error::DecryptFailed);
|
||||
}
|
||||
}
|
||||
for i in 0..count as u32 {
|
||||
let off = i as usize * 2048;
|
||||
@@ -1393,6 +1400,64 @@ mod tests {
|
||||
assert_eq!(res.files[0].bytes_unreadable, good.len() as u64);
|
||||
}
|
||||
|
||||
/// An UNDECRYPTABLE unit (DecryptFailed — wrong/missing key) is zero-filled
|
||||
/// and counted as loss through the public API exactly like a bad sector:
|
||||
/// the recovery-seam consolidation folded the old bytes_undecryptable bucket
|
||||
/// into bytes_unreadable, and the run must still report complete == false and
|
||||
/// bytes_lost() > 0 (this gates the CLI exit code / multipass re-run).
|
||||
#[test]
|
||||
fn undecryptable_unit_holes_file_and_accounts_loss() {
|
||||
let good = vec![0x55u8; 4 * 2048];
|
||||
let root = DirSpec {
|
||||
name: String::new(),
|
||||
icb_lba: 10,
|
||||
dir_data_lba: 11,
|
||||
files: Vec::new(),
|
||||
subdirs: vec![DirSpec {
|
||||
name: "BDMV".to_string(),
|
||||
icb_lba: 20,
|
||||
dir_data_lba: 21,
|
||||
files: Vec::new(),
|
||||
subdirs: vec![DirSpec {
|
||||
name: "STREAM".to_string(),
|
||||
icb_lba: 22,
|
||||
dir_data_lba: 23,
|
||||
files: vec![file("00001.m2ts", 24, 5000, good.clone(), true)],
|
||||
subdirs: vec![],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
let mut disc = build_disc(root);
|
||||
// The whole extent fails to decrypt (no/wrong key) rather than to read.
|
||||
for i in 0..4u32 {
|
||||
disc.decrypt_fail.insert(PART_START + 5000 + i);
|
||||
}
|
||||
let out = TmpDir::new("decryptfail");
|
||||
let res = clear_disc()
|
||||
.extract_tree(&mut disc, out.path(), &ExtractOptions::default())
|
||||
.expect("extract does not abort on an undecryptable unit");
|
||||
let got = read_out(out.path(), "BDMV/STREAM/00001.m2ts").expect("file written");
|
||||
assert_eq!(
|
||||
got.len(),
|
||||
good.len(),
|
||||
"holed file still sized to declared size"
|
||||
);
|
||||
assert!(
|
||||
got.iter().all(|&b| b == 0),
|
||||
"undecryptable range zero-filled"
|
||||
);
|
||||
assert!(
|
||||
!res.complete,
|
||||
"an undecryptable unit makes the rip incomplete"
|
||||
);
|
||||
assert!(
|
||||
res.bytes_lost() > 0,
|
||||
"decrypt loss counted, not reported clean"
|
||||
);
|
||||
assert_eq!(res.bytes_unreadable, good.len() as u64);
|
||||
assert_eq!(res.files[0].bytes_unreadable, good.len() as u64);
|
||||
}
|
||||
|
||||
/// Path sanitization rejects a host-illegal component in a disc file name.
|
||||
#[test]
|
||||
fn sanitize_rejects_illegal_component() {
|
||||
|
||||
+5
-3
@@ -152,7 +152,9 @@ fn sniff_video_codec(es: &[u8]) -> Option<Codec> {
|
||||
_ if (code & 0x9F) == 0x07 => saw_h264_sps = true,
|
||||
_ => {}
|
||||
}
|
||||
i += 3;
|
||||
// Skip the whole consumed `00 00 01 <code>` marker (4 bytes) so the
|
||||
// code byte isn't re-read as the start of an overlapping start code.
|
||||
i += 4;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
@@ -423,7 +425,7 @@ impl Disc {
|
||||
content_format: ContentFormat::MpegPs,
|
||||
codec_privates: Vec::new(),
|
||||
});
|
||||
next_id += 1;
|
||||
next_id = next_id.saturating_add(1);
|
||||
}
|
||||
|
||||
// Every remaining clip is its own title (unchanged behaviour). Iterated in
|
||||
@@ -461,7 +463,7 @@ impl Disc {
|
||||
content_format: ContentFormat::MpegPs,
|
||||
codec_privates: Vec::new(),
|
||||
});
|
||||
next_id += 1;
|
||||
next_id = next_id.saturating_add(1);
|
||||
}
|
||||
titles
|
||||
}
|
||||
|
||||
@@ -918,6 +918,37 @@ mod tests {
|
||||
assert_eq!(dts_core_sample_rate(&core), 48_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dts_core_sfreq_table_matches_the_dca_spec() {
|
||||
// Lock the SFREQ → sample-rate table to ffmpeg's authoritative
|
||||
// `avpriv_dca_sample_rates` (ETSI TS 102 114 Table 6-4). The high-rate
|
||||
// triad in particular — 48 k / 96 k / 192 k at indices 13/14/15 — must not
|
||||
// be shifted; a wrong entry would compute an N× frame duration and
|
||||
// reintroduce PTS drift on a 96/192 kHz DTS stream.
|
||||
let mut core = make_dts_core(512);
|
||||
let set_sfreq = |c: &mut [u8], idx: u8| c[8] = (c[8] & !0x3C) | ((idx & 0x0F) << 2);
|
||||
for (idx, want) in [
|
||||
(1u8, 8_000u32),
|
||||
(2, 16_000),
|
||||
(3, 32_000),
|
||||
(6, 11_025),
|
||||
(7, 22_050),
|
||||
(8, 44_100),
|
||||
(11, 12_000),
|
||||
(12, 24_000),
|
||||
(13, 48_000),
|
||||
(14, 96_000),
|
||||
(15, 192_000),
|
||||
] {
|
||||
set_sfreq(&mut core, idx);
|
||||
assert_eq!(
|
||||
dts_core_sample_rate(&core),
|
||||
want,
|
||||
"SFREQ {idx} must be {want} Hz"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_pes_rebases_to_its_own_pts_no_drift() {
|
||||
// Regression for the drift bug: a global running clock overshot a
|
||||
|
||||
Reference in New Issue
Block a user