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:
Matthew Jackson
2026-07-24 10:28:21 -07:00
parent eb0aa3556e
commit 967d0ac77e
3 changed files with 295 additions and 29 deletions
+98 -1
View File
@@ -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