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.
26 KiB
src/labels/mod.rs — design notes
Long-form rationale relocated from doc comments per the comment-guard
audience cap (internal items cap at 3 lines; see ci/comment-guard.py).
label_at
The VENDOR label occupying 1-based STN slot n of stream_type, if any.
Slot lookup is restricted to labels with no StreamId, and that
restriction is the whole point. stream_number is not one coordinate
system, it is two that share a name:
- on a vendor label it is a slot in the one stream table the config blob describes, which is what this function's callers count against; while
- on a derived label it is that stream's slot in its own playlist's table, which says nothing about any other playlist's numbering.
Reading a derived label out of a slot lookup therefore answers a question
about table A with a fact about table B. A label that carries a StreamId
names the stream it describes outright and is bound by that id instead —
see apply_labels.
find_anchor
Find the title the label list is actually describing, for one stream type.
A vendor label list is a single stream table — one playlist's STN slots — but it is handed to every title on the disc. When sibling playlists cover the same clip with different stream subsets, per-title ordinal numbering puts the same label on different physical PIDs in each, and the flags contradict each other. The list itself carries no playlist id, but it does carry a language per slot, and that sequence is a fingerprint: on the corpus, exactly one title's per-type language sequence reproduces the label list position for position, and content confirms that title's binding is the correct one.
So: the anchor is the title that anchor_score admits and that confirms
the most of the list. None means no title matches — the list describes a
stream table this disc scan cannot see, and nothing may be bound
authoritatively.
Only VENDOR slots take part. Derived labels (MPLS / CLPI) name their own stream and bind directly, so admitting them here would be asking a title to agree with a numbering that describes a different playlist — the mistake that made this gate a coin flip while the merged list was the only list there was.
anchor_score
How strongly this title's stream sequence matches the vendor label list, or
None if the title contradicts it and cannot be the table it describes.
A stream disqualifies the title when the vendor label on its slot states a different language. A slot the vendor list does not name constrains nothing: under-yield is the normal shape of these blobs — an authoring layer ships editorial labels for the streams it considers interesting and leaves the rest as bare slots — so a hole is the list's silence, not a disagreement.
The score is how much of the list the title positively CONFIRMS: slots where both sides state a language and state the same one. A list that names no languages at all scores zero everywhere and the ranking falls through to table size, which is what it did before this became a fingerprint.
apply_labels
Apply a pre-extracted set of labels to titles' streams.
Every label either NAMES the stream it describes or it does not, and that split — not a slot number — decides how it binds. Four tiers, most certain first:
-
This title is the anchor.
find_anchoridentifies the title whose stream table the vendor list is describing, so its slots are the list's own slots and bind directly. -
An anchor proved this PID. Every slot of the anchor yields a
(clip, PID) -> labelfact, and a PID is the same physical stream in every playlist that plays that clip. Sibling playlists bind through that map, so a vendor label lands on the same elementary stream no matter which playlist enumerates it, and a slot the anchor never showed us is not bound at all. -
The label names this stream. A derived label carries a
StreamIdread out of the very table the stream itself was built from, so(clip, PID)equality is identity, not inference. This is the floor that gives every stream its language and codec. -
Ordinal, language-checked. A vendor label with no anchor behind it falls back to the 1-based per-type STN slot — but only where tier 3 had nothing to say, and only when the languages do not contradict.
Tier 3 outranking tier 4 is the substance of this ordering: a label that
states which stream it belongs to beats a guess about which stream it might
belong to, even when the guess is the richer label. The policy where a title
cannot be bound confidently is to leave the stream alone: an unlabelled track
is a much smaller harm than a full-dialogue track wearing forced, which
presents to the user as a duplicate of the track they wanted, and which the
muxer can only undo on discs that state forced_on_flag. A subtitle label
carries nothing but the qualifier, so an unverifiable one has no upside at
all to trade against that risk, and the ordinal path applies one only when
the label and the stream both state a language and it is the same one.
Audio streams update purpose + label (codec/variant info; never
English purpose text). Subtitle streams update qualifier and the
forced flag.
Extracted from apply() so the matching logic is unit-testable without
needing a SectorSource / UdfFs.
merge_mpls_floor
Merge the MPLS-derived floor into a framework parser's label list.
The two lists do not share a coordinate system, and the merge must not
pretend they do. A framework stream_number is a slot in the one stream
table the vendor blob describes; an MPLS label's is that stream's slot in
its own playlist's table. Matching them by number — which is what this
function used to do — merges by an equality that means nothing, and the
merged entries then land on whatever stream happens to sit at that ordinal
in each title.
So nothing is merged BY slot. Every MPLS label whose stream is not already
named in framework is appended, keeping its own StreamId, and
apply_labels decides per stream which of the two reaches it: the
vendor's editorial label where an anchor puts it there, the floor
everywhere else. Framework labels stay richer and stay ahead — they are
simply no longer competing for a slot number.
Sorted at the end (vendor slots first, in slot order, then the named streams) so callers see a deterministic list.
append_clpi_orphans
Append CLPI ProgramInfo streams that no existing label already names. These are "orphan" streams — physically present in the .m2ts per CLPI's clip-authoritative view, but no MPLS playlist references them, so the framework and the MPLS floor both missed them. Empirically they are commentary or alternate-version streams the authoring tool left out of the published playlist. Returns the number appended.
Each carries the (clip, PID) it was read under, so it binds to that
stream and to nothing else. It used to be given stream_number = max(existing per type) + 1, +2, …, an ordinal invented here and shared
with the slot numbering the vendor list uses — which meant a title with
more streams than the list had slots could reach an orphan by counting, and
be labelled from a stream no playlist even plays. There is no ordinal now:
a stream that is in no playlist is in no title, so an orphan label binds to
nothing, which is exactly right and is now structural rather than lucky.
select_result
Pick the winning parser result from results (built in PARSERS order):
highest Confidence among non-empty results, with the earliest array
position winning on a tie — matching extract()'s strict-> first-wins
scan.
Iterator::max_by_key returns the LAST maximal element, so the key is
(confidence, Reverse(index)): among equal-confidence entries the one
with the smallest index has the largest Reverse(index) and is selected,
i.e. first wins.
collect_chapter_summary
Scan /BDMV/PLAYLIST/*.mpls, parse each, return a row per playlist with
chapter count (entry marks only) and total duration. Sorted by playlist
filename. Skipped entries (read error, parse error, no marks) silently
dropped — this is a diagnostic field, not a correctness-critical one.
jar_inventory
List filenames found under any /BDMV/JAR/<x>/ subdirectory of the disc.
Deduped, sorted. Returns an empty vec if no JAR dir is present.
pub(crate) so filename-based parsers (e.g. png_filenames) can scan
menu-asset names without a reader.
jar_inventory_from
The body of jar_inventory, over the /BDMV/JAR children directly, so
it is unit-testable without a UdfFs.
A BTreeSet, not Vec::contains: the entry names come from the disc's own
UDF directory records, so both the file count and the name lengths are
attacker-controlled, and a linear contains doing a full String compare
per candidate is quadratic in the number of files. The set also subsumes
the trailing sort — it yields sorted, deduplicated output directly.
test: jar_inventory_dedup_does_not_hang_on_a_hostile_directory
jar_inventory deduplicated with a linear Vec::contains, doing a full
String comparison per candidate — quadratic in a file count taken
straight from the disc's UDF directory records, with attacker-chosen
name lengths to inflate each comparison.
This is a HANG GUARD, and the name says so: a return to the linear scan
makes this fixture run for minutes (120 000² / 2 comparisons over a
180-byte shared prefix), which without the deadline would wedge CI
rather than fail it. It is not a complexity proof — no assertion here
can distinguish BTreeSet from any other sub-quadratic dedup, and the
clock-free half of the claim (dedup, sort, directory exclusion) belongs
to jar_inventory_dedups_sorts_and_skips_dirs below.
The deadline is a real margin, unlike the 6x one that made
paramount.rs's wall-clock test flake under a loaded CI box: measured
at 0.14 s debug / 0.07 s release against 10 s, so ~70x. A shared CPU
does not close that; a quadratic dedup does not survive it.
test: parsers_registry_order_locked
Lock the parser roster + order. If someone reorders the array or
adds/removes a parser, this test forces them to update the expectation
explicitly. The order is load-bearing: first matching parse() wins, so
reordering changes which parser claims a disc on overlapping detect
signals.
dbp + deluxe MUST stay at the end (their detect triggers on "any BD-J disc"; placing them earlier would short-circuit the stricter parsers above them).
test: select_result_first_wins_on_tie
select_result must pick the highest-confidence non-empty result and,
on a confidence tie, the FIRST in array order (regression for the old
analyze() max_by(...then(Equal)) no-op that picked the LAST).
extract() — the path that actually ships — used to re-derive this
same rule with its own inline > scan and had no test at all, so the
bug this test guards against could have recurred there unnoticed. It
now calls select_result, so this test covers both.
test: suppression_is_by_named_stream_not_by_slot_number
Spec: the merge suppresses a floor entry only when the list already NAMES that stream. A framework label occupying the same slot number is not the same fact — a vendor slot and a playlist STN slot are different coordinate systems — so it suppresses nothing.
This is the merge rule inverted from what it used to be: the old key was
(stream_type, stream_number), which dropped a floor entry whenever
some unrelated vendor slot happened to share its number, and kept one
whenever it did not. Both outcomes were decided by an accident of
counting.
test: log-injection guard on playlist-name logging
A playlist name is a raw UDF directory entry — disc-controlled bytes,
validated no further than a lossy UTF-8 decode. Logging it through
tracing's % (Display) sigil writes those bytes VERBATIM, so a crafted
.mpls filename carrying ANSI escapes or control characters forges
terminal output and log structure in any consumer rendering the event
(CWE-117). ? (Debug) escapes them, and str's Debug is exactly the
escaping this needs.
info! is not covered by the debug/trace-logging exemption: this fires
on an ordinary rip of an ordinary disc.
Mutation: put % back on playlist in apply_labels and this goes red.
test: forced_label_follows_the_pid_not_the_ordinal_across_sibling_playlists
The cross-playlist mis-binding this module's two-tier binding exists to
stop, in its measured shape: two playlists cover the identical feature
clip, one of them enumerates one extra subtitle ahead of the forced
slots, and the vendor label list describes the shorter of the two.
Numbering the same list from 1 inside each title puts the Forced
label on a different PID in each — in the longer playlist, on the full
dialogue track. The user then sees two identical-looking English
subtitle tracks, one of them wrongly flagged forced.
Binding by the PID an anchored title proved, rather than by ordinal,
puts forced on the same two physical streams in both playlists and
leaves the extra one alone.
test: ordinal_binding_drops_a_subtitle_label_that_contradicts_the_stream_language
No anchor to work from (nothing on the disc reproduces the label list's
language sequence), so binding falls back to the STN ordinal — and a
label that names a different language than the stream it would land on
is evidence the list is describing some other stream table. Drop it:
unlabelled beats mislabelled, because the payload here is forced.
test: generate_video_label_secondary_dolby_vision_el
Spec: a secondary (dependent-view) video stream with Dolby Vision
enhancement layer gets the brand string "Dolby Vision EL"; every other
HDR format on a secondary stream gets no label at all (that wording is a
CLI concern). Mutation: delete the HdrFormat::DolbyVision arm so it
falls through to the _ => String::new() catch-all, losing the brand.
test: generate_video_label_480_boundary
Spec: 480 lines is the SD floor — a stream with height exactly 480 must
get the "480p"/"480i" token (BD spec height boundary), not fall through
to the empty-resolution case. Mutation: h >= 480 -> h < 480 inverts
the boundary so a legitimate 480-line stream (h == 480) produces no
resolution token at all.
test: generate_video_label_sdr_produces_no_hdr_token
Spec: SDR is the unmarked default — it must never appear as a token in
the generated label (only non-SDR formats get an explicit tag).
Mutation: delete the HdrFormat::Sdr arm so it falls through to
_ => parts.push(hdr.name()), appending a spurious "SDR" token.
test: generate_audio_label_atmos_folds_brand
Spec: the Atmos-aware variant folds "Atmos" into the codec brand name
for TrueHD/DD+ carriers, distinct from the plain wrapper. Mutation: stub
the whole function to String::new() / a constant literal — either way
it stops reflecting the codec/channel inputs.
test: generate_audio_label_covers_pc_container_codecs
Spec: every disc-audio codec in the enum has a full marketing name,
including the lossy PC-container codecs (AAC/MP2/MP3/FLAC/Opus) that
generate_audio_label_all_codecs above doesn't cover. Mutation: delete
any one of these match arms — the codec falls through to
_ => return String::new(), silently losing its label.
codec_hint_consistent: chained-OR boundary hardening
The family-detection booleans chain h.contains(..) || ... synonym
checks. Each test below isolates ONE clause so a || -> && flip there
changes the verdict.
codec_hint_consistent_truehd_space_synonym
Isolates the "true hd" (space form) synonym in says_truehd, which
mutant testing hit at 396:44's ||. If that || is weakened to &&,
"True HD" alone (no "truehd" substring) no longer sets says_truehd,
names_family goes false entirely (no other family clause matches), and
the function takes the "no family named" early-return path — turning a
should-be-false verdict for a mismatched codec into true.
codec_hint_consistent_ddp_ac3_plus_no_hyphen_synonym
Isolates the "ac3+" (no-hyphen) synonym in says_ddp (398:9's ||).
A hint matching only this clause must still classify as DD+, not fall
through to the plain-AC3 says_ac3 check.
codec_hint_consistent_ddp_eac3_synonym
Isolates the "eac3" synonym in says_ddp (401:9's ||), the last
clause before the chain moves to "digital plus"/"dd+".
codec_hint_consistent_lpcm_bare_pcm_synonym
Isolates the "pcm" (no "lpcm") synonym in says_lpcm (409:40's ||).
A bare "PCM" hint on a non-LPCM stream must still be judged
inconsistent — if the || were &&, "PCM" alone would fail to set
says_lpcm, names_family would go false, and the function would take
the "no family named" path, wrongly returning true for ANY codec.
codec_hint_consistent_names_family_dts_ma_alone
Isolates the says_dts_ma || says_dts_hr disjunction inside the
names_family chain (418:60). A hint that sets says_dts_ma alone
(e.g. "Master Audio", without "hd ma") must still make names_family
true; weakening that || to && requires both clauses at once, so
names_family goes false and the function wrongly reports "consistent"
for a codec the hint never named.
codec_hint_consistent_truehd_arm_atmos_alone
Isolates the Codec::TrueHd => says_truehd || says_atmos arm (433:38).
An Atmos-tagged hint that names a DIFFERENT lossless carrier by name
(DD+) must still be judged consistent with a TrueHd stream purely on the
Atmos marker — || -> && would require the hint to ALSO say "truehd",
which an Atmos-only marker doesn't.
codec_hint_consistent_dts_arm_not_bypassed
Spec: Codec::Dts is consistent ONLY when the hint's DTS-family
bookkeeping (says_dts) is true, not just because names_family is
true via some other carrier. Mutation: delete the Codec::Dts => says_dts arm (438:9) — it falls to _ => true, so ANY named family is
(wrongly) "consistent" with a Dts stream.
codec_hint_consistent_lpcm_arm_not_bypassed
Spec: Codec::Lpcm is consistent ONLY when says_lpcm is true.
Mutation: delete the Codec::Lpcm => says_lpcm arm (439:9) — same
bypass-to-_ => true failure mode as the Dts arm above.
test: slot_lookup_never_returns_a_label_that_names_its_own_stream
Spec: a label that names its own stream is never reachable through the slot lookup, whatever number it carries.
The two numbers are different coordinate systems. A derived label's
stream_number is its slot in ITS OWN playlist's table; the vendor's is
a slot in the one table the config blob describes. label_at answers
questions about the second, so it must not return the first.
Mutation: drop the l.stream_id.is_none() term from label_at — the
derived label at slot 1 is found first and shadows the vendor's.
test: the_derived_floor_cannot_veto_the_anchor
Spec: the derived floor does not vote on which title anchors the vendor list — and in particular cannot VETO the title that does.
The vendor list names two subtitle slots, eng then fra, and the feature reproduces them. The feature has a third subtitle the vendor said nothing about; the merge dropped a derived label into "slot 3", numbered in a different playlist's coordinate system, and it says Spanish. Read as part of the vendor sequence that is a contradiction, so the feature is rejected, no title anchors, and every flag the list carries is lost.
This is the measured cost of a whole-sequence gate without provenance: the vendor's own slots stay correctly positioned while the merged ones break the sequence between them.
Mutation: drop the l.stream_id.is_none() term from label_at — the
derived "spa" is read as vendor slot 3, anchor_score returns None,
and there is no anchor.
test: the_anchor_is_the_title_that_confirms_most_of_the_list
Spec: among titles the vendor list admits, the one that CONFIRMS more of it wins — a slot where both sides state a language and state the same one is evidence; a slot where either is silent is not.
Here the shorter title matches both named slots outright while the longer one states no language at all, so it merely fails to contradict. Size alone would hand the anchor to the title that proved nothing.
Mutation: rank on n alone (drop score from the comparison) — the
three-stream title with no languages wins.
test: a_vendor_qualifier_does_not_leak_onto_a_featurettes_own_stream
Spec: an editorial qualifier the vendor list states for ONE stream table does not leak onto a different physical stream in a title that merely counts to the same ordinal.
Measured shape: the feature carries eleven subtitles whose first is SDH; a dozen featurettes each carry a single English subtitle, on a different clip and a different PID. Both are "subtitle number 1", so the ordinal fallback put SDH on all of them — and the languages agree, so the language gate could not catch it. What separates them is that the featurette's stream is named by the derived floor, and a label that names a stream outranks a label guessed onto it.
Mutation: move the by_id lookup in resolve after the ordinal
fallback — the guess wins again and the featurette is SDH.
test: an_anchor_proves_pids_only_for_the_clip_its_table_came_from
Spec: the anchor proves a (clip, PID) fact only for the clip its
stream table was READ FROM — the first play item — never for every clip
the anchor happens to play.
disc::bluray builds a title's stream list from play_items[0]'s STN
table, and tier 3 fifty lines below says exactly that by keying its
derived ids on clip0. Tier 2's harvest contradicted it: it recorded
the anchor's slot PIDs against EVERY clip the anchor plays, so a sibling
playlist that plays a LATER clip of the anchor bound the anchor's
editorial label onto whatever stream in that clip happens to reuse the
PID — a different physical stream, in a different clip, whose own
language says so.
Mutation: harvest over &title.clips instead of its first clip — the
featurette wears the feature's SDH again.
test: a_vendor_codec_claim_does_not_follow_the_ordinal_onto_a_bonus_clip
Spec: a vendor codec/variant claim does not follow the ordinal onto a bonus clip that carries a different codec.
Measured shape: the feature's first audio is object audio; a dozen menu and bonus titles each carry one plain stereo track. All of them are "audio number 1", and the hint names the same codec family as the stream it lands on, so the consistency guard passes it through and every bonus clip advertises the feature's format.
Mutation: as above — move by_id after the ordinal fallback.
test: a_title_shorter_than_the_vendor_list_cannot_anchor_it
Spec: a title whose stream count is smaller than the vendor list's highest slot cannot be the table that list describes.
Mutation: drop the n < extent term from find_anchor — the one-stream
title becomes eligible, and on a disc where it sorts first it takes the
anchor away from the title that actually has the slots.
test: an_unnamed_slot_does_not_disqualify_a_title
Spec: a slot the vendor list never names constrains nothing.
These blobs under-yield by design — the authoring layer ships editorial labels for the streams it finds interesting and leaves the rest as bare slots. Treating a hole as a disagreement rejects the very title the list describes. (Before provenance the holes were filled with labels numbered in an unrelated coordinate system, so the gate neither rejected nor admitted on evidence.)
Mutation: make anchor_score return None for an unnamed slot — no
title anchors and the forced flag is never delivered.
test: fill_gaps_leaves_order_untouched_when_nothing_added
Spec: the sort-by-(type, number) pass only runs when the merge actually
added something (added > 0); when MPLS contributed nothing new,
framework's existing order (however the caller built it) must be left
untouched. Mutation: added > 0 -> added >= 0 is always true, so the
sort runs unconditionally, silently reordering a framework list that
wasn't already in (type, number) order even on a no-op merge.
build_program_info / build_clpi (test fixtures)
build_program_info: builds a CLPI ProgramInfo section for one program
with the given (pid, stream_coding_info) pairs. Layout mirrors
crate::clpi::parse_program_info's expectations: length(4) + reserved(1)
- num_programs(1), then per-program spn(4)+pmt_pid(2)+num_streams(1) +num_groups(1), then per-stream pid(2)+sci_len(1)+sci.
build_clpi: builds a full CLPI byte buffer (HDMV header + ProgramInfo)
declaring the given (pid, coding_type, lang) streams. sci layout
follows crate::clpi::parse_program_info's per-coding-type match arms:
PG/IG = coding_type + 3-byte lang; audio (primary or secondary) =
coding_type + format/rate byte + 3-byte lang.
test: orphans_state_no_stn_slot_and_name_their_stream
(d) An orphan states NO STN slot, and is identified by the stream it was read from instead.
This test used to assert the opposite — that orphans continue the slot
numbering from max(existing) + 1. That number was invented here and
shared with the coordinate system label_at counts vendor slots in, so
a title with more streams of a type than the vendor list had slots could
reach an orphan by counting and be labelled from a stream no playlist
plays. An orphan is by definition in no playlist, hence in no title, so
there is no ordinal to give it.
codec_hint_consistent
Does the parser's codec_hint name a codec consistent with the stream's
actual codec? apply_labels uses this to keep richer-but-consistent
hints (e.g. "Dolby Atmos" on a TrueHD stream — Atmos is a TrueHD extension
the raw spec codec can't express) while rejecting mis-bound ones (e.g.
"AC-3 2.0" on a TrueHD stream, the shuffled-label bug). Matching is by codec
FAMILY parsed out of the hint string. "Atmos" with no carrier named is
treated as compatible with its lossless carriers (TrueHD / E-AC-3). A hint
naming no recognizable codec family (pure editorial, e.g. "Commentary") is
consistent — it isn't asserting a codec.
generate_audio_label_atmos
Atmos-aware variant: same codec/channel string as generate_audio_label
with the object-audio marker folded into the codec brand (e.g. "Dolby
TrueHD Atmos 7.1"). The "Atmos" string lives here in the label layer, not
in the core parser. Used when a bitstream probe detected an Atmos
substream and the stream still carries the basic (non-editorial) label.