mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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.
310 lines
15 KiB
Markdown
310 lines
15 KiB
Markdown
# 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.
|
|
</content>
|
|
</invoke>
|