libfreemkv 0.31.2

Open source raw disc access library for optical drives
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
//! Shared label vocabulary — canonical mappings used by ≥1 label parser.
//!
//! 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).
//!
//! 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.
//!
//! This module is NOT for BD spec STN codec IDs; those decode in
//! `mpls.rs` separately.

use super::{LabelPurpose, LabelQualifier};

// ── Codec aliases ────────────────────────────────────────────────────────────

/// Map a codec identifier found in label data to its display name.
///
/// These are well-known codec identifiers used across multiple BD-J
/// authoring tools. Matching is case-insensitive (on-disc tokens vary:
/// `ATMOS`, `Atmos`, `atmos`). Unknown codes pass through unchanged (in
/// their original casing) so callers can still surface vendor-specific
/// tokens we haven't catalogued.
pub fn codec(code: &str) -> &str {
    match code.to_ascii_uppercase().as_str() {
        "MLP" => "TrueHD",
        "AC3" | "AC" => "Dolby Digital",
        "DDL" => "Dolby Digital Plus",
        "WAV" => "PCM",
        "ATMOS" => "Dolby Atmos",
        // "DTS" is recognized but has no distinct display alias — return
        // the original token rather than a re-cased copy.
        _ => code,
    }
}

// ── Language: English / multi-word names → ISO 639-2 ─────────────────────────

/// Result of [`lang`] — ISO code + human-readable regional variant.
///
/// `code` is ISO 639-2 (always 3 lowercase letters).
/// `variant` is the regional dialect as a human-readable English word
/// (`"Brazilian"`, `"Castilian"`, `"Canadian"`, `"Simplified"`, ...)
/// or `""` when the input names just a bare language without
/// dialect ("Spanish" → variant=""). It is a short display token
/// suitable for the [`StreamLabel::variant`](super::StreamLabel) field,
/// to be surfaced verbatim by the UI.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LangInfo {
    pub code: &'static str,
    pub variant: &'static str,
}

/// Map a free-form language label fragment to an ISO 639-2 code AND
/// (where applicable) its regional variant.
///
/// Handles both bare English names ("English", "Spanish") and the
/// multi-word vendor variants we've 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`](super::StreamLabel) with the dialect.
pub fn lang(text: &str) -> Option<LangInfo> {
    let lower = text.to_lowercase();
    // Multi-word compounds first. Scan is positional (first hit wins),
    // so COMPOUND_LANGS MUST stay ordered longest-first.
    for (needle, code, variant) in COMPOUND_LANGS {
        if lower.contains(needle) {
            return Some(LangInfo { code, variant });
        }
    }
    // Bare names: word-boundary match (avoid "english" inside "englishman"
    // or any other accidental substring).
    for (needle, code) in BARE_LANGS {
        if has_word(&lower, needle) {
            return Some(LangInfo { code, variant: "" });
        }
    }
    None
}

const COMPOUND_LANGS: &[(&str, &str, &str)] = &[
    ("brazilian portuguese", "por", "Brazilian"),
    ("euro portuguese", "por", "European"),
    ("european portuguese", "por", "European"),
    ("castilian spanish", "spa", "Castilian"),
    ("latin american spanish", "spa", "Latin American"),
    ("latin spanish", "spa", "Latin American"),
    ("canadian french", "fra", "Canadian"),
    ("parisian french", "fra", "Parisian"),
    ("australian english", "eng", "Australian"),
    ("austrailian english", "eng", "Australian"), // disc-corpus typo, keep matching
    ("british english", "eng", "British"),
    ("simplified chinese", "zho", "Simplified"),
    ("traditional chinese", "zho", "Traditional"),
    ("mandarin chinese", "zho", "Mandarin"),
    ("cantonese chinese", "zho", "Cantonese"),
];

