labels: reuse b06 pixelogic cap, real ctrm order test, setNumber fallback

This commit is contained in:
Matthew Jackson
2026-09-29 19:27:31 -07:00
parent 9144baf0f0
commit 02fb788b9f
4 changed files with 22 additions and 36 deletions
+9 -5
View File
@@ -207,11 +207,8 @@ fn extract_title(xml_text: &str) -> Option<String> {
/// the denominator (both forms appear in the wild).
fn extract_disc_set(xml_text: &str) -> Option<(u32, u32)> {
// Real discs carry <di:setNumber>; <di:discNumber> is kept as a fallback.
let n = xml::text(xml_text, "setNumber")
.or_else(|| xml::text(xml_text, "discNumber"))?
.trim()
.parse::<u32>()
.ok()?;
let num = |tag| xml::text(xml_text, tag).and_then(|t| t.trim().parse::<u32>().ok());
let n = num("setNumber").or_else(|| num("discNumber"))?;
let total = xml::text(xml_text, "numSets")
.or_else(|| xml::text(xml_text, "numberOfSets"))?
.trim()
@@ -243,6 +240,13 @@ mod tests {
assert_eq!(parse_bdmt_xml(xml).expect("parse").2, Some((2, 3)));
}
#[test]
fn non_numeric_set_number_falls_back_to_disc_number() {
let xml = "<d><di:name>X</di:name><di:numSets>3</di:numSets>\
<di:setNumber>two</di:setNumber><di:discNumber>2</di:discNumber></d>";
assert_eq!(parse_bdmt_xml(xml).expect("parse").2, Some((2, 3)));
}
#[test]
fn long_title_and_description_are_capped() {
let big = "é".repeat(5000);
+10 -5
View File
@@ -242,11 +242,16 @@ mod tests {
#[test]
fn menu_base_tie_order_is_deterministic_by_prefix() {
let labels = parse_props(
"b_1.class=AudioButton\nb_1.streamNumber=2\nb_1.name=Second\n\
a_1.class=AudioButton\na_1.streamNumber=2\na_1.name=First\n",
);
assert_eq!(labels[0].name, "First");
// Inserted in reverse; many prefixes so hash order can't match by luck.
let mut text = String::new();
for i in (0..64).rev() {
text.push_str(&format!(
"p{i:02}.class=AudioButton\np{i:02}.streamNumber=2\np{i:02}.name=N{i:02}\n"
));
}
let names: Vec<String> = parse_props(&text).into_iter().map(|l| l.name).collect();
let want: Vec<String> = (0..64).map(|i| format!("N{i:02}")).collect();
assert_eq!(names, want);
}
#[test]
+2 -5
View File
@@ -78,9 +78,6 @@ impl UnknownParts {
}
}
// Retained-run cap for the label blob (far above any real disc's token count).
const MAX_STRINGS: usize = 65_536;
pub fn detect(_reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
super::jar_file_exists(udf, "bluray_project.bin")
}
@@ -88,8 +85,8 @@ pub fn detect(_reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult> {
let data = super::read_jar_file(reader, udf, "bluray_project.bin")?;
// The token grammar is `{lang3}_{codec?}_{purpose?}_{region?}_` so the
// shortest meaningful run is 4 chars (lang + underscore). Capped: untrusted blob.
let strings = text::extract_ascii_strings_capped(&data, 4, MAX_STRINGS);
// shortest meaningful run is 4 chars (lang + underscore). The extractor caps run count (untrusted blob).
let strings = text::extract_ascii_strings(&data, 4);
// Collects uncatalogued token components; if any, confidence downgrades to Medium
// and they're reported once (see `UnknownParts`). Sequential parse, so a plain
+1 -21
View File
@@ -13,24 +13,10 @@
/// Non-printable bytes (including `\t`, `\n`, NUL) terminate the
/// current run. Output strings are guaranteed valid UTF-8 (they're
/// pure 7-bit ASCII). Strings shorter than `min_len` are dropped.
#[cfg(test)]
pub fn extract_ascii_strings(data: &[u8], min_len: usize) -> Vec<String> {
extract_ascii_strings_capped(data, min_len, usize::MAX)
}
/// [`extract_ascii_strings`], stopping after `max_strings` runs so untrusted
/// input can't amplify into millions of owned `String`s.
pub fn extract_ascii_strings_capped(
data: &[u8],
min_len: usize,
max_strings: usize,
) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
for &b in data {
if out.len() >= max_strings {
return out;
}
if (0x20..=0x7E).contains(&b) {
current.push(b as char);
} else if !current.is_empty() && current.len() >= min_len {
@@ -39,7 +25,7 @@ pub fn extract_ascii_strings_capped(
current.clear();
}
}
if !current.is_empty() && current.len() >= min_len && out.len() < max_strings {
if !current.is_empty() && current.len() >= min_len {
out.push(current);
}
out
@@ -49,12 +35,6 @@ pub fn extract_ascii_strings_capped(
mod tests {
use super::*;
#[test]
fn capped_extraction_stops_at_the_cap() {
let got = extract_ascii_strings_capped(b"abcd\0abcd\0abcd\0abcd", 4, 2);
assert_eq!(got.len(), 2);
}
#[test]
fn extracts_simple_runs() {
let got = extract_ascii_strings(b"hello\0world\0", 3);