Number vendor label streams by STN slot, not by parsed entry

Sweep of every parser in src/labels/ for the numbering bug fixed in
pixelogic: a blob lists one entry per STN slot, but the parser advances
its per-kind counter only for entries it can use, so every entry it
skips shifts all later labels onto the wrong stream.

Three parsers were affected; the rest key each label off a number the
blob states outright and are immune.

  * paramount — `aud` / `sub` are the STN-ordered stream lists, and
    `forced_sub` / `*_com1_idx` index those same cells. A cell with an
    empty language was skipped without consuming its slot, so every
    label behind it bound one stream early while the vendor's own
    positional indices still pointed at the raw cell. `stream_number`
    is now the cell's 1-based position. The forced flag is the payload
    here, so the shift lands `forced` on a full-dialogue track.

  * mpls_universal — its counters must agree with the stream list
    `disc::bluray` builds from the same STN entries, since that list is
    what `apply_labels` counts against. They disagreed twice: a
    `coding_type == 0` padding entry was counted here and dropped
    there, and a PG coding_type in an audio STN slot (a layout
    `mpls::parse_stream_entry` has a dedicated arm for) was counted as
    audio here and built as a subtitle there. Both rules now live in
    one `label_type_for`.

  * deluxe — a binding construction whose Language `getstatic` did not
    resolve was skipped outright. It is still an STN slot; it just has
    nothing to label. It now advances the counter, with the list it
    belongs to taken from its CodingType argument or, failing that,
    from what its binding type's resolved siblings showed.

Two paramount tests asserted the renumbering as if it were the spec
(`empty_middle_slot_does_not_inflate_stream_number`,
`audio_stream_numbering_skips_empty_slots`) and are rewritten. Immunity
pins added for ctrm, dbp and criterion so the property cannot rot.

Also scrubs two commercial disc titles from the menu-graphic filename
examples in png_filenames and vocab.
This commit is contained in:
Matthew Jackson
2026-08-02 15:51:09 -07:00
parent f53abfe0d4
commit 9fda690d00
8 changed files with 534 additions and 57 deletions
+35
View File
@@ -263,6 +263,41 @@ mod tests {
assert_eq!(nums, vec![1, 2, 1]);
}
/// Immunity pin. `parse_stream_infos` emits one `StreamInfo` per
/// `*StreamInfos` element unconditionally — no filter, no `continue` — so
/// an element whose fields are missing or unrecognized still occupies its
/// position, and `assign_stream_numbers` still spends a number on it.
///
/// That is the property that keeps this parser out of the failure mode
/// where a skipped entry pulls every later label one stream forward. It
/// is load-bearing for the fallback path specifically: with no
/// `playbackconfig.xml` the numbers come purely from position in this
/// list, so dropping an element there would shift the rest.
///
/// Mutation: skip elements with an empty `ID`/`LangInfoID` → the two
/// real audio streams renumber to 1 and 2.
#[test]
fn unusable_stream_element_still_occupies_its_position() {
let sp = r#"
<AudioStreamInfos><ID>a0</ID><LangInfoID>ENG_US</LangInfoID></AudioStreamInfos>
<AudioStreamInfos></AudioStreamInfos>
<AudioStreamInfos><ID>a2</ID><LangInfoID>FRA</LangInfoID><Content>COMMENTARY</Content></AudioStreamInfos>
<SubtitleStreamInfos><ID>s0</ID><LangInfoID></LangInfoID><Qualifier>WAT</Qualifier></SubtitleStreamInfos>
<SubtitleStreamInfos><ID>s1</ID><LangInfoID>ENG</LangInfoID><Qualifier>SDH</Qualifier></SubtitleStreamInfos>
"#;
let infos = parse_stream_infos(sp);
assert_eq!(infos.len(), 5, "every element yields a StreamInfo");
let nums =
assign_stream_numbers(&infos, &HashMap::new()).expect("numbering space not exhausted");
assert_eq!(
nums,
vec![1, 2, 3, 1, 2],
"the blank element owns audio slot 2, so the commentary is slot 3"
);
assert_eq!(infos[2].purpose, LabelPurpose::Commentary);
assert_eq!(infos[4].qualifier, LabelQualifier::Sdh);
}
#[test]
fn fallback_does_not_collide_with_partial_map() {
// Map claims audio "a1" -> 1. The unmapped audio "a0" must NOT