The Windows UI needs current winsafe, whose real minimum is 1.89 (its manifest under-declares 1.87 while it uses NonNull::from_ref). Rather than stop at the minimum, this goes to current stable and fixes what that costs. The counter-intuitive result: 1.97 is CHEAPER than 1.89. libfreemkv had 54 clippy errors at 1.89 and 6 at 1.97, because clippy tightened the noisy collapsible_if lint in between. Stopping at the minimum would have been the most expensive choice available. Roughly 47 lints across the eight repos, the large majority auto-fixed: libfreemkv 6, freemkv-engine 14, bdemu 8, freemkv-keysources 7, autorip 6, freemkv-unlock 3, freemkv-i18n 3. The hand-fixed ones are a descending sort to sort_by_key(Reverse), four manual checked-division sites, a loop counter replaced by enumerate, and a loop whose first let-else became a while-let. Worth recording for whoever bumps next: clippy is MSRV-AWARE. Those 54 lints only appear once the crate DECLARES 1.89 or later, because let-chains become available. A bare `cargo +1.89 clippy` against a manifest still pinned at 1.87 reports clean and is meaningless — gate with the real precommit script, which is also the only thing that covers build scripts. The pin still sits below the Mac default, so it keeps doing its job: catching lint drift locally before CI sees it.
150 lines
5.4 KiB
Rust
150 lines
5.4 KiB
Rust
//! Menu-graphic filename language hints.
|
|
//!
|
|
//! Some BD-J discs encode per-language menu artwork with the language in the
|
|
//! filename, e.g. `Dune_UHD01_Eng_Composite1.png`,
|
|
//! `VForVendetta_UHD01_FRE_Composite2.png`. The `_UHD01_{LANG}_Composite`
|
|
//! marker is authored deliberately, so the set of `{LANG}` tokens is the set
|
|
//! of menu languages the disc ships.
|
|
//!
|
|
//! This is a language-only hint (no per-stream purpose/codec), so it runs at
|
|
//! [`Confidence::Low`] — it never displaces a real framework parser, and it
|
|
//! sits at the same tier as the MPLS floor. It is here so the pattern is a
|
|
//! first-class, testable parser that keeps picking up discs as the corpus
|
|
//! grows, rather than lost logic. Detection is precise: it fires only on the
|
|
//! `_UHD01_{LANG}_Composite` grammar with a `{LANG}` the vocab recognizes.
|
|
|
|
use super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelType, vocab};
|
|
use crate::sector::SectorSource;
|
|
use crate::udf::UdfFs;
|
|
|
|
pub fn detect(_reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
|
|
super::jar_inventory(udf)
|
|
.iter()
|
|
.any(|f| filename_lang(f).is_some())
|
|
}
|
|
|
|
pub fn parse(_reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult> {
|
|
let names = super::jar_inventory(udf);
|
|
let labels = labels_from_filenames(&names);
|
|
if labels.is_empty() {
|
|
return None;
|
|
}
|
|
// Low: language-only, derived from menu-asset filenames. A real framework
|
|
// parser (and even the MPLS floor's per-stream data) is preferred; this is
|
|
// a hint of which languages the disc menus offer.
|
|
Some(ParseResult::low(labels))
|
|
}
|
|
|
|
/// One audio [`StreamLabel`] per distinct menu language found, in first-seen
|
|
/// order, numbered 1-based. Split out from `parse` so it is unit-testable
|
|
/// without a `UdfFs`.
|
|
fn labels_from_filenames(names: &[String]) -> Vec<StreamLabel> {
|
|
let mut seen: Vec<&'static str> = Vec::new();
|
|
for name in names {
|
|
if let Some(code) = filename_lang(name)
|
|
&& !seen.contains(&code)
|
|
{
|
|
seen.push(code);
|
|
}
|
|
}
|
|
seen.into_iter()
|
|
.enumerate()
|
|
.map(|(i, code)| StreamLabel {
|
|
stream_number: (i as u16).saturating_add(1),
|
|
stream_type: StreamLabelType::Audio,
|
|
language: code.to_string(),
|
|
name: String::new(),
|
|
purpose: LabelPurpose::Normal,
|
|
qualifier: LabelQualifier::None,
|
|
codec_hint: String::new(),
|
|
variant: String::new(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Extract the ISO-639-2 language code from a `{title}_UHD01_{LANG}_Composite`
|
|
/// menu-graphic filename, or `None` if the name does not match the grammar or
|
|
/// carries a `{LANG}` the vocab does not recognize.
|
|
///
|
|
/// The `_UHD01_` marker plus the `_Composite` suffix keep this from firing on
|
|
/// unrelated PNGs (`KeyComposite4.png`, `LoadingComposite1.png` have no
|
|
/// `_UHD01_{LANG}_` segment).
|
|
fn filename_lang(name: &str) -> Option<&'static str> {
|
|
// Case-fold once; the marker/suffix are matched case-insensitively.
|
|
let lower = name.to_ascii_lowercase();
|
|
let marker = "_uhd01_";
|
|
let m = lower.find(marker)?;
|
|
let after = m + marker.len();
|
|
// The language token runs from `after` up to the next `_`.
|
|
let rest = &lower[after..];
|
|
let end = rest.find('_')?;
|
|
if !rest[end..].starts_with("_composite") {
|
|
return None;
|
|
}
|
|
let token = &name[after..after + end];
|
|
vocab::menu_lang(token)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn extracts_confirmed_samples() {
|
|
assert_eq!(filename_lang("Dune_UHD01_Eng_Composite1.png"), Some("eng"));
|
|
assert_eq!(filename_lang("Dune_UHD01_Ger_Composite2.png"), Some("deu"));
|
|
assert_eq!(
|
|
filename_lang("VForVendetta_UHD01_FRE_Composite2.png"),
|
|
Some("fra")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_non_language_composites() {
|
|
assert_eq!(filename_lang("KeyComposite4.png"), None);
|
|
assert_eq!(filename_lang("LoadingComposite1.png"), None);
|
|
assert_eq!(
|
|
filename_lang("FourKWarningsComposite1_bt2020_HDR.png"),
|
|
None
|
|
);
|
|
assert_eq!(filename_lang("Fast9_UPK75_Composite1.png"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_language_token_is_none() {
|
|
// A UHD01 marker but a token the vocab does not recognize must not
|
|
// produce a bogus language.
|
|
assert_eq!(filename_lang("Movie_UHD01_Zzz_Composite1.png"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn dedups_and_numbers_distinct_languages() {
|
|
let names = vec![
|
|
"Dune_UHD01_Eng_Composite1.png".to_string(),
|
|
"Dune_UHD01_Eng_Composite2.png".to_string(),
|
|
"Dune_UHD01_Ger_Composite1.png".to_string(),
|
|
"LoadingComposite1.png".to_string(),
|
|
];
|
|
let labels = labels_from_filenames(&names);
|
|
assert_eq!(labels.len(), 2);
|
|
assert_eq!(labels[0].language, "eng");
|
|
assert_eq!(labels[0].stream_number, 1);
|
|
assert_eq!(labels[1].language, "deu");
|
|
assert_eq!(labels[1].stream_number, 2);
|
|
assert!(
|
|
labels
|
|
.iter()
|
|
.all(|l| l.stream_type == StreamLabelType::Audio)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_matching_names_yields_empty() {
|
|
let names = vec![
|
|
"KeyComposite4.png".to_string(),
|
|
"disc.properties".to_string(),
|
|
];
|
|
assert!(labels_from_filenames(&names).is_empty());
|
|
}
|
|
}
|