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.
11 KiB
src/mux/ps.rs — extended notes
Relocated prose from doc/inline comments in ps.rs, trimmed there to stay
within the comment-guard's prose caps. Each ////// pointer in the source
names the section here it corresponds to.
Stream-ID constant rationale
EXTENDED_STREAM_ID(0xFD): the H.222.0 escape whereby the real stream id is thestream_id_extensioncarried in the PES extension. HD-DVD.evoputs its VC-1 video (and HD audio) here (a real HD-DVD title: VC-1 on0xFDext0x55); a transport stream never uses it. The elementary-stream bytes follow the PES header exactly like any other PES — only the routing key differs.MAX_PS_BUFFER: a length-0 (unbounded) video PES is delimited by the next PS-layer boundary; if a corrupt stream declares an unbounded PES and never follows it with a boundary,feed()would otherwise accumulate the entire input. Past this cap we force the in-progress unbounded PES to flush at the buffer end so untrusted input cannot drive unbounded allocation. A real DVD pack/PES is at most a few KB; this leaves generous slack while still bounding worst-case memory.
dvd_audio_pid rationale
The PID is 0xBD00 | sub_stream_id, which is unique per sub-stream id (AC-3
/ DTS 0x80..=0x8F, LPCM 0xA0..=0xA7). Unlike the old per-codec relative
arithmetic, distinct sub-ids therefore always yield distinct PIDs — so a
mixed-codec title (e.g. AC-3 + DTS, whose sub-ids are 0x80 and 0x88) can
never collide on one PID. This is the single source of truth shared with
Disc::scan_dvd_titles (src/disc/dvd.rs), which sets each
AudioStream.pid from the same function so demuxer output routes through
the title's pid_to_track.
HD-DVD (.evo Enhanced VOB) carries Dolby Digital Plus (E-AC-3) on
private_stream_1 sub-stream ids 0xC0..=0xC7 — a range DVD never uses
(DVD audio is 0x80..=0x8F / 0xA0..=0xA7), so admitting it here is purely
additive and cannot change any DVD mapping. The PID is 0xBD00 | sub just
like the DVD audio ranges, so a mixed HD-DVD title (four DD+ tracks
0xC0..0xC3) routes each track to its own distinct PID.
dvd_pid rationale
Routes by the REAL on-wire (stream_id, sub_stream_id) via the shared
[dvd_audio_pid] / [dvd_subtitle_pid] tables the scanner also uses —
never per-codec relative arithmetic, which collided on mixed-codec audio
(AC-3 0x80 and DTS 0x88 both mapping to 0xBD00).
None is returned for stream/sub-stream combinations the DVD title scanner
does not assign a PID to (e.g. MPEG audio 0xC0-0xDF, private stream 2,
unrecognized sub-stream ranges). The caller is expected to WARN-and-drop in
that case rather than silently mis-routing the packet.
PsDemuxer::pending_scan
Boundary-scan cursor for an unbounded (length-0) PES still waiting for its
terminating PS-layer unit: (buffer offset of the PES start code, buffer offset up to which the search has already proved there is no boundary).
Both are buffer-relative and are rebased when the buffer drains.
Without it, every feed re-searches the WHOLE accumulated payload from the
PES header: the buffer only stops growing at MAX_PS_BUFFER, so a stream
that declares an unbounded PES and then never emits a PS-layer start code (a
corrupt or crafted VOB) makes the demuxer scan up to 4 MiB per call,
quadratic in the bytes fed. Cleared whenever the PES is emitted, so it can
never outlive the packet it describes.
find_ps_boundary
Find the next PS-layer unit boundary at or after from: a start code whose
ID byte is a pack (0xBA), system header (0xBB), program-end (0xB9), or a
payload-carrying PES stream ID (0xBD..=0xEF).
A length-0 (unbounded) video PES must be delimited by the next PS-layer unit
— NOT by the next raw 00 00 01. The MPEG-2 video elementary stream inside
the PES is itself full of 00 00 01 xx start codes (picture 0x00, slices
0x01..=0xAF, GOP 0xB8, sequence 0xB3); a plain start-code scan would cut the
PES inside its own payload and re-scan the discarded video bytes as bogus PS
units. Restricting the search to PS-layer IDs (>= 0xB9, excluding the video
ES codes below it) frames the unbounded PES at the right boundary.
Returns (boundary, searched_to). searched_to is the offset up to which
every byte has been PROVED not to begin a PS-layer boundary start code, so a
later call over the same buffer (grown at the tail) may resume there instead
of re-scanning the payload from the PES header. When the scan runs off the
end, the last two bytes are NOT proved: a 00 00 01 prefix can straddle the
next feed's boundary by up to two bytes.
parse_pts — 5-byte PTS/DTS layout (ISO/IEC 13818-1 Table 2-17)
byte0: [prefix:4][pts 32..30:3][marker:1]
byte1: [pts 29..22:8]
byte2: [pts 21..15:7][marker:1]
byte3: [pts 14..7:8]
byte4: [pts 6..0:7][marker:1]
Test: only_private_stream_2_is_navigation_and_never_a_routable_stream
is_nav() exists to separate the ONE unmappable stream a DVD is expected to
contain — private_stream_2 (0xBF), the PCI/DSI navigation packs (ISO/IEC
13818-1 Table 2-22) — from every other packet whose dvd_pid() comes back
None, which is an unexpected, possibly-lost real stream. The mux loops use
the distinction to choose between a silent tally and a per-packet WARN, so
collapsing it to a constant either buries a genuine stream loss in the nav
tally, or floods the log with one warning per navigation pack on every DVD
ever ripped.
The invariant that ties the two together: is_nav() may only ever be true
where dvd_pid() is None — a packet that routes to a real track must
never be silently classified as navigation.
Test: max_ps_buffer_has_the_documented_value
MAX_PS_BUFFER is read by its own tests only through the same symbol, so a
mutated arithmetic expression in its definition changes what the symbol
itself evaluates to and every self-referential assertion still passes. Pin
the compiled value against a literal computed independently.
Test: pack_header_exact_fit_is_consumed_not_awaited
Pack-header framing (sc + 14 > len, then sc + pack_len > len) must accept
an EXACT fit — the whole pack (mandatory 14 bytes, or with stuffing) present
and not one byte more — rather than waiting for data that will never come.
Both checks are preceded by an unrelated start code so sc != 0: at
sc == 0 a sc + pack_len vs. sc * pack_len mutant collapses to the same
value (0) and the bound stays unreachable from any input.
Test: system_header_length_high_byte_is_not_dropped
header_len is a 16-bit big-endian field (buffer[sc+4] << 8 | buffer[sc+5]). A << -> >> mutation collapses the high byte to zero, so
any header_length > 255 is misread as just its low byte — here 300
(0x012C) misread as 44. A start code embedded 50 bytes in (well inside the
true 306-byte unit but exactly where the mis-parsed 50-byte unit would end)
must stay buried in the skipped body under correct parsing, and surface as a
bogus extra PES under the mutant.
Test: find_ps_boundary_handles_a_bare_start_code_at_the_buffer_head
find_ps_boundary's bounds check (sc + 3 >= data.len()) must stay an
ADDITION: a + -> - mutation at sc == 0 underflows the usize
subtraction and panics on a plain 3-byte start code with nothing after it —
exactly the tail a real feed can end on.
Test: an_unterminated_pes_is_not_rescanned_from_its_header_every_feed
An unbounded (length-0) PES is terminated by the next PS-LAYER unit, and
until one arrives the payload accumulates in the buffer. The search for that
unit must not restart at the PES header on every feed: the buffer only stops
growing at MAX_PS_BUFFER (4 MiB), and a feed is one read batch (at most
510 sectors ≈ 1 MiB, 60 sectors ≈ 120 KiB on an optical drive), so
re-scanning from byte 0 costs work quadratic in the bytes fed — up to 4 MiB
of scanning per call, for as long as a corrupt or crafted VOB withholds the
boundary. A conformant DVD ends every pack within 2048 bytes and never
reaches this state.
Measured directly, because a work bound has no packet-level shadow:
boundary_bytes_scanned counts the bytes find_ps_boundary examines. 256
chunks x 4 KiB of boundary-free payload is 1 MiB of input; re-scanning from
the header on every call examines 4 KiB * 256*257/2 = ~128 MiB.
Mutation: drop the Some((pes_at, searched_to)) if pes_at == sc arm so
from is always sc + 4.
Test: a_resume_cursor_survives_the_drain_of_units_ahead_of_the_unbounded_pes
The resume cursor is a BUFFER offset, so it must be rebased when the buffer drains — and this is the only test in which a drain actually happens while a cursor is live.
Mutations this catches, both halves of
self.pending_scan.map(|(pes_at, searched_to)| (pes_at - pos, searched_to - pos))
in extract_packets:
searched_to - pos->searched_to: the next call resumesposbytes PAST where the previous scan actually stopped, so that window is never examined. Here the terminating pack header lands inside it and is missed outright — the unbounded PES runs on past its real end, swallowing the following unit. That is a CORRECTNESS failure, not a slow path, and the assertion on the emitted packet catches it.pes_at - pos->pes_at: the stale offset no longer equals the PES's post-drainsc, the resume arm stops matching and the search restarts at the PES header. Caught byboundary_bytes_scanned, which is why the fixture puts 64 KiB of payload in the SAME chunk that opens the PES: that is exactly the span a restart re-examines, so the mutant roughly doubles the bytes scanned.
an_unterminated_pes_is_not_rescanned_from_its_header_every_feed cannot
reach either: it opens the unbounded PES as the very FIRST bytes of the very
first feed, so nothing ever drains ahead of it, pos stays 0 and the
subtraction is a no-op. The comment above it asserts neither component can
underflow; nothing exercised the arithmetic at all.
So this fixture puts COMPLETE PS units — a pack header and a length-bounded
PES — ahead of the unbounded video PES in the same chunk. The loop
consumes them, breaks on the unbounded PES, and drains pos bytes with
pending_scan live: exactly the real DVD shape, where a video PES opens
partway through a read batch.
Test: parse_stream_id_extension_walks_every_optional_field_to_the_right_offset
parse_stream_id_extension walks every optional PES-header field (PTS/DTS,
ESCR, ES_rate, DSM_trick_mode, additional_copy_info, PES_CRC) and every
optional PES_extension sub-field (PES_private_data, pack_header_field,
program_packet_sequence_counter, P-STD_buffer) before reaching
stream_id_extension. Every one of those skips is a pos +=; a single
mutated increment (-=/*=) misaligns every read after it. This test arms
EVERY optional field at once with a known byte count, so any single wrong
skip anywhere in the chain lands on the wrong byte and the assertion fails —
one test proving the whole walk, rather than one per field.
Test: length_bounded_pes_exact_fit_is_emitted_not_awaited
A length-bounded PES (pes_packet_len != 0) must be emitted the moment its
declared length is EXACTLY satisfied by the buffer (sc + 6 > len, then
e = sc + 6 + pes_packet_len; e > len), not held back waiting for a byte
that will never arrive. Feed nothing after the packet and don't flush — if
the boundary checks were >= instead of >, an exact fit would incorrectly
be treated as "not enough data yet" and the packet would never be produced.