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:
+1
-1
@@ -2113,7 +2113,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_keys_path2_5_mk_pool_brute_resolves_unkeyed_disc() {
|
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
|
// 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.
|
// 2.5 must km_verifies that MK against the MKB and resolve.
|
||||||
use super::super::decrypt::aes_ecb_encrypt as enc;
|
use super::super::decrypt::aes_ecb_encrypt as enc;
|
||||||
|
|||||||
+1
-1
@@ -1299,7 +1299,7 @@ impl Disc {
|
|||||||
/// zero-padded, so reading either ships up to ~124 MiB of nothing —
|
/// 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
|
/// trim to the record stream so callers send/store a few MB, not
|
||||||
/// 128 MiB.
|
/// 128 MiB.
|
||||||
fn read_aacs_inputs_from_reader(
|
pub(crate) fn read_aacs_inputs_from_reader(
|
||||||
reader: &mut dyn SectorSource,
|
reader: &mut dyn SectorSource,
|
||||||
udf_fs: &udf::UdfFs,
|
udf_fs: &udf::UdfFs,
|
||||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ pub struct DiscInputs {
|
|||||||
/// [`crate::Disc::inputs`] leaves it empty for the caller to fill.
|
/// [`crate::Disc::inputs`] leaves it empty for the caller to fill.
|
||||||
pub samples: Vec<Vec<u8>>,
|
pub samples: Vec<Vec<u8>>,
|
||||||
/// The disc's human title — the UDF/ISO volume identifier (e.g.
|
/// 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
|
/// `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
|
/// record it (keyed by `disc_hash`) to build a hash→title catalog. Not used
|
||||||
/// in any AACS derivation.
|
/// in any AACS derivation.
|
||||||
|
|||||||
+3
-3
@@ -1358,7 +1358,7 @@ mod apply_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_rejects_mismatched_codec_hint_and_uses_stream_codec() {
|
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
|
// "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.
|
// contradicts the stream's real codec → discard it, use the stream's own.
|
||||||
let mut titles = vec![title_with(vec![audio(
|
let mut titles = vec![title_with(vec![audio(
|
||||||
@@ -1378,7 +1378,7 @@ mod apply_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_unshuffles_cross_labeled_streams() {
|
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
|
// and a DD+ stream wears "TrueHD 5.1". Each is corrected from its own
|
||||||
// stream codec, eliminating the shuffle.
|
// stream codec, eliminating the shuffle.
|
||||||
let mut titles = vec![title_with(vec![
|
let mut titles = vec![title_with(vec![
|
||||||
@@ -1463,7 +1463,7 @@ mod apply_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_normalizes_plain_consistent_hint_to_marketing() {
|
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
|
// but short-form. A sibling DD+ track that fell back uses the marketing
|
||||||
// form — keeping the short form here would read inconsistently, so a
|
// form — keeping the short form here would read inconsistently, so a
|
||||||
// plain (non-richer) consistent hint is normalized to the stream's own.
|
// plain (non-richer) consistent hint is normalized to the stream's own.
|
||||||
|
|||||||
@@ -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
|
/// 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.
|
/// emitted together as one access unit, delimited by the next valid core sync.
|
||||||
/// This preserves the lossless extension data instead of downgrading to lossy
|
/// 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 {
|
pub struct DtsParser {
|
||||||
buf: Vec<u8>,
|
buf: Vec<u8>,
|
||||||
/// PTS of the access unit currently being assembled in `buf` (the unit
|
/// 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
|
// sync. Emitting on the core boundary keeps the core + every following
|
||||||
// extension substream together (the lossless data), instead of the
|
// extension substream together (the lossless data), instead of the
|
||||||
// old per-PES emit that dropped the extension PES packets and
|
// old per-PES emit that dropped the extension PES packets and
|
||||||
// downgraded the track to lossy DTS core (the Dunkirk / Fight Club
|
// downgraded the track to lossy DTS core (the lossy-core
|
||||||
// bug). The PTS is the core frame's PTS, captured when the unit began.
|
// 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
|
// Capture the access unit's PTS base on a fresh buffer, or whenever a
|
||||||
// prior forced (safety-valve) flush left it invalidated — in the
|
// prior forced (safety-valve) flush left it invalidated — in the
|
||||||
// forced case the bytes still in `buf` are not a real core frame, so
|
// forced case the bytes still in `buf` are not a real core frame, so
|
||||||
@@ -534,12 +534,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keeps_dts_hd_extension_in_separate_pes_packets() {
|
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
|
// arrives in one PES, then its DTS-HD MA extension substreams arrive
|
||||||
// in SEPARATE following PES packets on the same PID. The parser must
|
// in SEPARATE following PES packets on the same PID. The parser must
|
||||||
// stitch core + all trailing extensions into one access unit — not
|
// stitch core + all trailing extensions into one access unit — not
|
||||||
// emit a core-only (lossy) frame and drop the extension PES packets
|
// 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();
|
let mut parser = DtsParser::new();
|
||||||
|
|
||||||
// Frame 1: core (512) + two extension substreams (256 + 200).
|
// Frame 1: core (512) + two extension substreams (256 + 200).
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub struct HevcParser {
|
|||||||
// This is the ONLY copy the player gets out-of-band, and a player re-applies
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// PPS-id-0 redefinition.
|
||||||
fn handle_param_set(first: &mut Option<Vec<u8>>, nal: &[u8], frame_data: &mut Vec<u8>) {
|
fn handle_param_set(first: &mut Option<Vec<u8>>, nal: &[u8], frame_data: &mut Vec<u8>) {
|
||||||
match first {
|
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
|
/// 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,
|
/// 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).
|
/// so the second segment decoded against the wrong PPS (CABAC desync).
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+51
-1
@@ -1461,7 +1461,7 @@ mod tests {
|
|||||||
// Regression for BOTH 0.31.0 bugs through `read_file`: this is the
|
// Regression for BOTH 0.31.0 bugs through `read_file`: this is the
|
||||||
// exact path `Disc::read_aacs_inputs_from_reader` uses to read
|
// exact path `Disc::read_aacs_inputs_from_reader` uses to read
|
||||||
// `/AACS/MKB_RO.inf` + `Unit_Key_RO.inf`. A Long-AD, multi-extent
|
// `/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
|
// pre-fix Short-AD-only parser this read stopped after the first
|
||||||
// extent, which (a) truncated the mux and (b) made autorip's
|
// extent, which (a) truncated the mux and (b) made autorip's
|
||||||
// `key_files()` see a short/garbage AACS file → `MissingInputs` →
|
// `key_files()` see a short/garbage AACS file → `MissingInputs` →
|
||||||
@@ -1491,6 +1491,56 @@ mod tests {
|
|||||||
assert!(data[4096..].iter().all(|&b| b == 0xCC));
|
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]
|
#[test]
|
||||||
fn merge_ranges_saturates_near_u32_max() {
|
fn merge_ranges_saturates_near_u32_max() {
|
||||||
// Adjacent ranges near u32::MAX must not panic (debug) or wrap.
|
// Adjacent ranges near u32::MAX must not panic (debug) or wrap.
|
||||||
|
|||||||
Reference in New Issue
Block a user