Files
libfreemkv/docs/vocab.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
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.
2026-09-02 12:40:55 -07:00

146 lines
7.0 KiB
Markdown

# `src/labels/vocab.rs` — shared label vocabulary
Labels come from BD-J authoring tool files (bluray_project.bin,
playlists.xml, menu_base.prop, .class string pools, etc.) — NOT from BD
spec fields. This module is the central, regression-tested source of
truth for:
- Codec brand name aliases (`MLP` → `TrueHD`).
- English / multi-word language name → ISO 639-2 code.
- English text → `LabelPurpose` (Commentary / Descriptive / etc.).
- English text → `LabelQualifier` (SDH / Forced / Descriptive Service).
Not for BD spec STN codec IDs; those decode in `mpls.rs` separately.
## Rules of engagement
1. Only map values we are 100% certain about — published codec names,
well-known ISO 639-2 mappings, vendor-documented purpose keywords.
2. Unknown codes / unrecognized phrases pass through raw or return
`None`. We never guess.
3. Matching is case-insensitive and word-boundary-aware where relevant
(so "Commenter" doesn't match "commentary"). Anchoring on whole
tokens is the responsibility of this module — callers pass raw
text, we handle it.
## `lang()` — free-form language name → ISO 639-2 + variant
Handles both bare English names ("English", "Spanish") and the
multi-word vendor variants seen in the corpus ("Brazilian Portuguese",
"Castilian Spanish", "Canadian French"). Match is case-insensitive.
Compound phrases are scanned BEFORE bare names, so "Brazilian
Portuguese" returns `LangInfo { code: "por", variant: "Brazilian" }`
rather than being consumed by the bare "Portuguese" entry. Within
`COMPOUND_LANGS` the scan is positional (first `contains` hit wins), so
that table MUST be maintained longest-first — a longer phrase must
precede any shorter phrase it contains (e.g. "latin american spanish"
before "latin spanish").
Bare-name matches return `variant: ""`. Returns `None` for unrecognized
input — callers decide whether to fall back to MPLS spec codes, pass
through raw, or drop the stream. Never guesses.
Why the variant: returning only the ISO code would silently drop
regional dialect info — "Brazilian Portuguese 5.1" would become
`language="por", variant=""` and the UI would display plain
"Portuguese" even though the disc explicitly labeled the stream
Brazilian. Returning the variant lets callers populate
`StreamLabel::variant` with the dialect.
## `iso639_1_to_iso639_2()` — the whole ISO 639-1 set
Covers the WHOLE of ISO 639-1, unlike `menu_lang`, whose table only
spans the languages that show up in Blu-ray menu-graphic filenames.
Callers that convert a spec field — a DVD IFO attribute block, say —
need the whole set: narrowing it to the menu vocabulary would fold
every other language onto one value and make a disc's tracks
indistinguishable from each other.
`ISO_639_1_TO_2` pairs every ISO 639-1 two-letter code with its ISO
639-2/**T** (terminological) code — the variant the rest of this crate
uses (`deu` not `ger`, `fra` not `fre`, `zho` not `chi`, plus `ces`,
`nld`, `ell`, `ron`, `slk`, `isl`, `eus`, `hrv`) — so `lang` and
`menu_lang` both normalize to it and the three tables cannot disagree
(`iso639_1_agrees_with_menu_lang` pins that). For the 165 codes where
639-2/B and /T are identical this distinction does not arise; it only
matters for the 20-odd languages with a distinct bibliographic code.
`ISO_639_1_DEPRECATED` covers the three two-letter codes ISO 639-1 has
since withdrawn, mapped to their replacements. DVD-Video froze its
language list on the 1988 edition, so discs authored to the spec carry
these spellings and no other table sees them: `iw` Hebrew (now `he`),
`in` Indonesian (now `id`), `ji` Yiddish (now `yi`).
## `purpose()` / `qualifier()` — English text → enum
`purpose()` recognized keywords (case-insensitive; single-word keywords
are word-boundary matched, multi-word phrases are substring matched):
- "commentary", "director's commentary" → `Commentary`
- "descriptive", "description", "audio description", "described" → `Descriptive`
- "score", "music only" → `Score`
- "ime" (alternate music for closing themes etc.) → `Ime`
- anything else → `Normal`
Word-boundary matching means "Commentary track" matches but "Commenter
Pro audio" does not. Multi-word phrases like "audio description" and
"music only" are matched as plain substrings.
`qualifier()` recognized keywords (case-insensitive, word-boundary
matched):
- "sdh", "captions" → `Sdh`
- "forced", "forced narrative" → `Forced`
- "rnib", "descriptive service" → `DescriptiveService`
- anything else → `None`
SDH (Subtitles for the Deaf and Hard of hearing) wins over Forced when
both keywords are present, because an SDH track is its own stream
regardless of whether the player flags it as "forced".
## `has_word()` — word-boundary primitive
The load-bearing primitive for `lang` / `purpose` / `qualifier`:
bare-token matchers MUST use it, otherwise we match "english" inside
"englishman" and "sdh" inside "lambdash". The existing parsers used
`.contains()` and got lucky on the corpus; vocab guarantees the
boundary.
Char-aware, not byte-level: an accented/CJK char adjacent to the match
is alphanumeric and so NOT a boundary, preventing false positives like
"sdh" inside "cafésch". Needles are ASCII, so byte offsets align with
char bounds.
## Test rationale (mutation-testing notes)
- `purpose_descriptive_service_substring_without_word_boundary`: the
multi-word-compound fast path in `purpose()` ORs two independent
phrase checks ("audio description" / "descriptive service"). Each
phrase, when it appears as a *word*-bounded match, is independently
caught by the `has_word` fallback further down — so the OR only
matters when a phrase appears as a *substring inside a larger word*
(no boundary), which `.contains()` still catches but `has_word()`
would reject. Mutation: replace `||` with `&&` at the compound check
→ since "audio description" is absent in the test input, the AND
fails, the fast path doesn't fire, and the fallback
`has_word("descriptive")` also fails (no word boundary before
"descriptive" in "nondescriptive"), so `purpose()` wrongly returns
`Normal` instead of `Descriptive`.
- `menu_lang_covers_every_table_entry`: `menu_lang()` maps every
authoring-filename token in its table (ISO-639-2/B and /T spellings,
plus ISO-639-1) to the canonical /T code used by the rest of the
pipeline. Exhaustive per-arm check: deleting any single match arm
makes that arm's tokens return `None` instead of the documented code.
- `iso639_1_table_is_complete_and_well_formed`: structural invariants
of `ISO_639_1_TO_2` — it must hold the complete ISO 639-1 set (184
codes), every key a distinct pair of lowercase letters and every
value three lowercase letters. A typo'd or duplicated row fails here
rather than silently mislabelling a track.
- `iso639_1_agrees_with_menu_lang`: the two tables must not disagree.
Every two-letter token `menu_lang` accepts has to yield the same ISO
639-2/T code through `iso639_1_to_iso639_2`, so a DVD-sourced
language and a Blu-ray menu-label language for the same tongue never
produce different `Language` elements.