tests: read_aacs_inputs Long-AD coverage; scrub title names from comments

Add a precommit fixture proving Disc::read_aacs_inputs reads a Long-AD,
multi-extent /AACS/Unit_Key_RO.inf in full — the exact input the online
key-request path depends on (no disc/deploy needed). Make
read_aacs_inputs_from_reader pub(crate) for the test.

Reword internal comments/doc examples to generic descriptions instead of
specific media titles.
This commit is contained in:
Matthew Jackson
2026-06-07 21:25:29 -07:00
parent 222a596c55
commit 2a55bab3ed
7 changed files with 66 additions and 16 deletions
+1 -1
View File
@@ -2113,7 +2113,7 @@ mod tests {
#[test]
fn resolve_keys_path2_5_mk_pool_brute_resolves_unkeyed_disc() {
// The Dunkirk case: this disc's own hash/VID are NOT in keydb, but its
// The keyless-disc case: this disc's own hash/VID are NOT in keydb, but its
// Media Key IS — filed under a sibling disc that shares its MKB. Path
// 2.5 must km_verifies that MK against the MKB and resolve.
use super::super::decrypt::aes_ecb_encrypt as enc;
+1 -1
View File
@@ -1299,7 +1299,7 @@ impl Disc {
/// zero-padded, so reading either ships up to ~124 MiB of nothing —
/// trim to the record stream so callers send/store a few MB, not
/// 128 MiB.
fn read_aacs_inputs_from_reader(
pub(crate) fn read_aacs_inputs_from_reader(
reader: &mut dyn SectorSource,
udf_fs: &udf::UdfFs,
) -> Result<(Vec<u8>, Vec<u8>)> {
+1 -1
View File
@@ -36,7 +36,7 @@ pub struct DiscInputs {
/// [`crate::Disc::inputs`] leaves it empty for the caller to fill.
pub samples: Vec<Vec<u8>>,
/// The disc's human title — the UDF/ISO volume identifier (e.g.
/// `WICKED_FOR_GOOD`), falling back to the BDMV `<di:name>` when present.
/// `TITLE_2024`), falling back to the BDMV `<di:name>` when present.
/// `None` when not captured. Identity only, no secret; a key service may
/// record it (keyed by `disc_hash`) to build a hash→title catalog. Not used
/// in any AACS derivation.
+3 -3
View File
@@ -1358,7 +1358,7 @@ mod apply_tests {
#[test]
fn apply_rejects_mismatched_codec_hint_and_uses_stream_codec() {
// Paddington case: a TrueHD+Atmos main track the parser mislabeled
// TrueHD+Atmos relabel case: a TrueHD+Atmos main track the parser mislabeled
// "AC-3 2.0" (a compat-core hint bound to the wrong stream). The hint
// contradicts the stream's real codec → discard it, use the stream's own.
let mut titles = vec![title_with(vec![audio(
@@ -1378,7 +1378,7 @@ mod apply_tests {
#[test]
fn apply_unshuffles_cross_labeled_streams() {
// Wicked case: hints fully cross-bound — a TrueHD stream wears "AC-3 5.1"
// Cross-bound hints case: hints fully cross-bound — a TrueHD stream wears "AC-3 5.1"
// and a DD+ stream wears "TrueHD 5.1". Each is corrected from its own
// stream codec, eliminating the shuffle.
let mut titles = vec![title_with(vec![
@@ -1463,7 +1463,7 @@ mod apply_tests {
#[test]
fn apply_normalizes_plain_consistent_hint_to_marketing() {
// Wicked's French track: a DD+ stream whose hint "AC-3+ 5.1" is correct
// A French DD+ track: a DD+ stream whose hint "AC-3+ 5.1" is correct
// but short-form. A sibling DD+ track that fell back uses the marketing
// form — keeping the short form here would read inconsistently, so a
// plain (non-richer) consistent hint is normalized to the stream's own.
+5 -5
View File
@@ -19,7 +19,7 @@ const DTS_HD_EXT_SYNC: [u8; 4] = [0x64, 0x58, 0x20, 0x25];
/// a core frame plus all of its trailing DTS-HD extension substreams are
/// emitted together as one access unit, delimited by the next valid core sync.
/// This preserves the lossless extension data instead of downgrading to lossy
/// core (the Dunkirk / Fight Club lossy-core bug).
/// core (the lossy-core downgrade bug).
pub struct DtsParser {
buf: Vec<u8>,
/// PTS of the access unit currently being assembled in `buf` (the unit
@@ -140,8 +140,8 @@ impl CodecParser for DtsParser {
// sync. Emitting on the core boundary keeps the core + every following
// extension substream together (the lossless data), instead of the
// old per-PES emit that dropped the extension PES packets and
// downgraded the track to lossy DTS core (the Dunkirk / Fight Club
// bug). The PTS is the core frame's PTS, captured when the unit began.
// downgraded the track to lossy DTS core (the lossy-core
// downgrade bug). The PTS is the core frame's PTS, captured when the unit began.
// Capture the access unit's PTS base on a fresh buffer, or whenever a
// prior forced (safety-valve) flush left it invalidated — in the
// forced case the bytes still in `buf` are not a real core frame, so
@@ -534,12 +534,12 @@ mod tests {
#[test]
fn keeps_dts_hd_extension_in_separate_pes_packets() {
// The real Blu-ray layout (ground-truthed on Dunkirk): the DTS core
// The real Blu-ray layout (ground-truthed on real UHD discs): the DTS core
// arrives in one PES, then its DTS-HD MA extension substreams arrive
// in SEPARATE following PES packets on the same PID. The parser must
// stitch core + all trailing extensions into one access unit — not
// emit a core-only (lossy) frame and drop the extension PES packets
// (the Dunkirk / Fight Club lossy-core bug).
// (the lossy-core downgrade bug).
let mut parser = DtsParser::new();
// Frame 1: core (512) + two extension substreams (256 + 200).
+4 -4
View File
@@ -28,7 +28,7 @@ pub struct HevcParser {
// This is the ONLY copy the player gets out-of-band, and a player re-applies
// it at every keyframe (ffmpeg's hvcC→Annex-B insertion). A stream may
// redefine a parameter set mid-title under the SAME id with a different body
// (Fight Club redefines PPS id 0 partway through). Any occurrence whose body
// (some discs redefine PPS id 0 partway through). Any occurrence whose body
// DIFFERS from this codecPrivate copy must therefore be emitted IN-BAND at
// each point it appears (i.e. at every keyframe of the redefined segment) so
// it overrides the re-applied codecPrivate set; otherwise those frames decode
@@ -65,7 +65,7 @@ impl HevcParser {
/// same id) → emitted IN-BAND (length-prefixed) at EVERY occurrence, so it
/// overrides the hvcC copy the player re-applies at each keyframe. Emitting it
/// only once is not enough — the next keyframe's hvcC re-insertion would revert
/// it. This matches what a conforming muxer produces and fixes the Fight Club
/// it. This matches what a conforming muxer produces and fixes mid-title
/// PPS-id-0 redefinition.
fn handle_param_set(first: &mut Option<Vec<u8>>, nal: &[u8], frame_data: &mut Vec<u8>) {
match first {
@@ -829,10 +829,10 @@ mod tests {
);
}
// --- parameter-set redefinition (Fight Club bug) ---
// --- parameter-set redefinition (mid-title redefinition bug) ---
/// A parameter set REDEFINED mid-stream (same id, different body) must be
/// emitted INLINE so the decoder re-activates it. Fight Club redefines PPS
/// emitted INLINE so the decoder re-activates it. Some discs redefine PPS
/// id 0 partway through the title; the old parser kept only the first PPS,
/// so the second segment decoded against the wrong PPS (CABAC desync).
#[test]
+51 -1
View File
@@ -1461,7 +1461,7 @@ mod tests {
// Regression for BOTH 0.31.0 bugs through `read_file`: this is the
// exact path `Disc::read_aacs_inputs_from_reader` uses to read
// `/AACS/MKB_RO.inf` + `Unit_Key_RO.inf`. A Long-AD, multi-extent
// file (Dunkirk-class UHD layout) must return ALL its bytes. With the
// file (large UHD/Blu-ray layout) must return ALL its bytes. With the
// pre-fix Short-AD-only parser this read stopped after the first
// extent, which (a) truncated the mux and (b) made autorip's
// `key_files()` see a short/garbage AACS file → `MissingInputs` →
@@ -1491,6 +1491,56 @@ mod tests {
assert!(data[4096..].iter().all(|&b| b == 0xCC));
}
#[test]
fn read_aacs_inputs_reads_long_ad_files_in_full() {
// PRECOMMIT proof for the autorip online-keyserver path (no disc / no
// deploy). autorip's key request is gated on Disc::read_aacs_inputs
// (keysource.rs key_files()): it reads /AACS/Unit_Key_RO.inf and
// /AACS/MKB_RO.inf. On a Long-AD disc (UHD / large Blu-ray) the
// pre-0.31.1 Short-AD-only reader truncated those files at their first
// extent, breaking key derivation. This
// fixture lays a Long-AD, multi-extent Unit_Key_RO.inf under /AACS and
// asserts read_aacs_inputs returns its FULL content — i.e. the keyserver
// inputs are complete, so the request is built correctly.
let aacs = DirEntry {
name: "AACS".to_string(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![
file_entry("Unit_Key_RO.inf", 5, 4096), // Long-AD, 2 extents
file_entry("MKB_RO.inf", 7, 2048),
],
};
let root = DirEntry {
name: String::new(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![aacs],
};
let mut reader = MapReader::new();
// Unit_Key_RO.inf: Long-AD ICB with two recorded extents.
reader.put(5, build_efe_long(4096, &[(0, 2048, 10), (0, 2048, 30)]));
reader.put(10, [0xAA; 2048]);
reader.put(30, [0xBB; 2048]);
// MKB_RO.inf: single Long-AD extent (content is opaque to this test).
reader.put(7, build_efe_long(2048, &[(0, 2048, 50)]));
reader.put(50, [0xCC; 2048]);
let fs = fs_with(0, 0, root);
let (inf, _mkb) = crate::disc::Disc::read_aacs_inputs_from_reader(&mut reader, &fs)
.expect("read_aacs_inputs must succeed for a Long-AD disc");
assert_eq!(
inf.len(),
4096,
"Unit_Key_RO.inf (Long-AD, multi-extent) must read in full — the \
pre-0.31.1 Short-AD parser truncated it to the first 2048-byte extent"
);
assert!(inf[..2048].iter().all(|&b| b == 0xAA));
assert!(inf[2048..].iter().all(|&b| b == 0xBB));
}
#[test]
fn merge_ranges_saturates_near_u32_max() {
// Adjacent ranges near u32::MAX must not panic (debug) or wrap.