mux: fix DTS core-header false-drops + close TrueHD/mux gate coverage
DTS core decodability gate (core_header_drop_reason) — full ETSI TS 102 114
spec-conformance sweep against ffmpeg ff_dca_parse_core_frame_header and
dcadec parse_frame_header:
- deficit_samples: only require ==32 for NORMAL frames (FTYPE==1). A
TERMINATION frame (FTYPE==0, the last frame of a stream) legitimately
carries fewer and is fully decodable; the old unconditional check dropped
it on every stream that ends on one — a guaranteed per-track silence gap.
Matches ffmpeg (normal_frame && deficit != DCA_PCMBLOCK_SAMPLES) and
dcadec (branches on normal_frame).
- reserved bit (after RATE): both reference decoders SKIP it (ffmpeg
skip_bits1, dcadec bits_skip1 "Reserved field") and never reject on it.
Rejecting was a false-drop that silenced any real stream whose encoder
set the bit. Relaxed to read-and-discard; DropReason::ReservedBit removed.
Swept and confirmed spec-correct as-is (no change): npcmblocks multiple-of-8,
frame_size>=96, audio_mode>=16 (ffmpeg-permissive), sample-rate validity
table (matches avpriv_dca_sample_rates incl 96k/192k at 14/15), LFE flag==3
invalid, PCMR bits table (matches dcadec sample_res {16,16,20,20,0,24,24,0}).
Bit-read order verified field-by-field against dcadec. bit_rate is left
unvalidated (lenient, never-false-drop direction) as before.
Tests: termination frame with small deficit is kept; normal frame with bad
deficit is dropped; reserved-bit-set frame is kept. make_bad_dts_core now
uses an invalid LFE flag (duration-neutral) instead of the relaxed reserved
bit.
TrueHD: add coverage for the EXTENDED major-sync header CRC path (ms[25]&1,
mshdr=28+2+2n) — previously zero-tested, the exact path a shipped endianness
bug once used to silently drop whole 7.1/Atmos tracks. Trailer is an
independently-computed oracle (separate CRC-16/0x2D, anchored to the 0x4FF7
catalogue value, NOT crc16_mlp), stored little-endian; test asserts accept,
body-corruption reject, and big-endian-trailer reject.
mux driver: extract the finish completion mapping into pure mux_run_completed
so the finalize_failed -> completed=false branch (reachable only via real
write-thread wedge timing) is unit-tested; add an out-of-range
MuxInput::Session title_index test asserting a clean Error::MuxTrackRange
(E9011) instead of a panic.
This commit is contained in:
+98
-1
@@ -540,6 +540,20 @@ fn reader_event_fn(events: Arc<dyn MuxEvents>) -> crate::sector::prefetched::Eve
|
||||
/// The reader-agnostic driver body: headers → gate → sink → pump → finish.
|
||||
/// Split out so it can be unit-tested against a synthetic [`Stream`] (the
|
||||
/// injection seam), independent of which constructor built `stream`.
|
||||
/// Whether a finished mux counts as COMPLETED. A clean operator stop
|
||||
/// (`interrupted`), a wedged/halted finalize (`finalize_failed` — the write
|
||||
/// [`Pipeline`] returned `Halted`/`PipelineJoinTimeout` from `finish`), or a
|
||||
/// halt cancellation each force `completed = false`, so the consumer runs its
|
||||
/// stop-preserves-staging path instead of reporting a truncated file as done.
|
||||
///
|
||||
/// Extracted as a pure fn because the `finalize_failed` branch is otherwise
|
||||
/// reachable only through real write-thread wedge timing (the internally-built
|
||||
/// `WriteSink` offers no seam to force a `finish` timeout deterministically), so
|
||||
/// the mapping is unit-tested here directly.
|
||||
fn mux_run_completed(interrupted: bool, finalize_failed: bool, halt_cancelled: bool) -> bool {
|
||||
!(interrupted || finalize_failed || halt_cancelled)
|
||||
}
|
||||
|
||||
fn drive_mux(
|
||||
mut stream: Box<dyn Stream>,
|
||||
dest_url: &str,
|
||||
@@ -738,7 +752,7 @@ fn drive_mux(
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
if interrupted || finalize_failed || halt.is_cancelled() {
|
||||
if !mux_run_completed(interrupted, finalize_failed, halt.is_cancelled()) {
|
||||
return Ok(MuxOutcome {
|
||||
completed: false,
|
||||
output_opened: true,
|
||||
@@ -1692,6 +1706,89 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// FIX 3: a mux whose read side drained cleanly (`interrupted = false`, halt
|
||||
/// not cancelled) but whose write pipeline WEDGED on finish (`finish_with_halt`
|
||||
/// → `Err(Halted | PipelineJoinTimeout)` → `finalize_failed = true`) must fall
|
||||
/// through to `completed = false` — never surface a truncated file as a
|
||||
/// finished rip. The wedge is reachable only via real write-thread timing, so
|
||||
/// the completion mapping is tested via the extracted pure fn.
|
||||
///
|
||||
/// Mutation: dropping `finalize_failed` from `mux_run_completed`'s condition
|
||||
/// makes `mux_run_completed(false, true, false)` return `true` → this fails.
|
||||
#[test]
|
||||
fn finalize_failed_forces_incomplete_outcome() {
|
||||
// The load-bearing case: clean drain, wedged finalize → NOT completed.
|
||||
assert!(
|
||||
!mux_run_completed(false, true, false),
|
||||
"a wedged/halted finalize must force completed = false"
|
||||
);
|
||||
// A fully clean finish is the only path to completed = true.
|
||||
assert!(
|
||||
mux_run_completed(false, false, false),
|
||||
"a clean drain + clean finalize completes"
|
||||
);
|
||||
// The other two forcers likewise yield incomplete.
|
||||
assert!(
|
||||
!mux_run_completed(true, false, false),
|
||||
"operator stop → incomplete"
|
||||
);
|
||||
assert!(
|
||||
!mux_run_completed(false, false, true),
|
||||
"halt cancel → incomplete"
|
||||
);
|
||||
}
|
||||
|
||||
/// FIX 4: `MuxInput::Session` with a `title_index` past the disc's title count
|
||||
/// must surface a clean `Error::MuxTrackRange` (code E9011), NOT panic on the
|
||||
/// out-of-range `titles.get(idx)`. Everything else is valid (disc scanned,
|
||||
/// reader staged) so the range guard is the sole failure.
|
||||
///
|
||||
/// Mutation: replacing the `.ok_or(MuxTrackRange…)?` guard with `.unwrap()`
|
||||
/// panics on the out-of-range index → this test fails.
|
||||
#[test]
|
||||
fn mux_input_session_out_of_range_title_is_clean_error_not_panic() {
|
||||
use crate::disc::Extent;
|
||||
use crate::session::DiscSession;
|
||||
|
||||
let unit_key = [0x5Au8; 16];
|
||||
let reader = Box::new(AacsUnitReader {
|
||||
unit: encrypted_audio_unit(&unit_key),
|
||||
capacity: 2048,
|
||||
});
|
||||
let mut title = aac_audio_title(0x1100);
|
||||
title.extents = vec![Extent {
|
||||
start_lba: 0,
|
||||
sector_count: 3,
|
||||
}];
|
||||
let disc = aacs_session_disc(title, unit_key);
|
||||
let num_titles = disc.titles.len();
|
||||
let mut session = DiscSession::from_parts_for_test(Some(disc), Some(reader), None);
|
||||
|
||||
let opts = MuxOptions {
|
||||
skip_errors: false,
|
||||
batch_sectors: 3,
|
||||
raw: false,
|
||||
send_deadline: Some(Duration::from_secs(60)),
|
||||
};
|
||||
let halt = Halt::new();
|
||||
let err = mux_stream(
|
||||
MuxInput::Session {
|
||||
session: &mut session,
|
||||
title_index: num_titles + 5, // out of range
|
||||
},
|
||||
"null://",
|
||||
&opts,
|
||||
&halt,
|
||||
Arc::new(NoopEvents),
|
||||
)
|
||||
.expect_err("an out-of-range title index must be a clean error, not a panic");
|
||||
// MuxTrackRange renders as "E9011: track/tracks".
|
||||
assert!(
|
||||
err.to_string().contains("E9011"),
|
||||
"expected MuxTrackRange (E9011), got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The shared `resolve_inline_base_map` helper's gating: an AACS key set
|
||||
/// yields a map (Some); CSS/clear/None and `raw` yield None (CSS self-cracks
|
||||
/// in `DiscStream::new`; raw is ciphertext passthrough). Guards the Session
|
||||
|
||||
Reference in New Issue
Block a user