Security - Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged. CI / tooling - clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny dependency-audit gate with first-party git sources allowed (#15); audience-based comment-guard (#17). Docs - README (keydb ownership, BD-J parser count), drive-access transport/reset notes, mux timeline module-doc links.
15 KiB
DTS / DTS-HD elementary stream parser (src/mux/codec/dts.rs)
Internal design notes moved out of source comments (see ci/comment-guard.py
prose caps). Each section is pointed to by a short // comment at the
corresponding site in dts.rs.
DTS_HD_EXT_SYNC
An access unit is delimited by the next CORE sync; the parser locates and
exactly sizes each extension substream (via exss_frame_size) so a false
core sync inside the EXSS payload can't split the AU and truncate the
lossless extension.
emit_or_drop
Gates an assembled access unit through the decodability check and either
pushes it or drops it. au_pts/dur_ns are already stamped on the shared
PTS clock (which the caller advances whether or not the AU survives), so a
drop leaves the following audio on its true timeline — a gap, not a shift.
Every drop is logged (fail-loud, never silent).
stamp_pts
front is the AU's own core-PES PTS (from front_pts); dur_ns its
decoded duration.
The model matches the (correct) AC-3 path: re-base to each PES's own
container timestamp, and advance by one frame duration ONLY within a run of
AUs that share the same PES. A new PES (its front differs from the
previous AU's) trusts its own timestamp — so the emitted timeline tracks the
container and never drifts. Advancing within a PES is what fixes the DVD
case (several DTS core frames packed in one PES, which otherwise all
collided on that single PES timestamp → "non monotonically increasing dts").
A global running clock was WRONG here: once accumulated frame durations
exceeded the PES spacing it never re-based, drifting the track minutes past
the real length over a feature-long title.
CORE_HEADER_MIN_BYTES
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
MIN_CORE_FRAME_BYTES (the decoded-frame-size validity floor). They must
not be conflated: this one gates buffer reads of the header, the other
rejects implausible decoded sizes.
MIN_CORE_FRAME_BYTES
Minimum plausible decoded DTS core frame size, per ETSI TS 102 114: the
on-wire FSIZE floor is 95, so a real core frame is at least 96 bytes. A
decoded core_size below this means we matched a false/corrupt core sync
(a lucky 0x7FFE8001 in extension-substream payload whose 14-bit fsize
decoded to a tiny value) rather than a real frame, so we resync instead of
closing an access unit at a junk boundary and dropping the DTS-HD extension
tail.
PTS_UNSET
Sentinel for "no valid PTS base captured yet". Real PTS-in-ns values are non-negative (derived from the unsigned 90 kHz PES timestamp), so a negative value can never collide with a genuine timestamp. Used to mark the PTS base invalid after a forced flush so the next PES sets it regardless of buffer state.
flush_tail
Emits the final buffered access unit (the last core + its extension substreams, which had no following core sync to close it during streaming), gated through the decodability check. Requires a complete core frame; drops a bare partial sync tail.
SYNCWORD_BYTES
Byte length shared by the DTS core sync and the DTS-HD EXSS sync (both
32-bit words). next_core_boundary finds the next valid core sync after
the current core frame, to delimit the access unit: extension-substream
payload can contain byte sequences that match the core syncword, so each
candidate is validated by decoding its core size — a match whose decoded
size is implausible (< MIN_CORE_FRAME_BYTES or > MAX_AU_BYTES) is a
false sync and is skipped, continuing the search.
EXSS header field bit widths
EXSS_USER_DEFINED_BITS, EXSS_INDEX_BITS, EXSS_HEADER_SIZE_TYPE_BITS,
etc. are the DTS-HD extension-substream (EXSS) header field bit widths
(ETSI TS 102 114, ExtSS header). bHeaderSizeType selects the short form
(nuExtSSHeaderSize 8 bits, nuExtSSFsize 16 bits) or, for larger
substreams, the long form (12 / 20 bits).
exss_frame_size
DTS-HD extension substream (EXSS) total byte size — INCLUDING the
0x64582025 syncword — read precisely from its header. buf must begin
with DTS_HD_EXT_SYNC. Returns None when the size fields aren't fully
buffered.
nuExtSSFsize is the total frame size in bytes minus one. Parsing it lets
the AU framer skip the extension by its exact length instead of scanning
its (arbitrary) payload for a core sync.
next_core_boundary
Offset where the current access unit ends (the start of the next core frame). The AU is the core frame plus its trailing DTS-HD extension substreams, which are skipped PRECISELY by their declared size — so a chance core syncword inside the XLL lossless payload can never be mistaken for the next AU boundary (the bug that truncated the extension and produced the "Failed to decode block code(s)" class). Falls back to the heuristic core-sync scan only when an extension can't be sized (malformed / truncated input).
scan_for_next_core
Heuristic fallback (the pre-fix behaviour): scan forward for the next core syncword whose decoded size is plausible. Used only when precise extension skipping can't proceed; a chance core syncword in extension payload usually decodes to an implausible size and is skipped.
dts_core_frame_size
fsize is the 14-bit field at bits 46-59 of the header (bytes 5-7). On the
wire fsize is the frame length minus one, so this returns fsize + 1,
i.e. the core frame length in bytes (range 1..=16384). Callers treat the
result as the actual byte length and the MIN..=MAX range checks assume so.
Returns 0 when data is shorter than CORE_HEADER_MIN_BYTES — every call
site rejects that via the minimum-frame lower bound, so a 0 is never
mistaken for a valid tiny frame.
dts_core_samples
Samples in one DTS core frame: (NBLKS + 1) * 32. NBLKS (7 bits) is the
core-header PCM-sample-block count (ETSI TS 102 114) that fixes the frame's
decoded sample count. Bit layout after the 32-bit sync: FTYPE(1) SHORT(5)
CPF(1) NBLKS(7) FSIZE(14) …, so NBLKS = byte4 bit0 + byte5 bits7-2.
DTS_PCMBLOCK_SAMPLES / DTS_SUBBAND_SAMPLES (core-header validity)
DTS core-header validity constants (ETSI TS 102 114). For a NORMAL frame
deficit_samples must equal DTS_PCMBLOCK_SAMPLES — a termination frame
may carry fewer; npcmblocks must be a multiple of DTS_SUBBAND_SAMPLES;
audio_mode must be below DTS_AMODE_COUNT; lfe_present == DTS_LFE_FLAG_INVALID is rejected.
DTS_AMODE_COUNT
Number of LEGAL AMODE (channel-arrangement) codes. The 6-bit AMODE field
(ETSI TS 102 114 §5.3.1) has 16 defined channel arrangements, codes 0-15;
only 16-63 are reserved/user-defined and undecodable. The per-AMODE channel
counts in ETSI TS 102 114 §5.3.1 cover all 16, confirming they are
decodable — codes 10-15 are the 6/7/8-channel layouts. A frame is dropped
only when audio_mode >= DTS_AMODE_COUNT (i.e. a truly reserved 16-63
code); dropping a legal 10-15 multichannel core would silence recoverable
audio.
DTS_CORE_SR_VALID
Sample rate (Hz) per core SFREQ code (ETSI TS 102 114 Table 6-4); a 0
entry marks a reserved code that fails header validation as an invalid
sample rate. Valid entries are locked to the spec by
dts_core_sfreq_table_matches_the_dca_spec; the reserved codes are
{0, 4, 5, 9, 10}.
core_header_drop_reason
Decodability gate: the core-frame header validity checks from ETSI TS 102
114. Returns Some(reason) when the DTS core-frame header is invalid — in
which case the packet is undecodable ("Invalid data found") and dropping it
loses nothing a decoder could have used. Returns None (keep) for a
decodable header OR if the header can't be fully read (never false-drop on
our own buffer underrun; the framer only emits AUs whose core is fully
buffered and ≥ 96 bytes).
The 4-byte core sync is already validated by the framer, so this reads the header fields that follow it. The 16-bit CPF header CRC is not verified (we skip past it) and the audio-header/side-info CRCs are likewise not checked, because decoders treat those bytes as optional/ignored — verifying them would drop frames that decode fine (false positives).
Test: stamp_pts_reuses_front_when_no_running_cursor_yet
stamp_pts's third arm (else if front != PTS_UNSET) is only reached once
the first arm's front != self.last_front_pts has already failed (i.e.
front == self.last_front_pts) and no within-PES running cursor is
available yet (next_pts_ns == PTS_UNSET). In that state the AU must still
be stamped with its own (repeated) front timestamp, not silently collapsed
to 0 — a != -> == typo on the guard would only take this arm when
front IS the unset sentinel, producing 0 instead here and the sentinel
value itself if front really were unset.
Test: drain_front_collapses_offset_zero_markers_instead_of_leaking
Every drained access unit that shares its PES with the previous one pushes
a new (0, pts) marker once rebased; drain_front must collapse those down
to the single most-recent one each time, or pts_marks grows once per
access unit for the life of the track — an unbounded allocation on a
multi-hour disc. front_pts() alone can't observe this: it always finds the
last offset-0 entry regardless of how many duplicates precede it, so the
leak is invisible unless something checks that the collapse actually ran.
Test: make_exss
Builds a real DTS-HD EXSS substream of total bytes (short header form),
with an optional false DTS core syncword embedded in its payload (decoding
to a plausible core size) — to prove precise sizing, not a payload scan,
bounds the extension.
Test: make_dts_ext
Builds a minimal DTS-HD extension substream of size bytes (just the sync
- zero-padding). The parser delimits extensions by the next CORE sync, not by the extension's own size header, so a valid header isn't required — only that the bytes carry no spurious core sync.
Test: bogus_tiny_core_sync
Builds 4 bytes that look like a DTS core sync but whose fsize field
decodes to a tiny core_size (< MIN_CORE_FRAME_BYTES). With the dead-code
guards this passed validation and could close an access unit at a junk
boundary; with the fix it must be drained and resynced past.
Test: next_core_boundary_exact_syncword_length_is_not_need_more
Only the 4-byte EXSS syncword is buffered after the core frame — no header
fields at all. The guard buf.len() < pos + SYNCWORD_BYTES must be strict
<: at exactly pos + SYNCWORD_BYTES the sync bytes ARE fully present, so
the function proceeds to identify them as an extension sync (then fails to
size the header and falls back, ending in NextCore::None here since
nothing after it looks like a core sync either). A <= typo would instead
return NeedMore at this exact length without ever inspecting what the 4
buffered bytes are.
Test: make_bad_dts_core
Builds a structurally-framed but UNDECODABLE core: a valid make_dts_core
whose LFE flag is set to the reserved value 3 (DTS_LFE_FLAG_INVALID). It
still sizes and syncs correctly (so the framer delimits it normally), but
the core-frame header validity check rejects it as an invalid LFE flag
(ETSI TS 102 114 §5.3.1: an LFF value of 3 is invalid). LFE is byte10
bits2-1, and does NOT feed the frame duration (NBLKS + SFREQ only), so a
dropped bad core still carries the same DTS_CORE_DUR_NS as its good peers.
Test: reparse_real_dts_file
Real-data fixture (ignored). Re-parses a raw .dts elementary stream
through DtsParser and writes the emitted access units back out, so the
garbage-extension → core-only drop can be validated against an actual
damaged stream (e.g. an extracted DTS-HD MA track) end-to-end with an
external DTS decoder. Env: DTS_IN (input), DTS_OUT (output).
cargo test --lib dts::tests::reparse_real_dts_file -- --ignored --nocapture
Test: find_sync_matches_a_buffer_that_is_exactly_the_syncword
find_sync scans 0..=len-4, so a buffer that is EXACTLY the syncword
still matches. The existing tests cover 0, 3 and "longer than 4", which
leaves the len == 4 boundary open — and that is the case a syncword split
across PES packets lands on the moment its last byte arrives.
Test: a_core_header_of_exactly_the_minimum_length_is_decoded_not_deferred
CORE_HEADER_MIN_BYTES is "enough bytes to DECODE the size field", so a
buffer holding exactly that many must be decoded, not deferred. The
distinction is visible precisely at the boundary: with 10 bytes of a core
sync whose decoded size is sub-spec, the parser must recognise the false
sync, drain past it and resync down to the 3-byte carry-over tail. Waiting
instead leaves the false sync sitting at the front of the buffer, where it
blocks every later real core behind it.
Test: flush_emits_a_core_that_exactly_fills_the_buffer
The end-of-stream flush must emit a final access unit whose core is exactly as long as the buffer — the ordinary case, since the last AU is closed by end-of-stream rather than by a following core sync. Rejecting it at the boundary silently drops the last frame of every DTS track.
Test: flush_discards_a_long_buffer_that_does_not_begin_with_a_core_sync
The flush guard is a DISJUNCTION: a buffer that does not BEGIN with a core
sync is discarded whatever its length. Requiring both conditions instead
lets a long run of junk through to dts_core_frame_size, which happily
decodes a 14-bit size out of arbitrary bytes — and the flush then emits an
"access unit" that is not DTS at all.
The junk here is sized so that mis-decoding it yields a plausible core size that the buffer fully covers, which is exactly when the wrong answer looks like a right one.
Test: exss_frame_size_needs_the_worst_case_header_and_no_more
EXSS_HEADER_MIN_BYTES is the WORST-CASE header length — the long form's
43 bits after the syncword, rounded up to 6 bytes, plus the 4-byte sync. It
gates whether an extension substream can be sized precisely, and that is
what keeps a chance core syncword inside XLL payload from being mistaken
for the next AU boundary; too large and every extension falls back to the
payload scan, too small and the reader runs off a truncated header.
Pinned at both sides of the boundary rather than by value, so the field widths it is summed from stay honest.
Test: an_access_unit_carries_the_source_of_the_packet_its_core_arrived_in
The whole point of the shared buffer: an access unit whose core arrived in an EARLIER packet keeps that packet's source offset, not the offset of whichever packet completed it. At a clip boundary the two belong to different clips, and taking the later one puts the unit in the wrong clip — which is what left nine audio and subtitle tracks unplaceable.