diff --git a/src/aacs/resolve.rs b/src/aacs/resolve.rs index 42f9cc7..452a9a1 100644 --- a/src/aacs/resolve.rs +++ b/src/aacs/resolve.rs @@ -1345,7 +1345,7 @@ mod tests { } #[test] fn stride_v10_is_48_v20_is_64_and_picks_distinct_keys() { - // AACS 1.0 stride = 48, AACS 2.0/2.1 stride = 64 (keys.rs:30-35). + // AACS 1.0 stride = 48, AACS 2.0/2.1 stride = 64 (aacs/inf.rs). // Lay keys at 64-byte stride. Parsing at V20 stride must pick exactly // those keys; parsing the SAME bytes at V10 (48) stride would read the // wrong (intermediate) bytes for key 2 onward — proving the stride @@ -1447,7 +1447,7 @@ mod tests { } #[test] fn parse_unit_key_ro_cps_unit_numbers_are_1_based() { - // The disc's CPS unit numbers are emitted as (i+1) — keys.rs:162. + // The disc's CPS unit numbers are emitted as (i+1) — aacs/inf.rs. let data = build_unit_key_ro(3, 48); let p = parse_unit_key_ro(&data, AacsVersion::V10).unwrap(); assert_eq!( @@ -1936,7 +1936,7 @@ mod tests { // // The rc.6 E7017/E7022 split is also exercised end-to-end through the // `ensure_decryptable` gate in `disc/mod.rs`. These tests pin the - // *classifier* directly at the keys.rs seam and cover the branches the + // *classifier* directly at the aacs::resolve seam and cover the branches the // gate test does not: VID-present (must never be VidUnavailable), the // processing-keys-only material path, and the version dispatch / Ok path. diff --git a/src/aacs/variant.rs b/src/aacs/variant.rs index bc4e99d..cdc6b25 100644 --- a/src/aacs/variant.rs +++ b/src/aacs/variant.rs @@ -889,7 +889,7 @@ mod tests { fn mkb_records_matches_walk_mkb_framing() { // The lazy `mkb_records` iterator and the owning `walk_mkb` must agree on // (offset, type, len) for every record — they share the one framing - // walker, and every keys.rs MKB walk now relies on this equivalence. + // walker, and every aacs::resolve/derive MKB walk now relies on this equivalence. let mut mkb = vec![0x10, 0x00, 0x00, 0x06, 0xAA, 0xBB]; mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x08, 1, 2, 3, 4]); mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0xFF]); // terminator + trailing diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index 2a01134..83aed7c 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -572,7 +572,7 @@ mod tests { } /// A content certificate: type byte@0 (0x00 = V10, else V20), - /// bus_encryption bit7@1, cc_id@14..20 (aacs/keys.rs parse_content_cert, + /// bus_encryption bit7@1, cc_id@14..20 (aacs/inf.rs parse_content_cert, /// which requires ≥20 bytes and reads the bus flag from `data[1] >> 7`). fn build_content_cert(cert_type: u8, bus_encryption: bool) -> Vec { let mut v = vec![0u8; 20]; @@ -584,7 +584,7 @@ mod tests { /// An MKB with one Type-and-Version record (type 0x10) carrying the /// version as BE u32 at record offset 8, followed by a recorded EOF /// record then trailing zero padding. mkb_content_len walks records - /// and stops at the first padding (type 0) byte (aacs/keys.rs). + /// and stops at the first padding (type 0) byte (aacs/inf.rs). fn build_mkb(version: u32, pad_to: usize) -> Vec { let mut v = Vec::new(); // Type 0x10 record, length 16 (>= 12 so version is read). diff --git a/src/disc/mod.rs b/src/disc/mod.rs index e781552..f7e1457 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -2129,12 +2129,9 @@ impl Disc { titles: &[DiscTitle], ) -> DiscFormat { use crate::aacs::mkb::{AacsVersion, mkb_type}; - if udf_fs.find_dir("/HVDVD_TS").is_some() { - return DiscFormat::HdDvd; - } - if udf_fs.find_dir("/VIDEO_TS").is_some() { - return DiscFormat::Dvd; - } + // Tree priority MUST match the title-scan dispatch (BDMV → HVDVD_TS → + // VIDEO_TS): otherwise a disc carrying two trees would be classified as + // one format but enumerated as another (e.g. BD titles tagged HdDvd). if udf_fs.find_dir("/BDMV").is_some() { // Only the Type-and-Version record (first record) is needed. if let Ok(mkb) = udf_fs.read_file_prefix(reader, "/AACS/MKB_RO.inf", 64) { @@ -2151,6 +2148,12 @@ impl Disc { other => other, }; } + if udf_fs.find_dir("/HVDVD_TS").is_some() { + return DiscFormat::HdDvd; + } + if udf_fs.find_dir("/VIDEO_TS").is_some() { + return DiscFormat::Dvd; + } DiscFormat::Unknown } diff --git a/src/mux/codec/dts.rs b/src/mux/codec/dts.rs index 29a77dc..892cffc 100644 --- a/src/mux/codec/dts.rs +++ b/src/mux/codec/dts.rs @@ -140,6 +140,12 @@ impl DtsParser { /// this without a clean boundary we resync rather than stall or balloon. const MAX_AU_BYTES: usize = 65536; +/// Cap on buffered PTS marks. A real AU spans a few PES; this bounds the deque so +/// a run of zero-length timed PES packets (which grow no buffer bytes, so the +/// `drain_front` prune never fires) cannot accumulate marks without bound on +/// hostile program-stream input. +const MAX_PTS_MARKS: usize = 64 * 1024; + /// Number of leading bytes that must be buffered before the core `fsize` field /// (bytes 5-7) can be decoded. This is a HEADER-LAYOUT minimum — "enough bytes /// to read the size field" — and is deliberately distinct from @@ -231,6 +237,14 @@ impl CodecParser for DtsParser { // that core's timestamp even when its extensions / the following core // arrive (with a later PTS) in this same parse() call. self.pts_marks.push((self.buf.len(), pts_ns)); + // Backstop: a run of zero-length (sub-header-only) PES packets that each + // carry a PTS grows no buffer bytes, so `drain_front` (which prunes marks) + // never runs. Bound the deque directly — drop the oldest, which belongs to + // an already-emitted or lost AU — so hostile PS input can't accumulate + // marks without bound. + if self.pts_marks.len() > MAX_PTS_MARKS { + self.pts_marks.remove(0); + } self.buf.extend_from_slice(&pes.data); let mut frames = Vec::new(); @@ -915,6 +929,22 @@ mod tests { assert_eq!(dts_core_sample_rate(&core), 48_000); } + #[test] + fn pts_marks_stay_bounded_on_zero_length_pes() { + // A run of zero-length (sub-header-only) DTS PES packets that each carry a + // PTS grows no buffer bytes, so drain_front (which prunes marks) never + // runs. The MAX_PTS_MARKS backstop must bound the deque regardless. + let mut parser = DtsParser::new(); + for i in 0..(MAX_PTS_MARKS * 2) { + parser.parse(&make_pes(Vec::new(), Some(i as i64))); + } + assert!( + parser.pts_marks.len() <= MAX_PTS_MARKS, + "pts_marks bounded, got {}", + parser.pts_marks.len() + ); + } + #[test] fn new_pes_rebases_to_its_own_pts_no_drift() { // Regression for the drift bug: a global running clock overshot a @@ -922,6 +952,14 @@ mod tests { // PES arrives whose PTS is BEHIND where accumulated frame durations // would put a running clock, the AU must re-base to that PES's OWN // timestamp — tracking the container, not drifting ahead of it. + // + // The re-base can make one emitted PTS sit just below the previous AU's + // (a fresh PES whose PTS lands under the within-PES cursor). That is + // CORRECT here and is NOT a muxer defect: the parser reports the true + // container timestamps, and the mkv muxer applies the strictly-monotonic + // per-track nudge to AUDIO at emit time (`mkv::block_ts` / `monotonic_ts`, + // tested in `mkv.rs`), so the written block DTS is always monotonic. The + // alternative — clamping in the parser — is what reintroduced the drift. let mut parser = DtsParser::new(); // PES A: core1 + core2 (2 frames), pts 90000. let mut pes_a = make_dts_core(512);