const BARE_LANGS: &[(&str, &str)] = &[
    ("english", "eng"),
    ("spanish", "spa"),
    ("french", "fra"),
    ("german", "deu"),
    ("italian", "ita"),
    ("japanese", "jpn"),
    ("chinese", "zho"),
    ("mandarin", "zho"),
    ("cantonese", "zho"),
    ("portuguese", "por"),
    ("polish", "pol"),
    ("czech", "ces"),
    ("hungarian", "hun"),
    ("dutch", "nld"),
    ("korean", "kor"),
    ("arabic", "ara"),
    ("hindi", "hin"),
    ("turkish", "tur"),
    ("thai", "tha"),
    ("swedish", "swe"),
    ("norwegian", "nor"),
    ("danish", "dan"),
    ("finnish", "fin"),
    ("hebrew", "heb"),
    ("russian", "rus"),
    ("greek", "ell"),
    ("vietnamese", "vie"),
    ("indonesian", "ind"),
    ("malay", "msa"),
    ("ukrainian", "ukr"),
    ("romanian", "ron"),
    ("bulgarian", "bul"),
    ("croatian", "hrv"),
    ("serbian", "srp"),
    ("slovak", "slk"),
    ("slovenian", "slv"),
    ("estonian", "est"),
    ("latvian", "lav"),
    ("lithuanian", "lit"),
    ("icelandic", "isl"),
    ("basque", "eus"),
    ("catalan", "cat"),
    ("galician", "glg"),
];

// ── Purpose ──────────────────────────────────────────────────────────────────

/// Classify a free-form English label string into a [`LabelPurpose`].
///
/// Recognized keywords (case-insensitive, word-boundary 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.
pub fn purpose(text: &str) -> LabelPurpose {
    let lower = text.to_lowercase();
    // Multi-word compounds first — they're more specific.
    if lower.contains("audio description") || lower.contains("descriptive service") {
        return LabelPurpose::Descriptive;
    }
    if lower.contains("music only") {
        return LabelPurpose::Score;
    }
    if has_word(&lower, "commentary") {
        return LabelPurpose::Commentary;
    }
    if has_word(&lower, "descriptive")
        || has_word(&lower, "description")
        || has_word(&lower, "described")
    {
        return LabelPurpose::Descriptive;
    }
    if has_word(&lower, "score") {
        return LabelPurpose::Score;
    }
    if has_word(&lower, "ime") {
        return LabelPurpose::Ime;
    }
    LabelPurpose::Normal
}

// ── Qualifier ────────────────────────────────────────────────────────────────

/// Classify a free-form English label string into a [`LabelQualifier`].
///
/// 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".
pub fn qualifier(text: &str) -> LabelQualifier {
    let lower = text.to_lowercase();
    if has_word(&lower, "sdh") || has_word(&lower, "captions") {
        return LabelQualifier::Sdh;
    }
    if lower.contains("descriptive service") || has_word(&lower, "rnib") {
        return LabelQualifier::DescriptiveService;
    }
    if has_word(&lower, "forced") {
        return LabelQualifier::Forced;
    }
    LabelQualifier::None
}

// ── Internal: word-boundary matching ────────────────────────────────────────

/// True if `needle` appears in `haystack` surrounded by non-alphanumeric
/// boundaries (or string ends). `haystack` is assumed lowercase already.
///
/// This is 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.
fn has_word(haystack: &str, needle: &str) -> bool {
    if needle.is_empty() {
        return false;
    }
    // Boundary check is char-aware (not byte-level): a non-ASCII letter
    // adjacent to the match (e.g. an accented or CJK char, which is
    // multiple UTF-8 bytes) is alphanumeric and so is NOT a boundary,
    // preventing false positives like "sdh" inside "cafésch". Needles
    // are ASCII tokens, so a byte-offset match aligns with char
    // boundaries in `haystack`.
    for (idx, _) in haystack.match_indices(needle) {
        // Char immediately before the match.
        let before_is_alnum = haystack[..idx]
            .chars()
            .next_back()
            .is_some_and(char::is_alphanumeric);
        // Char immediately after the match.
        let after_is_alnum = haystack[idx + needle.len()..]
            .chars()
            .next()
            .is_some_and(char::is_alphanumeric);
        if !before_is_alnum && !after_is_alnum {
            return true;
        }
    }
    false
}

// ── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn codec_known_aliases() {
        assert_eq!(codec("MLP"), "TrueHD");
        assert_eq!(codec("AC3"), "Dolby Digital");
        assert_eq!(codec("AC"), "Dolby Digital");
        assert_eq!(codec("DDL"), "Dolby Digital Plus");
        assert_eq!(codec("atmos"), "Dolby Atmos");
        assert_eq!(codec("WAV"), "PCM");
        assert_eq!(codec("DTS"), "DTS");
    }

    #[test]
    fn codec_case_insensitive() {
        // On-disc casing varies; all forms must canonicalize.
        assert_eq!(codec("ATMOS"), "Dolby Atmos");
        assert_eq!(codec("Atmos"), "Dolby Atmos");
        assert_eq!(codec("atmos"), "Dolby Atmos");
        assert_eq!(codec("mlp"), "TrueHD");
        assert_eq!(codec("ac3"), "Dolby Digital");
    }

    #[test]
    fn codec_unknown_passes_through() {
        assert_eq!(codec("FX9"), "FX9");
        assert_eq!(codec(""), "");
        // Unknown tokens keep their original casing.
        assert_eq!(codec("Vendor_X"), "Vendor_X");
    }

    fn li(code: &'static str, variant: &'static str) -> LangInfo {
        LangInfo { code, variant }
    }

    #[test]
    fn lang_bare_names_have_empty_variant() {
        assert_eq!(lang("English"), Some(li("eng", "")));
        assert_eq!(lang("english"), Some(li("eng", "")));
        assert_eq!(lang("Spanish 5.1 Dolby Digital"), Some(li("spa", "")));
        assert_eq!(lang("japanese"), Some(li("jpn", "")));
        assert_eq!(lang("Italian"), Some(li("ita", "")));
    }

    #[test]
    fn lang_compounds_carry_variant() {
        assert_eq!(
            lang("Brazilian Portuguese 5.1"),
            Some(li("por", "Brazilian"))
        );
        assert_eq!(lang("Castilian Spanish"), Some(li("spa", "Castilian")));
        assert_eq!(
            lang("Canadian French Dolby Digital"),
            Some(li("fra", "Canadian"))
        );
        assert_eq!(
            lang("Latin American Spanish"),
            Some(li("spa", "Latin American"))
        );
        assert_eq!(lang("Simplified Chinese"), Some(li("zho", "Simplified")));
        assert_eq!(lang("British English"), Some(li("eng", "British")));
    }

    #[test]
    fn lang_compounds_win_over_bare() {
        // Brazilian Portuguese must map to (por, Brazilian) via the
        // compound rule, not be intercepted by bare "portuguese"
        // (which would yield (por, "") and lose the variant).
        assert_eq!(lang("Brazilian Portuguese").unwrap().variant, "Brazilian");
        assert_eq!(lang("Canadian French").unwrap().variant, "Canadian");
    }

    #[test]
    fn lang_unknown_returns_none() {
        assert_eq!(lang("Klingon Dolby Atmos"), None);
        assert_eq!(lang(""), None);
        assert_eq!(lang("eng"), None); // 3-letter codes are not English names
    }

    #[test]
    fn lang_word_boundary_avoids_substring_false_positive() {
        // No false positive — "engineering" must NOT match "english".
        assert_eq!(lang("Audio Engineering Demo"), None);
    }

    #[test]
    fn purpose_recognizes_commentary() {
        assert_eq!(purpose("English Commentary"), LabelPurpose::Commentary);
        assert_eq!(purpose("Director's Commentary"), LabelPurpose::Commentary);
    }

    #[test]
    fn purpose_recognizes_descriptive() {
        assert_eq!(
            purpose("English Descriptive Audio"),
            LabelPurpose::Descriptive
        );
        assert_eq!(purpose("Audio Description"), LabelPurpose::Descriptive);
        assert_eq!(purpose("Described Video"), LabelPurpose::Descriptive);
    }

    #[test]
    fn purpose_descriptive_service_routes_to_descriptive() {
        // "Descriptive Service" is qualifier territory but the purpose
        // implication is Descriptive — vocab::purpose treats it as such.
        assert_eq!(
            purpose("English Descriptive Service"),
            LabelPurpose::Descriptive
        );
    }

    #[test]
    fn purpose_word_boundary_avoids_commenter_false_positive() {
        // "Commenter Pro audio" does NOT match "commentary" — the
        // existing dbp/ctrm hand-rolls would have. Vocab is stricter.
        assert_eq!(purpose("Commenter Pro audio track"), LabelPurpose::Normal);
    }

    #[test]
    fn purpose_recognizes_score() {
        assert_eq!(purpose("Music Only"), LabelPurpose::Score);
        assert_eq!(purpose("Isolated Score"), LabelPurpose::Score);
    }

    #[test]
    fn purpose_unknown_is_normal() {
        assert_eq!(purpose("English Dolby Atmos"), LabelPurpose::Normal);
        assert_eq!(purpose(""), LabelPurpose::Normal);
    }

    #[test]
    fn purpose_recognizes_ime() {
        assert_eq!(purpose("IME"), LabelPurpose::Ime);
        assert_eq!(purpose("English ime"), LabelPurpose::Ime);
        // Word-boundary: "ime" inside "time" must not match.
        assert_eq!(purpose("Showtime audio"), LabelPurpose::Normal);
    }

    #[test]
    fn has_word_treats_non_ascii_letter_as_a_letter_boundary() {
        // A non-ASCII (multi-byte) letter glued to the needle is NOT a
        // word boundary, so the needle must not match there.
        assert!(!has_word("cafésdh", "sdh")); // 'é' precedes "sdh"
        assert!(!has_word("日本sdh", "sdh"));
        // But a real boundary (space / punctuation / non-letter) matches.
        assert!(has_word("café sdh", "sdh"));
        assert!(has_word("日本 sdh", "sdh"));
        assert!(has_word("sdh", "sdh"));
    }

    #[test]
    fn qualifier_recognizes_sdh() {
        assert_eq!(qualifier("English SDH"), LabelQualifier::Sdh);
        assert_eq!(qualifier("English Captions"), LabelQualifier::Sdh);
    }

    #[test]
    fn qualifier_recognizes_forced() {
        assert_eq!(qualifier("English Forced"), LabelQualifier::Forced);
        assert_eq!(qualifier("Forced Narrative"), LabelQualifier::Forced);
    }

    #[test]
    fn qualifier_recognizes_descriptive_service() {
        assert_eq!(
            qualifier("English RNIB"),
            LabelQualifier::DescriptiveService
        );
        assert_eq!(
            qualifier("English Descriptive Service"),
            LabelQualifier::DescriptiveService
        );
    }

    #[test]
    fn qualifier_sdh_wins_over_forced_when_both_present() {
        // SDH track is its own stream regardless of forced flag.
        assert_eq!(qualifier("English Forced SDH"), LabelQualifier::Sdh);
    }

    #[test]
    fn qualifier_unknown_is_none() {
        assert_eq!(qualifier("English"), LabelQualifier::None);
        assert_eq!(qualifier(""), LabelQualifier::None);
    }

    #[test]
    fn has_word_basic() {
        assert!(has_word("english forced", "english"));
        assert!(has_word("english forced", "forced"));
        assert!(has_word("english", "english"));
    }

    #[test]
    fn has_word_rejects_substring() {
        assert!(!has_word("engineering", "english"));
        assert!(!has_word("englishman", "english"));
        assert!(!has_word("aenglish", "english"));
    }

    #[test]
    fn has_word_punctuation_boundary() {
        // "(SDH)" is a valid boundary — parentheses count as non-alphanum.
        assert!(has_word("english (sdh)", "sdh"));
        assert!(has_word("commentary,extra,info", "commentary"));
    }

    // ── Additional hardening tests ─────────────────────────────────────────

    /// Spec: `MLP` is the Pixelogic token for Dolby TrueHD.
    /// AUDIO_CODECS in pixelogic lists it; vocab maps it to "TrueHD".
    /// Mutation: remove "MLP" from the codec match → "MLP" passes through.
    #[test]
    fn codec_mlp_maps_to_truehd() {
        assert_eq!(codec("MLP"), "TrueHD");
        assert_eq!(codec("mlp"), "TrueHD");
        assert_eq!(codec("Mlp"), "TrueHD");
    }

    /// Spec: `AC` (without the `3` suffix) is also a recognized alias for
    /// Dolby Digital in Pixelogic tokens.
    /// Mutation: remove `"AC"` from the match arm → "AC" passes through.
    #[test]
    fn codec_ac_without_3_maps_to_dolby_digital() {
        assert_eq!(codec("AC"), "Dolby Digital");
        assert_eq!(codec("ac"), "Dolby Digital");
    }

    /// Spec: `DDL` is Dolby's internal token for Dolby Digital Plus (EAC-3).
    /// Mutation: remove `"DDL"` arm → "DDL" passes through.
    #[test]
    fn codec_ddl_maps_to_dolby_digital_plus() {
        assert_eq!(codec("DDL"), "Dolby Digital Plus");
        assert_eq!(codec("ddl"), "Dolby Digital Plus");
    }

    /// Spec: `WAV` (PCM WAV) maps to "PCM" display string.
    /// Mutation: remove `"WAV"` arm → "WAV" passes through.
    #[test]
    fn codec_wav_maps_to_pcm() {
        assert_eq!(codec("WAV"), "PCM");
        assert_eq!(codec("wav"), "PCM");
    }

    /// Spec: `ATMOS` maps to "Dolby Atmos" (the brand string).
    /// Mutation: remove `"ATMOS"` arm → "ATMOS" passes through unchanged.
    #[test]
    fn codec_atmos_maps_to_dolby_atmos() {
        assert_eq!(codec("ATMOS"), "Dolby Atmos");
        assert_eq!(codec("Atmos"), "Dolby Atmos");
        assert_eq!(codec("atmos"), "Dolby Atmos");
    }

    /// Spec: `DTS` is recognized but passes through unchanged (no alias needed).
    /// Unknown codes return IN THEIR ORIGINAL CASING (the match branch is `_ => code`).
    /// Mutation: add `"DTS" => "DTS-HD"` → DTS incorrectly upgraded.
    #[test]
    fn codec_dts_passes_through_unchanged() {
        assert_eq!(codec("DTS"), "DTS");
        // Lowercase input returns lowercase — unknown codes pass through raw.
        assert_eq!(codec("dts"), "dts");
    }

    /// Spec: COMPOUND_LANGS must be ordered longest-first so that
    /// "Brazilian Portuguese" is matched before bare "Portuguese".
    /// Mutation: put "portuguese" before "brazilian portuguese" in the table →
    /// Brazilian Portuguese returns variant="", losing the regional info.
    #[test]
    fn compound_lang_longest_match_wins() {
        let r = lang("Brazilian Portuguese 5.1 Dolby").unwrap();
        assert_eq!(r.code, "por");
        assert_eq!(r.variant, "Brazilian");

        let r = lang("Castilian Spanish").unwrap();
        assert_eq!(r.code, "spa");
        assert_eq!(r.variant, "Castilian");

        let r = lang("Latin American Spanish").unwrap();
        assert_eq!(r.code, "spa");
        assert_eq!(r.variant, "Latin American");
    }

    /// Spec: bare language name lookup uses word-boundary matching.
    /// Mutation: use `.contains()` instead of `has_word()` → "engineering" matches "english".
    #[test]
    fn lang_no_false_positive_substring() {
        assert_eq!(lang("Audio Engineering"), None);
        assert_eq!(lang("Francispeople"), None);
    }

    /// Spec: all 36 bare-lang entries must resolve correctly.
    /// Mutation: swap two entries in BARE_LANGS → wrong code returned.
    #[test]
    fn lang_bare_all_entries_spot_check() {
        let cases = [
            ("English", "eng"),
            ("Spanish", "spa"),
            ("French", "fra"),
            ("German", "deu"),
            ("Italian", "ita"),
            ("Japanese", "jpn"),
            ("Chinese", "zho"),
            ("Korean", "kor"),
            ("Portuguese", "por"),
            ("Polish", "pol"),
            ("Czech", "ces"),
            ("Hungarian", "hun"),
            ("Dutch", "nld"),
            ("Arabic", "ara"),
            ("Russian", "rus"),
            ("Swedish", "swe"),
            ("Finnish", "fin"),
        ];
        for (name, code) in cases {
            let r = lang(name).unwrap_or_else(|| panic!("lang({:?}) must be Some", name));
            assert_eq!(r.code, code, "wrong code for {}", name);
            assert_eq!(r.variant, "", "bare lang {} must have empty variant", name);
        }
    }

    /// Spec: `purpose()` recognizes "commentary" (word-boundary).
    /// Mutation: use `contains("comment")` → "commenter" wrongly matches.
    #[test]
    fn purpose_commentary_word_boundary() {
        assert_eq!(purpose("English Commentary"), LabelPurpose::Commentary);
        assert_eq!(purpose("Commenter Track"), LabelPurpose::Normal);
        assert_eq!(purpose("recommentary"), LabelPurpose::Normal);
    }

    /// Spec: "Director's Commentary" is a recognized phrase.
    /// Mutation: require exact "commentary" without apostrophe prefix → fails.
    #[test]
    fn purpose_directors_commentary_recognized() {
        assert_eq!(purpose("Director's Commentary"), LabelPurpose::Commentary);
    }

    /// Spec: `purpose()` recognizes "audio description" compound phrase.
    /// Mutation: remove the compound `audio description` check → Descriptive broken.
    #[test]
    fn purpose_audio_description_compound() {
        assert_eq!(purpose("Audio Description"), LabelPurpose::Descriptive);
        assert_eq!(
            purpose("English Audio Description"),
            LabelPurpose::Descriptive
        );
    }

    /// Spec: "descriptive service" maps to Descriptive via compound check.
    /// Mutation: remove "descriptive service" compound → Normal returned.
    #[test]
    fn purpose_descriptive_service_compound() {
        assert_eq!(
            purpose("English Descriptive Service"),
            LabelPurpose::Descriptive
        );
    }

    /// Spec: "music only" maps to Score via compound check.
    /// Mutation: remove "music only" compound → Normal returned.
    #[test]
    fn purpose_music_only_maps_to_score() {
        assert_eq!(purpose("Music Only"), LabelPurpose::Score);
        assert_eq!(purpose("English Music Only Track"), LabelPurpose::Score);
    }

    /// Spec: "score" (bare word) maps to Score.
    /// Mutation: remove `has_word(&lower, "score")` check → Normal returned.
    #[test]
    fn purpose_score_bare_word() {
        assert_eq!(purpose("Isolated Score"), LabelPurpose::Score);
        assert_eq!(purpose("Score Track"), LabelPurpose::Score);
    }

    /// Spec: "ime" maps to Ime (alternate music track).
    /// Mutation: remove `has_word(&lower, "ime")` check → Normal returned.
    #[test]
    fn purpose_ime_recognized() {
        assert_eq!(purpose("IME"), LabelPurpose::Ime);
        assert_eq!(purpose("English ime track"), LabelPurpose::Ime);
    }

    /// Spec: "ime" inside "time" or "anime" must NOT match.
    /// Mutation: use `contains("ime")` → "anime", "time" falsely match.
    #[test]
    fn purpose_ime_no_substring_match() {
        assert_eq!(purpose("Showtime Audio"), LabelPurpose::Normal);
        assert_eq!(purpose("Anime Commentary"), LabelPurpose::Commentary);
    }

    /// Spec: `qualifier()` prioritizes SDH over Forced when both present.
    /// Mutation: reverse the SDH check order → Forced returned when both present.
    #[test]
    fn qualifier_sdh_priority_over_forced() {
        assert_eq!(qualifier("English Forced SDH"), LabelQualifier::Sdh);
        assert_eq!(qualifier("SDH Forced"), LabelQualifier::Sdh);
    }

    /// Spec: "captions" maps to Sdh (closed-caption subtitles for deaf).
    /// Mutation: remove `has_word(&lower, "captions")` → "captions" returns None.
    #[test]
    fn qualifier_captions_maps_to_sdh() {
        assert_eq!(qualifier("English Captions"), LabelQualifier::Sdh);
        assert_eq!(qualifier("Closed Captions"), LabelQualifier::Sdh);
    }

    /// Spec: "forced narrative" → Forced qualifier.
    /// Mutation: remove "forced" check → None returned.
    #[test]
    fn qualifier_forced_narrative() {
        assert_eq!(qualifier("Forced Narrative"), LabelQualifier::Forced);
        assert_eq!(
            qualifier("English Forced Subtitles"),
            LabelQualifier::Forced
        );
    }

    /// Spec: "rnib" → DescriptiveService qualifier.
    /// Mutation: remove `has_word(&lower, "rnib")` → None returned.
    #[test]
    fn qualifier_rnib_maps_to_descriptive_service() {
        assert_eq!(
            qualifier("English RNIB"),
            LabelQualifier::DescriptiveService
        );
    }

    /// Spec: "descriptive service" compound → DescriptiveService.
    /// Mutation: remove compound check → None returned.
    #[test]
    fn qualifier_descriptive_service_compound() {
        assert_eq!(
            qualifier("English Descriptive Service"),
            LabelQualifier::DescriptiveService
        );
    }

    /// Word boundary: "sdh" inside "lambdash" must not match.
    /// Mutation: use `contains("sdh")` → "lambdash" falsely triggers SDH.
    #[test]
    fn qualifier_no_substring_sdh() {
        assert_eq!(qualifier("lambdash"), LabelQualifier::None);
        assert_eq!(qualifier("Swedish"), LabelQualifier::None); // "swe" not "sdh"
    }

    /// ISO 639-2 codes as input (e.g. "eng") must NOT match via `lang()` because
    /// the function maps English *names*, not ISO codes.
    /// Mutation: add an ISO-code lookup table → "eng" returned for iso input.
    #[test]
    fn lang_iso_code_input_returns_none() {
        assert_eq!(lang("eng"), None);
        assert_eq!(lang("fra"), None);
        assert_eq!(lang("jpn"), None);
        assert_eq!(lang("zho"), None);
    }

    /// Compound lang "Australian English" → (eng, Australian).
    /// Mutation: put "australian english" after "english" → bare "English" wins.
    #[test]
    fn compound_lang_australian_english() {
        let r = lang("Australian English").unwrap();
        assert_eq!(r.code, "eng");
        assert_eq!(r.variant, "Australian");
    }

    /// Compound lang corpus typo "Austrailian English" (missing 'l') must still match.
    /// Mutation: remove the typo entry → no variant info.
    #[test]
    fn compound_lang_austrailian_typo_matched() {
        let r = lang("Austrailian English").unwrap();
        assert_eq!(r.code, "eng");
        assert_eq!(r.variant, "Australian");
    }

    /// Euro Portuguese vs European Portuguese: both map to (por, European).
    /// Mutation: remove "euro portuguese" → "Euro Portuguese" returns (por, "").
    #[test]
    fn compound_lang_euro_portuguese() {
        let r = lang("Euro Portuguese").unwrap();
        assert_eq!(r.code, "por");
        assert_eq!(r.variant, "European");

        let r = lang("European Portuguese").unwrap();
        assert_eq!(r.code, "por");
        assert_eq!(r.variant, "European");
    }

    /// `has_word` empty needle returns false (guard against infinite loop).
    /// Mutation: remove empty-needle early return → always returns true for empty needle.
    #[test]
    fn has_word_empty_needle_is_false() {
        assert!(!has_word("anything", ""));
        assert!(!has_word("", ""));
    }

    /// `codec()` with empty string passes through as empty (no panic).
    /// Mutation: remove guard → match panics on empty.
    #[test]
    fn codec_empty_passes_through() {
        assert_eq!(codec(""), "");
    }
}