use std::path::Path;
use crate::diagnostics::{Location, ValidationIssue};
use crate::mxf::codes::St2067_2_2016;
pub fn check_timed_text(regxml: &str, path: &Path) -> Vec<ValidationIssue> {
let mut issues = Vec::new();
let has_timed_text =
regxml.contains(":TimedTextDescriptor") || regxml.contains(":IMFTimedTextDescriptor");
if !has_timed_text {
return issues;
}
if let Some(cf) = extract_field(regxml, "ContainerFormat") {
if let Some(bytes) = crate::mxf::audio_mca::parse_ul_bytes(&cf) {
if bytes[14] != 0x13 {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::TimedTextMappingKindNot0x13,
format!(
"MXF {} timed-text ContainerFormat UL byte 15 = 0x{:02x} \
— ST 429-5 §7 requires Mapping Kind = 0x13 for IMSC. \
ContainerFormat = {}",
path.display(),
bytes[14],
cf.trim(),
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
}
if let Some(enc) = extract_field(regxml, "UCSEncoding") {
let enc = enc.trim();
if !enc.eq_ignore_ascii_case("UTF-8") && !enc.eq_ignore_ascii_case("UTF8") {
issues.push(
ValidationIssue::from_code(St2067_2_2016::TimedTextUCSEncodingNotUTF8,
format!(
"MXF {} TimedTextDescriptor UCSEncoding = '{}' — ST 2067-2 §5.4 requires UTF-8.",
path.display(),
enc,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
if let Some(ns) = extract_field(regxml, "NamespaceURI") {
let ns = ns.trim();
const ACCEPTABLE: &[&str] = &[
"http://www.w3.org/ns/ttml/profile/imsc1/text",
"http://www.w3.org/ns/ttml/profile/imsc1/image",
"http://www.w3.org/ns/ttml/profile/imsc1.1/text",
"http://www.w3.org/ns/ttml/profile/imsc1.1/image",
];
if !ACCEPTABLE.contains(&ns) {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::TimedTextNamespaceNotIMSC,
format!(
"MXF {} TimedTextDescriptor NamespaceURI = '{}' — ST 2067-2 §5.4 \
requires one of the IMSC1 profile namespaces (text or image, 1.0 or 1.1).",
path.display(),
ns,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
for mime in extract_all_fields(regxml, "MIMEType") {
let mime = mime.trim();
const ACCEPTABLE: &[&str] = &["image/png", "application/x-font-opentype"];
if !ACCEPTABLE.contains(&mime) {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::TimedTextResourceMIMETypeUnsupported,
format!(
"MXF {} TimeTextResourceSubDescriptor MIMEType = '{}' — ST 2067-2 \
§5.4.5/6 requires image/png or application/x-font-opentype.",
path.display(),
mime,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
issues
}
use crate::mxf::audio_mca::{extract_all_fields, extract_field};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skips_non_timed_text_mxf() {
let xml = r#"<ns1:Preface><ns1:WAVEPCMDescriptor/></ns1:Preface>"#;
let issues = check_timed_text(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues.is_empty(),
"timed text pipeline must be silent on non-timed-text MXF, got: {:#?}",
issues
);
}
#[test]
fn flags_timed_text_mapping_kind_not_0x13() {
let xml = r#"<ns1:TimedTextDescriptor>
<ns2:ContainerFormat>urn:smpte:ul:060e2b34.04010101.0d010301.02061200</ns2:ContainerFormat>
<ns2:UCSEncoding>UTF-8</ns2:UCSEncoding>
<ns2:NamespaceURI>http://www.w3.org/ns/ttml/profile/imsc1/text</ns2:NamespaceURI>
</ns1:TimedTextDescriptor>"#;
let issues = check_timed_text(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("TimedTextMappingKindNot0x13")),
"expected TimedTextMappingKindNot0x13, got: {:#?}",
issues
);
}
#[test]
fn flags_non_utf8_encoding() {
let xml = r#"<ns1:TimedTextDescriptor>
<ns2:UCSEncoding>ISO-8859-1</ns2:UCSEncoding>
<ns2:NamespaceURI>http://www.w3.org/ns/ttml/profile/imsc1/text</ns2:NamespaceURI>
</ns1:TimedTextDescriptor>"#;
let issues = check_timed_text(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("TimedTextUCSEncodingNotUTF8")),
"expected TimedTextUCSEncodingNotUTF8, got: {:#?}",
issues
);
}
#[test]
fn flags_non_imsc_namespace() {
let xml = r#"<ns1:TimedTextDescriptor>
<ns2:UCSEncoding>UTF-8</ns2:UCSEncoding>
<ns2:NamespaceURI>http://example.org/not-imsc</ns2:NamespaceURI>
</ns1:TimedTextDescriptor>"#;
let issues = check_timed_text(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("TimedTextNamespaceNotIMSC")),
"expected TimedTextNamespaceNotIMSC, got: {:#?}",
issues
);
}
#[test]
fn flags_unsupported_resource_mime_type() {
let xml = r#"<ns1:TimedTextDescriptor>
<ns2:UCSEncoding>UTF-8</ns2:UCSEncoding>
<ns2:NamespaceURI>http://www.w3.org/ns/ttml/profile/imsc1/text</ns2:NamespaceURI>
<ns1:TimeTextResourceSubDescriptor>
<ns2:MIMEType>application/json</ns2:MIMEType>
</ns1:TimeTextResourceSubDescriptor>
</ns1:TimedTextDescriptor>"#;
let issues = check_timed_text(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("TimedTextResourceMIMETypeUnsupported")),
"expected TimedTextResourceMIMETypeUnsupported, got: {:#?}",
issues
);
}
#[test]
fn accepts_clean_imsc_timed_text_descriptor() {
let xml = r#"<ns1:TimedTextDescriptor>
<ns2:UCSEncoding>UTF-8</ns2:UCSEncoding>
<ns2:NamespaceURI>http://www.w3.org/ns/ttml/profile/imsc1.1/text</ns2:NamespaceURI>
<ns1:TimeTextResourceSubDescriptor>
<ns2:MIMEType>image/png</ns2:MIMEType>
</ns1:TimeTextResourceSubDescriptor>
<ns1:TimeTextResourceSubDescriptor>
<ns2:MIMEType>application/x-font-opentype</ns2:MIMEType>
</ns1:TimeTextResourceSubDescriptor>
</ns1:TimedTextDescriptor>"#;
let issues = check_timed_text(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues.is_empty(),
"clean IMSC1.1 + acceptable MIME types should produce zero diagnostics, got: {:#?}",
issues
);
}
}