labels: one control-character filter for disc-authored titles

bdmt title and description text kept raw control characters from CDATA and plain bodies,
so a disc could put a terminal escape sequence into a shown title or a file name. It now
goes through the same filter as the Blu-ray di:name title.
This commit is contained in:
Matthew Jackson
2026-10-04 12:25:45 -07:00
parent 3b0c1f2c6c
commit dc1a0cec5a
3 changed files with 25 additions and 8 deletions
+3 -5
View File
@@ -511,11 +511,9 @@ impl Disc {
if let Some(end) = xml[s..].find("</di:name>") {
// Disc-authored text reaches terminals and file names:
// control characters (decoded from `&#27;` too) are dropped.
let title: String = xml_text_decode(xml[s..s + end].trim())
.chars()
.filter(|c| !c.is_control())
.collect();
let title = title.trim().to_string();
let title = crate::labels::display_text(&xml_text_decode(
xml[s..s + end].trim(),
));
if !title.is_empty() && !crate::labels::is_placeholder_title(&title)
{
return Some(title);
+21 -2
View File
@@ -182,10 +182,10 @@ fn field_text(raw: &str) -> Option<String> {
.strip_prefix("<![CDATA[")
.and_then(|r| r.strip_suffix("]]>"))
{
Some(inner) if !inner.contains("]]>") => inner.trim().to_string(),
Some(inner) if !inner.contains("]]>") => display_text(inner),
Some(_) => return None,
None if looks_like_xml(raw) => return None,
None => decode_entities(raw),
None => display_text(&decode_entities(raw)),
};
if out.is_empty() || looks_like_xml(&out) {
return None;
@@ -233,6 +233,13 @@ fn decode_entities(s: &str) -> String {
out
}
/// Disc-authored text as shown and put in file names: control characters (an escape
/// sequence, a raw newline) dropped, then trimmed.
pub(crate) fn display_text(s: &str) -> String {
let kept: String = s.chars().filter(|c| !c.is_control()).collect();
kept.trim().to_string()
}
/// True for the generic `Blu-ray` some discs (Warner's non-English bdmt files) put where
/// the title belongs: not a title, so callers skip to the next candidate.
pub(crate) fn is_placeholder_title(title: &str) -> bool {
@@ -322,6 +329,18 @@ mod tests {
assert!(d.expect("desc").len() <= MAX_BDMT_TEXT);
}
// A title or description, CDATA or plain, keeps no control character (a terminal escape
// sequence, a raw newline), as the Blu-ray di:name path does not.
#[test]
fn disc_text_drops_control_characters() {
let xml = "<d><di:name><![CDATA[Movie\x1b]0;pwned\x07]]></di:name>\
<di:description>Line\x1b[2Jone\ntwo</di:description></d>";
let (t, d, _) = parse_bdmt_xml(xml).expect("parse");
assert_eq!(t, "Movie]0;pwned");
assert_eq!(d.as_deref(), Some("Line[2Jonetwo"));
assert_eq!(display_text(" \x1bA\tB "), "AB");
}
#[test]
fn cap_text_cuts_on_a_char_boundary() {
let s = format!("aa{}", "\u{4e16}".repeat(400));
+1 -1
View File
@@ -34,7 +34,7 @@ use crate::udf::UdfFs;
// disc-level metadata via `labels::DiscMetadata`. The module itself
// stays private — analyze() drives the parse path.
pub use bdmt::DiscMetadata;
pub(crate) use bdmt::is_placeholder_title;
pub(crate) use bdmt::{display_text, is_placeholder_title};
// Re-exported via crate::disc — the public API surfaces these next to
// AudioStream/SubtitleStream so callers can map purpose/qualifier to display