pub mod types;
pub use types::{
CodingEquations, ColorPrimaries, ContentKind, CplNamespace, EditRate, LanguageTag,
MarkerLabel, McaTagSymbol, Resolution, TransferCharacteristic, VideoCodec,
};
pub mod validate;
pub use validate::validate_cpl as validate_cpl_constraints;
pub mod codes;
use base64::Engine;
use crate::assetmap::{HashAlgorithm, ImfUuid};
use quick_xml::events::Event;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use thiserror::Error;
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
use std::io::Write;
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
use std::process::Command;
#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
use libxml::parser::Parser as XmlParser;
#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
use xmlsec::{XmlSecKey, XmlSecKeyFormat, XmlSecSignatureContext};
#[cfg(feature = "typescript")]
use ts_rs::TS;
#[cfg(feature = "wasm")]
use tsify::Tsify;
#[derive(Debug, Error)]
pub enum CplParseError {
#[error("XML parse error: {0}")]
Xml(#[from] quick_xml::DeError),
#[error("strict unknown XML token(s): {0}")]
StrictUnknownXml(String),
#[error("strict schema violation: {0}")]
StrictSchema(String),
#[error("XMLDSIG verifier is required for selected signature mode")]
SignatureVerifierRequired,
#[error("XMLDSIG verification failed: {0}")]
SignatureVerificationFailed(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnknownFieldMode {
Ignore,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaStrictMode {
Off,
Basic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureValidationMode {
Ignore,
RequirePresence,
VerifyIfPresent,
RequireValid,
}
pub trait XmlSignatureVerifier {
fn verify(&self, xml_content: &str) -> Result<(), String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ReferenceDigestXmlDsigVerifier;
impl XmlSignatureVerifier for ReferenceDigestXmlDsigVerifier {
fn verify(&self, xml_content: &str) -> Result<(), String> {
validate_signature_structure(xml_content)?;
validate_reference_digests(xml_content)
}
}
#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
#[derive(Debug, Clone)]
pub struct XmlSecCrateVerifier {
key_data: Vec<u8>,
key_format: XmlSecKeyFormat,
key_password: Option<String>,
}
#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
impl XmlSecCrateVerifier {
pub fn from_key_data(key_data: Vec<u8>, key_format: XmlSecKeyFormat) -> Self {
Self {
key_data,
key_format,
key_password: None,
}
}
pub fn from_pem(key_data: impl AsRef<[u8]>) -> Self {
Self::from_key_data(key_data.as_ref().to_vec(), XmlSecKeyFormat::Pem)
}
pub fn with_password(mut self, password: impl Into<String>) -> Self {
self.key_password = Some(password.into());
self
}
}
#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
impl XmlSignatureVerifier for XmlSecCrateVerifier {
fn verify(&self, xml_content: &str) -> Result<(), String> {
validate_signature_structure(xml_content)?;
let mut tmp = tempfile::NamedTempFile::new()
.map_err(|e| format!("failed to create temp xml file: {}", e))?;
tmp.write_all(xml_content.as_bytes())
.map_err(|e| format!("failed to write temp xml file: {}", e))?;
tmp.flush()
.map_err(|e| format!("failed to flush temp xml file: {}", e))?;
let doc = XmlParser::default()
.parse_file(tmp.path().to_string_lossy().as_ref())
.map_err(|e| format!("xml parse failed for xmlsec verifier: {}", e))?;
let key = XmlSecKey::from_memory(
&self.key_data,
self.key_format,
self.key_password.as_deref(),
)
.map_err(|e| format!("xmlsec key load failed: {}", e))?;
let mut ctx = XmlSecSignatureContext::new();
ctx.insert_key(key);
let valid = ctx
.verify_document(&doc)
.map_err(|e| format!("xmlsec verify failed: {}", e))?;
if valid {
Ok(())
} else {
Err("xmlsec signature verification returned invalid".to_string())
}
}
}
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
#[derive(Debug, Clone)]
pub struct XmlSec1Verifier {
binary_path: String,
extra_args: Vec<String>,
}
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
impl Default for XmlSec1Verifier {
fn default() -> Self {
let binary_path = std::env::var("IMF_XMLSEC1_BIN").unwrap_or_else(|_| "xmlsec1".to_string());
Self {
binary_path,
extra_args: Vec::new(),
}
}
}
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
impl XmlSec1Verifier {
pub fn new() -> Self {
Self::default()
}
pub fn with_binary_path(mut self, binary_path: impl Into<String>) -> Self {
self.binary_path = binary_path.into();
self
}
pub fn with_extra_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.extra_args = args.into_iter().map(Into::into).collect();
self
}
}
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
impl XmlSignatureVerifier for XmlSec1Verifier {
fn verify(&self, xml_content: &str) -> Result<(), String> {
let mut tmp = tempfile::NamedTempFile::new()
.map_err(|e| format!("failed to create temp xml file: {}", e))?;
tmp.write_all(xml_content.as_bytes())
.map_err(|e| format!("failed to write temp xml file: {}", e))?;
tmp.flush()
.map_err(|e| format!("failed to flush temp xml file: {}", e))?;
let mut command = Command::new(&self.binary_path);
command.arg("--verify");
for arg in &self.extra_args {
command.arg(arg);
}
command.arg(tmp.path());
let output = command.output().map_err(|e| {
format!(
"failed to execute '{} --verify': {}",
self.binary_path, e
)
})?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let message = if !stderr.is_empty() {
stderr
} else if !stdout.is_empty() {
stdout
} else {
format!("xmlsec1 exited with status {}", output.status)
};
Err(message)
}
}
pub struct CplParseOptions<'a> {
pub unknown_field_mode: UnknownFieldMode,
pub schema_strict_mode: SchemaStrictMode,
pub signature_validation_mode: SignatureValidationMode,
pub signature_verifier: Option<&'a dyn XmlSignatureVerifier>,
}
impl Default for CplParseOptions<'_> {
fn default() -> Self {
Self {
unknown_field_mode: UnknownFieldMode::Ignore,
schema_strict_mode: SchemaStrictMode::Off,
signature_validation_mode: SignatureValidationMode::Ignore,
signature_verifier: None,
}
}
}
pub fn strict_production_parse_options<'a>(
signature_verifier: &'a dyn XmlSignatureVerifier,
) -> CplParseOptions<'a> {
CplParseOptions {
unknown_field_mode: UnknownFieldMode::Error,
schema_strict_mode: SchemaStrictMode::Basic,
signature_validation_mode: SignatureValidationMode::RequireValid,
signature_verifier: Some(signature_verifier),
}
}
pub fn recommended_signature_verifier() -> Box<dyn XmlSignatureVerifier> {
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
{
Box::new(XmlSec1Verifier::default())
}
#[cfg(not(all(feature = "xmlsec1", not(target_arch = "wasm32"))))]
{
Box::new(ReferenceDigestXmlDsigVerifier)
}
}
fn validate_signature_structure(xml_content: &str) -> Result<(), String> {
let signature_xml = extract_first_element(xml_content, "Signature")
.ok_or_else(|| "missing Signature element".to_string())?;
if extract_first_element(&signature_xml, "SignedInfo").is_none() {
return Err("missing SignedInfo element".to_string());
}
let signature_value_raw = extract_first_element_text(&signature_xml, "SignatureValue")
.ok_or_else(|| "missing SignatureValue element".to_string())?;
let signature_value = collapse_xml_text(&signature_value_raw);
if signature_value.is_empty() {
return Err("SignatureValue is empty".to_string());
}
let decoded = base64::engine::general_purpose::STANDARD
.decode(signature_value.as_bytes())
.map_err(|e| format!("invalid SignatureValue base64: {}", e))?;
if decoded.is_empty() {
return Err("SignatureValue decodes to zero bytes".to_string());
}
if let Some(signature_method_alg) = extract_signature_method_algorithm(&signature_xml) {
let is_supported = matches!(
signature_method_alg.as_str(),
"http://www.w3.org/2000/09/xmldsig#rsa-sha1"
| "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
| "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
| "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
);
if !is_supported {
return Err(format!(
"unsupported SignatureMethod algorithm: {}",
signature_method_alg
));
}
}
Ok(())
}
fn validate_reference_digests(xml_content: &str) -> Result<(), String> {
let signature_xml = extract_first_element(xml_content, "Signature")
.ok_or_else(|| "missing Signature element".to_string())?;
let references = extract_reference_entries(&signature_xml)?;
if references.is_empty() {
return Err("SignedInfo contains no Reference elements".to_string());
}
for reference in references {
let digest_algorithm = HashAlgorithm::from_uri(&reference.digest_method_algorithm)
.ok_or_else(|| {
format!(
"unsupported DigestMethod algorithm: {}",
reference.digest_method_algorithm
)
})?;
let expected_digest = base64::engine::general_purpose::STANDARD
.decode(reference.digest_value.as_bytes())
.map_err(|e| format!("invalid DigestValue base64: {}", e))?;
if expected_digest.len() != digest_algorithm.digest_len() {
return Err(format!(
"DigestValue length {} does not match {} digest length {}",
expected_digest.len(),
digest_algorithm,
digest_algorithm.digest_len()
));
}
match reference.uri.as_deref().unwrap_or("") {
"" => {
let unsigned_xml = strip_first_signature_element(xml_content)
.ok_or_else(|| "failed to remove Signature element for URI=\"\"".to_string())?;
let normalized = normalize_xml_for_digest(&unsigned_xml);
let actual_digest = compute_hash(digest_algorithm, normalized.as_bytes());
if actual_digest != expected_digest {
return Err(format!(
"DigestValue mismatch for Reference URI=\"\" (algorithm {})",
digest_algorithm
));
}
}
uri if uri.starts_with('#') => {}
uri => {
return Err(format!(
"unsupported Reference URI '{}'; only empty or fragment URIs are supported",
uri
));
}
}
}
Ok(())
}
fn compute_hash(algorithm: HashAlgorithm, bytes: &[u8]) -> Vec<u8> {
match algorithm {
HashAlgorithm::Sha1 => {
use sha1::Digest;
let mut hasher = sha1::Sha1::new();
hasher.update(bytes);
hasher.finalize().to_vec()
}
HashAlgorithm::Sha256 => {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(bytes);
hasher.finalize().to_vec()
}
}
}
#[derive(Debug, Clone)]
struct SignatureReferenceEntry {
uri: Option<String>,
digest_method_algorithm: String,
digest_value: String,
}
fn extract_signature_method_algorithm(signature_xml: &str) -> Option<String> {
let re = regex::Regex::new(
r#"<(?:(?:\w+):)?SignatureMethod\b[^>]*\bAlgorithm\s*=\s*\"([^\"]+)\"[^>]*/?>"#,
)
.ok()?;
re.captures(signature_xml)
.and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()))
}
fn extract_reference_entries(signature_xml: &str) -> Result<Vec<SignatureReferenceEntry>, String> {
let reference_re = regex::Regex::new(
r#"(?s)<(?:(?:\w+):)?Reference\b([^>]*)>(.*?)</(?:(?:\w+):)?Reference>"#,
)
.map_err(|e| e.to_string())?;
let uri_re = regex::Regex::new(r#"\bURI\s*=\s*\"([^\"]*)\""#).map_err(|e| e.to_string())?;
let digest_method_re = regex::Regex::new(
r#"<(?:(?:\w+):)?DigestMethod\b[^>]*\bAlgorithm\s*=\s*\"([^\"]+)\"[^>]*/?>"#,
)
.map_err(|e| e.to_string())?;
let digest_value_re = regex::Regex::new(
r#"(?s)<(?:(?:\w+):)?DigestValue\b[^>]*>(.*?)</(?:(?:\w+):)?DigestValue>"#,
)
.map_err(|e| e.to_string())?;
let mut out = Vec::new();
for captures in reference_re.captures_iter(signature_xml) {
let attrs = captures
.get(1)
.map(|m| m.as_str())
.ok_or_else(|| "internal parse error while reading Reference attributes".to_string())?;
let inner = captures
.get(2)
.map(|m| m.as_str())
.ok_or_else(|| "internal parse error while reading Reference body".to_string())?;
let uri = uri_re
.captures(attrs)
.and_then(|c| c.get(1).map(|m| m.as_str().to_string()));
let digest_method_algorithm = digest_method_re
.captures(inner)
.and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()))
.ok_or_else(|| "Reference missing DigestMethod/@Algorithm".to_string())?;
let digest_value = digest_value_re
.captures(inner)
.and_then(|c| c.get(1).map(|m| collapse_xml_text(m.as_str())))
.ok_or_else(|| "Reference missing DigestValue".to_string())?;
out.push(SignatureReferenceEntry {
uri,
digest_method_algorithm,
digest_value,
});
}
Ok(out)
}
fn strip_first_signature_element(xml: &str) -> Option<String> {
let re = regex::Regex::new(
r#"(?s)<(?:(?:\w+):)?Signature\b[^>]*>.*?</(?:(?:\w+):)?Signature\s*>"#,
)
.ok()?;
let m = re.find(xml)?;
let mut out = String::with_capacity(xml.len() - (m.end() - m.start()));
out.push_str(&xml[..m.start()]);
out.push_str(&xml[m.end()..]);
Some(out)
}
fn normalize_xml_for_digest(xml: &str) -> String {
let no_decl = xml
.strip_prefix("\u{FEFF}")
.unwrap_or(xml)
.trim();
let decl_re = regex::Regex::new(r#"(?s)^\s*<\?xml[^>]*\?>"#).unwrap();
let without_decl = decl_re.replace(no_decl, "").to_string();
let inter_tag_ws_re = regex::Regex::new(r#">\s+<"#).unwrap();
inter_tag_ws_re.replace_all(without_decl.trim(), "><").to_string()
}
fn extract_first_element(xml: &str, local_name: &str) -> Option<String> {
let escaped = regex::escape(local_name);
let pattern = format!(
r#"(?s)<(?:(?:\w+):)?{name}\b[^>]*>.*?</(?:(?:\w+):)?{name}\s*>"#,
name = escaped
);
let re = regex::Regex::new(&pattern).ok()?;
re.find(xml).map(|m| m.as_str().to_string())
}
fn extract_first_element_text(xml: &str, local_name: &str) -> Option<String> {
let escaped = regex::escape(local_name);
let pattern = format!(
r#"(?s)<(?:(?:\w+):)?{name}\b[^>]*>(.*?)</(?:(?:\w+):)?{name}\s*>"#,
name = escaped
);
let re = regex::Regex::new(&pattern).ok()?;
re.captures(xml)
.and_then(|c| c.get(1).map(|m| m.as_str().to_string()))
}
fn collapse_xml_text(text: &str) -> String {
text.chars().filter(|c| !c.is_whitespace()).collect()
}
mod de_helpers {
use crate::cpl::{
CodingEquations, ColorPrimaries, EditRate, LanguageTag,
McaTagSymbol, TransferCharacteristic, VideoCodec,
};
use serde::{Deserialize, Deserializer};
pub fn de_optional_edit_rate<'de, D: Deserializer<'de>>(d: D) -> Result<Option<EditRate>, D::Error> {
let s = String::deserialize(d)?;
let trimmed = s.trim();
if trimmed.is_empty() { Ok(None) }
else {
let normalized = trimmed.replace('/', " ");
EditRate::parse(&normalized).map(Some).map_err(serde::de::Error::custom)
}
}
pub fn de_optional_color_primaries<'de, D: Deserializer<'de>>(d: D) -> Result<Option<ColorPrimaries>, D::Error> {
let s = String::deserialize(d)?;
Ok(if s.trim().is_empty() { None } else { Some(ColorPrimaries::from_ul(s.trim())) })
}
pub fn de_optional_transfer_characteristic<'de, D: Deserializer<'de>>(d: D) -> Result<Option<TransferCharacteristic>, D::Error> {
let s = String::deserialize(d)?;
Ok(if s.trim().is_empty() { None } else { Some(TransferCharacteristic::from_ul(s.trim())) })
}
pub fn de_optional_video_codec<'de, D: Deserializer<'de>>(d: D) -> Result<Option<VideoCodec>, D::Error> {
let s = String::deserialize(d)?;
Ok(if s.trim().is_empty() { None } else { Some(VideoCodec::from_ul(s.trim())) })
}
pub fn de_optional_coding_equations<'de, D: Deserializer<'de>>(d: D) -> Result<Option<CodingEquations>, D::Error> {
let s = String::deserialize(d)?;
Ok(if s.trim().is_empty() { None } else { Some(CodingEquations::from_ul(s.trim())) })
}
pub fn de_optional_mca_tag_symbol<'de, D: Deserializer<'de>>(d: D) -> Result<Option<McaTagSymbol>, D::Error> {
let s = String::deserialize(d)?;
Ok(if s.trim().is_empty() { None } else { Some(McaTagSymbol::parse(s.trim())) })
}
pub fn de_optional_language_tag<'de, D: Deserializer<'de>>(d: D) -> Result<Option<LanguageTag>, D::Error> {
let s = String::deserialize(d)?;
let trimmed = s.trim();
if trimmed.is_empty() { Ok(None) }
else { LanguageTag::parse(trimmed).map(Some).map_err(serde::de::Error::custom) }
}
pub fn de_optional_color_siting<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u32>, D::Error> {
let s = String::deserialize(d)?;
let s = s.trim();
if s.is_empty() { return Ok(None); }
if let Ok(n) = s.parse::<u32>() { return Ok(Some(n)); }
let v = match s.to_lowercase().as_str() {
"cositing" => 0,
"horizcositing" => 1,
"threetap" => 2,
"quincunx" => 3,
"rec709" => 4,
"rec601" => 6,
_ => return Ok(None),
};
Ok(Some(v))
}
pub fn de_language_tag_list<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<LanguageTag>, D::Error> {
let s = String::deserialize(d)?;
s.split(',')
.map(|part| part.trim())
.filter(|part| !part.is_empty())
.map(|part| LanguageTag::parse(part).map_err(serde::de::Error::custom))
.collect()
}
}
fn default_content_kind() -> ContentKindElement {
ContentKindElement {
kind: ContentKind::Other("unknown".to_string()),
scope: None,
}
}
pub const CONTENT_KIND_DEFAULT_SCOPE: &str =
"http://www.smpte-ra.org/schemas/2067-3/2013#content-kind";
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContentKindElement {
pub kind: ContentKind,
pub scope: Option<String>,
}
impl ContentKindElement {
pub fn effective_scope(&self) -> &str {
self.scope.as_deref().unwrap_or(CONTENT_KIND_DEFAULT_SCOPE)
}
}
impl std::fmt::Display for ContentKindElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.kind.fmt(f)
}
}
impl PartialEq<ContentKind> for ContentKindElement {
fn eq(&self, other: &ContentKind) -> bool {
self.kind == *other
}
}
impl From<ContentKind> for ContentKindElement {
fn from(kind: ContentKind) -> Self {
Self { kind, scope: None }
}
}
impl<'de> Deserialize<'de> for ContentKindElement {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct ContentKindElementVisitor;
impl<'de> Visitor<'de> for ContentKindElementVisitor {
type Value = ContentKindElement;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or an object with text content and optional @scope attribute")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(ContentKindElement {
kind: ContentKind::parse(value),
scope: None,
})
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut text = None;
let mut scope = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"$text" | "#text" | "$value" => {
if text.is_some() {
return Err(de::Error::duplicate_field("text"));
}
text = Some(map.next_value::<String>()?);
}
"@scope" | "scope" => {
if scope.is_some() {
return Err(de::Error::duplicate_field("scope"));
}
let raw: String = map.next_value()?;
let trimmed = raw.trim();
if !trimmed.is_empty() {
scope = Some(trimmed.to_string());
}
}
_ => {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let kind = ContentKind::parse(text.as_deref().unwrap_or(""));
Ok(ContentKindElement { kind, scope })
}
}
deserializer.deserialize_any(ContentKindElementVisitor)
}
}
impl Serialize for ContentKindElement {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
if self.scope.is_some() {
let mut state = serializer.serialize_struct("ContentKindElement", 2)?;
state.serialize_field("$text", &self.kind.to_string())?;
state.serialize_field("@scope", &self.scope)?;
state.end()
} else {
serializer.serialize_str(&self.kind.to_string())
}
}
}
#[cfg(feature = "jsonschema")]
impl schemars::JsonSchema for ContentKindElement {
fn schema_name() -> String {
"ContentKindElement".to_owned()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::*;
let string_schema = gen.subschema_for::<String>();
let mut obj = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj_validation = obj.object();
obj_validation.properties.insert("$text".to_owned(), gen.subschema_for::<String>());
obj_validation.properties.insert("@scope".to_owned(), gen.subschema_for::<Option<String>>());
obj_validation.required.insert("$text".to_owned());
SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![string_schema, obj.into()]),
..Default::default()
})),
..Default::default()
}
.into()
}
}
pub const MARKER_LABEL_DEFAULT_SCOPE: &str =
"http://www.smpte-ra.org/schemas/2067-3/2013#standard-markers";
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct MarkerLabelElement {
pub label: MarkerLabel,
pub scope: Option<String>,
}
impl MarkerLabelElement {
pub fn effective_scope(&self) -> &str {
self.scope.as_deref().unwrap_or(MARKER_LABEL_DEFAULT_SCOPE)
}
}
impl std::fmt::Display for MarkerLabelElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.label.fmt(f)
}
}
impl PartialEq<MarkerLabel> for MarkerLabelElement {
fn eq(&self, other: &MarkerLabel) -> bool {
self.label == *other
}
}
impl From<MarkerLabel> for MarkerLabelElement {
fn from(label: MarkerLabel) -> Self {
Self { label, scope: None }
}
}
impl<'de> Deserialize<'de> for MarkerLabelElement {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct MarkerLabelElementVisitor;
impl<'de> Visitor<'de> for MarkerLabelElementVisitor {
type Value = MarkerLabelElement;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or an object with text content and optional @scope attribute")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(MarkerLabelElement {
label: MarkerLabel::parse(value),
scope: None,
})
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut text = None;
let mut scope = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"$text" | "#text" | "$value" => {
if text.is_some() {
return Err(de::Error::duplicate_field("text"));
}
text = Some(map.next_value::<String>()?);
}
"@scope" | "scope" => {
if scope.is_some() {
return Err(de::Error::duplicate_field("scope"));
}
let raw: String = map.next_value()?;
let trimmed = raw.trim();
if !trimmed.is_empty() {
scope = Some(trimmed.to_string());
}
}
_ => {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let label = MarkerLabel::parse(text.as_deref().unwrap_or(""));
Ok(MarkerLabelElement { label, scope })
}
}
deserializer.deserialize_any(MarkerLabelElementVisitor)
}
}
impl Serialize for MarkerLabelElement {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
if self.scope.is_some() {
let mut state = serializer.serialize_struct("MarkerLabelElement", 2)?;
state.serialize_field("$text", &self.label.to_string())?;
state.serialize_field("@scope", &self.scope)?;
state.end()
} else {
serializer.serialize_str(&self.label.to_string())
}
}
}
#[cfg(feature = "jsonschema")]
impl schemars::JsonSchema for MarkerLabelElement {
fn schema_name() -> String {
"MarkerLabelElement".to_owned()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::*;
let string_schema = gen.subschema_for::<String>();
let mut obj = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj_validation = obj.object();
obj_validation.properties.insert("$text".to_owned(), gen.subschema_for::<String>());
obj_validation.properties.insert("@scope".to_owned(), gen.subschema_for::<Option<String>>());
obj_validation.required.insert("$text".to_owned());
SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![string_schema, obj.into()]),
..Default::default()
})),
..Default::default()
}
.into()
}
}
pub fn strip_xml_namespaces(xml: &str) -> String {
let tag_prefix_re = regex::Regex::new(r"<(/?)(\w+):(\w)").unwrap();
let result = tag_prefix_re.replace_all(xml, "<$1$3");
let xmlns_prefix_re = regex::Regex::new(r#"\s+xmlns:\w+="[^"]*""#).unwrap();
xmlns_prefix_re.replace_all(&result, "").to_string()
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct LanguageString {
pub text: String,
pub language: Option<LanguageTag>, }
impl Default for LanguageString {
fn default() -> Self {
Self {
text: String::new(),
language: None,
}
}
}
impl std::fmt::Display for LanguageString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(lang) = &self.language {
write!(f, "{} ({})", self.text, lang)
} else {
write!(f, "{}", self.text)
}
}
}
impl<'de> Deserialize<'de> for LanguageString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct LanguageStringVisitor;
impl<'de> Visitor<'de> for LanguageStringVisitor {
type Value = LanguageString;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or an object with text and optional language")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(LanguageString {
text: value.to_string(),
language: None,
})
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut text = None;
let mut language = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"$text" | "#text" | "$value" => {
if text.is_some() {
return Err(de::Error::duplicate_field("text"));
}
text = Some(map.next_value()?);
}
"@language" | "language" => {
if language.is_some() {
return Err(de::Error::duplicate_field("language"));
}
let raw: String = map.next_value()?;
let trimmed = raw.trim();
if !trimmed.is_empty() {
language = Some(LanguageTag(trimmed.to_string()));
}
}
_ => {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(LanguageString {
text: text.unwrap_or_default(),
language,
})
}
}
deserializer.deserialize_any(LanguageStringVisitor)
}
}
impl Serialize for LanguageString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
if self.language.is_some() {
let mut state = serializer.serialize_struct("LanguageString", 2)?;
state.serialize_field("$text", &self.text)?;
state.serialize_field("@language", &self.language)?;
state.end()
} else {
serializer.serialize_str(&self.text)
}
}
}
#[cfg(feature = "jsonschema")]
impl schemars::JsonSchema for LanguageString {
fn schema_name() -> String {
"LanguageString".to_owned()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::*;
let string_schema = gen.subschema_for::<String>();
let mut obj = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj_validation = obj.object();
obj_validation.properties.insert("$text".to_owned(), gen.subschema_for::<String>());
obj_validation.properties.insert("@language".to_owned(), gen.subschema_for::<Option<String>>());
obj_validation.required.insert("$text".to_owned());
SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![string_schema, obj.into()]),
..Default::default()
})),
..Default::default()
}
.into()
}
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct LocaleList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Locale"))]
#[cfg_attr(feature = "wasm", serde(rename = "locale", alias = "Locale"))]
pub locales: Vec<Locale>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Locale {
#[cfg_attr(not(feature = "wasm"), serde(rename = "LanguageList", default))]
#[cfg_attr(feature = "wasm", serde(rename = "languageList", alias = "LanguageList", default))]
pub language_list: Option<LanguageList>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "RegionList", default))]
#[cfg_attr(feature = "wasm", serde(rename = "regionList", alias = "RegionList", default))]
pub region_list: Option<RegionList>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentMaturityRatingList", default))]
#[cfg_attr(feature = "wasm", serde(rename = "contentMaturityRatingList", alias = "ContentMaturityRatingList", default))]
pub content_maturity_rating_list: Option<ContentMaturityRatingList>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContentMaturityRatingList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentMaturityRating"))]
#[cfg_attr(feature = "wasm", serde(rename = "contentMaturityRating", alias = "ContentMaturityRating"))]
pub ratings: Vec<ContentMaturityRating>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContentMaturityRating {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Agency"))]
#[cfg_attr(feature = "wasm", serde(rename = "agency", alias = "Agency"))]
pub agency: String,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Rating", default))]
#[cfg_attr(feature = "wasm", serde(rename = "rating", alias = "Rating", default))]
pub rating: Option<String>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Audience", default))]
#[cfg_attr(feature = "wasm", serde(rename = "audience", alias = "Audience", default))]
pub audience: Option<AudienceElement>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct AudienceElement {
#[serde(rename = "@scope", default)]
pub scope: Option<String>,
#[serde(rename = "$text", default)]
pub text: Option<String>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct LanguageList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Language"))]
#[cfg_attr(feature = "wasm", serde(rename = "language", alias = "Language"))]
pub languages: Vec<LanguageTag>, }
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct RegionList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Region"))]
#[cfg_attr(feature = "wasm", serde(rename = "region", alias = "Region"))]
pub regions: Vec<String>, }
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ExtensionProperties {
#[cfg_attr(not(feature = "wasm"), serde(rename = "ApplicationIdentification", default))]
#[cfg_attr(feature = "wasm", serde(rename = "applicationIdentification", alias = "ApplicationIdentification", default))]
pub application_identification: Option<String>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "MaxCLL", default))]
#[cfg_attr(feature = "wasm", serde(rename = "maxCLL", alias = "MaxCLL", default))]
pub max_cll: Option<u32>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "MaxFALL", default))]
#[cfg_attr(feature = "wasm", serde(rename = "maxFALL", alias = "MaxFALL", default))]
pub max_fall: Option<u32>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct EssenceDescriptorList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "EssenceDescriptor"))]
#[cfg_attr(feature = "wasm", serde(rename = "essenceDescriptor", alias = "EssenceDescriptor"))]
pub essence_descriptors: Vec<EssenceDescriptor>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct EssenceDescriptor {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
#[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
pub id: ImfUuid,
#[cfg_attr(not(feature = "wasm"), serde(rename = "RGBADescriptor", default))]
#[cfg_attr(feature = "wasm", serde(rename = "rgbaDescriptor", alias = "RGBADescriptor", default))]
pub rgba_descriptor: Option<RGBADescriptor>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "CDCIDescriptor", default))]
#[cfg_attr(feature = "wasm", serde(rename = "cdciDescriptor", alias = "CDCIDescriptor", default))]
pub cdci_descriptor: Option<CDCIDescriptor>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "WAVEPCMDescriptor", default))]
#[cfg_attr(feature = "wasm", serde(rename = "wavePCMDescriptor", alias = "WAVEPCMDescriptor", default))]
pub wave_pcm_descriptor: Option<WAVEPCMDescriptor>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "DCTimedTextDescriptor", default))]
#[cfg_attr(feature = "wasm", serde(rename = "dcTimedTextDescriptor", alias = "DCTimedTextDescriptor", default))]
pub dc_timed_text_descriptor: Option<DCTimedTextDescriptor>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "IABEssenceDescriptor", default))]
#[cfg_attr(feature = "wasm", serde(rename = "iabEssenceDescriptor", alias = "IABEssenceDescriptor", default))]
pub iab_essence_descriptor: Option<IABEssenceDescriptor>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ISXDDataEssenceDescriptor", default))]
#[cfg_attr(feature = "wasm", serde(rename = "isxdDataEssenceDescriptor", alias = "ISXDDataEssenceDescriptor", default))]
pub isxd_data_essence_descriptor: Option<ISXDDataEssenceDescriptor>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct RGBADescriptor {
#[serde(rename = "InstanceID", default)]
pub instance_id: Option<String>,
#[serde(rename = "DisplayWidth", default)]
pub display_width: Option<u32>,
#[serde(rename = "DisplayHeight", default)]
pub display_height: Option<u32>,
#[serde(rename = "StoredWidth", default)]
pub stored_width: Option<u32>,
#[serde(rename = "StoredHeight", default)]
pub stored_height: Option<u32>,
#[serde(rename = "SampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub sample_rate: Option<EditRate>,
#[serde(rename = "ImageAspectRatio", default)]
pub image_aspect_ratio: Option<String>,
#[serde(rename = "ColorPrimaries", default, deserialize_with = "de_helpers::de_optional_color_primaries")]
pub color_primaries: Option<ColorPrimaries>,
#[serde(rename = "TransferCharacteristic", default, deserialize_with = "de_helpers::de_optional_transfer_characteristic")]
pub transfer_characteristic: Option<TransferCharacteristic>,
#[serde(rename = "CodingEquations", default, deserialize_with = "de_helpers::de_optional_coding_equations")]
pub coding_equations: Option<CodingEquations>,
#[serde(rename = "PictureCompression", default, deserialize_with = "de_helpers::de_optional_video_codec")]
pub picture_compression: Option<VideoCodec>,
#[serde(rename = "FrameLayout", default)]
pub frame_layout: Option<String>,
#[serde(rename = "DisplayF2Offset", default)]
pub display_f2_offset: Option<i32>,
#[serde(rename = "ComponentMaxRef", default)]
pub component_max_ref: Option<u32>,
#[serde(rename = "ComponentMinRef", default)]
pub component_min_ref: Option<u32>,
#[serde(rename = "ScanningDirection", default)]
pub scanning_direction: Option<String>,
#[serde(rename = "StoredF2Offset", default)]
pub stored_f2_offset: Option<i32>,
#[serde(rename = "SampledWidth", default)]
pub sampled_width: Option<u32>,
#[serde(rename = "SampledHeight", default)]
pub sampled_height: Option<u32>,
#[serde(rename = "SampledXOffset", default)]
pub sampled_x_offset: Option<u32>,
#[serde(rename = "SampledYOffset", default)]
pub sampled_y_offset: Option<u32>,
#[serde(rename = "AlphaTransparency", default)]
pub alpha_transparency: Option<String>,
#[serde(rename = "ImageAlignmentOffset", default)]
pub image_alignment_offset: Option<u32>,
#[serde(rename = "ImageStartOffset", default)]
pub image_start_offset: Option<u32>,
#[serde(rename = "ImageEndOffset", default)]
pub image_end_offset: Option<u32>,
#[serde(rename = "FieldDominance", default)]
pub field_dominance: Option<u32>,
#[serde(rename = "AlphaMaxRef", default)]
pub alpha_max_ref: Option<u32>,
#[serde(rename = "AlphaMinRef", default)]
pub alpha_min_ref: Option<u32>,
#[serde(rename = "Palette", default)]
pub palette: Option<String>,
#[serde(rename = "PaletteLayout", default)]
pub palette_layout: Option<String>,
#[serde(rename = "LinkedTrackID", default)]
pub linked_track_id: Option<u32>,
#[serde(rename = "SubDescriptors", default)]
pub sub_descriptors: Option<VideoSubDescriptors>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct CDCIDescriptor {
#[serde(rename = "InstanceUID", alias = "InstanceID", default)]
pub instance_id: Option<String>,
#[serde(rename = "StoredWidth", default)]
pub stored_width: Option<u32>,
#[serde(rename = "StoredHeight", default)]
pub stored_height: Option<u32>,
#[serde(rename = "DisplayWidth", default)]
pub display_width: Option<u32>,
#[serde(rename = "DisplayHeight", default)]
pub display_height: Option<u32>,
#[serde(rename = "ActiveWidth", default)]
pub active_width: Option<u32>,
#[serde(rename = "ActiveHeight", default)]
pub active_height: Option<u32>,
#[serde(rename = "SampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub sample_rate: Option<EditRate>,
#[serde(rename = "ImageAspectRatio", default)]
pub image_aspect_ratio: Option<String>,
#[serde(rename = "ColorPrimaries", default, deserialize_with = "de_helpers::de_optional_color_primaries")]
pub color_primaries: Option<ColorPrimaries>,
#[serde(rename = "TransferCharacteristic", default, deserialize_with = "de_helpers::de_optional_transfer_characteristic")]
pub transfer_characteristic: Option<TransferCharacteristic>,
#[serde(rename = "CodingEquations", default, deserialize_with = "de_helpers::de_optional_coding_equations")]
pub coding_equations: Option<CodingEquations>,
#[serde(rename = "PictureCompression", default, deserialize_with = "de_helpers::de_optional_video_codec")]
pub picture_compression: Option<VideoCodec>,
#[serde(rename = "ComponentDepth", default)]
pub component_depth: Option<u32>,
#[serde(rename = "FrameLayout", default)]
pub frame_layout: Option<String>,
#[serde(rename = "DisplayF2Offset", default)]
pub display_f2_offset: Option<i32>,
#[serde(rename = "HorizontalSubsampling", default)]
pub horizontal_subsampling: Option<u32>,
#[serde(rename = "VerticalSubsampling", default)]
pub vertical_subsampling: Option<u32>,
#[serde(rename = "ColorSiting", default, deserialize_with = "de_helpers::de_optional_color_siting")]
pub color_siting: Option<u32>,
#[serde(rename = "BlackRefLevel", default)]
pub black_ref_level: Option<u32>,
#[serde(rename = "WhiteRefLevel", default)]
pub white_ref_level: Option<u32>,
#[serde(rename = "ColorRange", default)]
pub color_range: Option<u32>,
#[serde(rename = "StoredF2Offset", default)]
pub stored_f2_offset: Option<i32>,
#[serde(rename = "SampledWidth", default)]
pub sampled_width: Option<u32>,
#[serde(rename = "SampledHeight", default)]
pub sampled_height: Option<u32>,
#[serde(rename = "SampledXOffset", default)]
pub sampled_x_offset: Option<u32>,
#[serde(rename = "SampledYOffset", default)]
pub sampled_y_offset: Option<u32>,
#[serde(rename = "AlphaTransparency", default)]
pub alpha_transparency: Option<String>,
#[serde(rename = "ImageAlignmentOffset", default)]
pub image_alignment_offset: Option<u32>,
#[serde(rename = "ImageStartOffset", default)]
pub image_start_offset: Option<u32>,
#[serde(rename = "ImageEndOffset", default)]
pub image_end_offset: Option<u32>,
#[serde(rename = "FieldDominance", default)]
pub field_dominance: Option<u32>,
#[serde(rename = "ReversedByteOrder", default)]
pub reversed_byte_order: Option<String>,
#[serde(rename = "PaddingBits", default)]
pub padding_bits: Option<i32>,
#[serde(rename = "AlphaSampleDepth", default)]
pub alpha_sample_depth: Option<u32>,
#[serde(rename = "LinkedTrackID", default)]
pub linked_track_id: Option<u32>,
#[serde(rename = "SubDescriptors", default)]
pub sub_descriptors: Option<VideoSubDescriptors>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct VideoSubDescriptors {
#[serde(rename = "PHDRMetadataTrackSubDescriptor", default)]
pub phdr_metadata_track_sub_descriptor: Option<PHDRMetadataTrackSubDescriptor>,
#[serde(rename = "JPEG2000SubDescriptor", default)]
pub jpeg2000_sub_descriptor: Option<JPEG2000SubDescriptor>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct JPEG2000SubDescriptor {
#[serde(rename = "InstanceID", default)]
pub instance_id: Option<String>,
#[serde(rename = "Rsiz", default)]
pub rsiz: Option<u32>,
#[serde(rename = "Xsiz", default)]
pub xsiz: Option<u32>,
#[serde(rename = "Ysiz", default)]
pub ysiz: Option<u32>,
#[serde(rename = "XOsiz", default)]
pub xo_siz: Option<u32>,
#[serde(rename = "YOsiz", default)]
pub yo_siz: Option<u32>,
#[serde(rename = "XTsiz", default)]
pub xt_siz: Option<u32>,
#[serde(rename = "YTsiz", default)]
pub yt_siz: Option<u32>,
#[serde(rename = "XTOsiz", default)]
pub xto_siz: Option<u32>,
#[serde(rename = "YTOsiz", default)]
pub yto_siz: Option<u32>,
#[serde(rename = "Csiz", default)]
pub csiz: Option<u32>,
#[serde(rename = "CodingStyleDefault", default)]
pub coding_style_default: Option<String>,
#[serde(rename = "QuantizationDefault", default)]
pub quantization_default: Option<String>,
#[serde(rename = "J2CLayout", default)]
pub j2c_layout: Option<J2CLayout>,
#[serde(rename = "J2KExtendedCapabilities", default)]
pub j2k_extended_capabilities: Option<J2KExtendedCapabilities>,
#[serde(rename = "PictureComponentSizing", default)]
pub picture_component_sizing: Option<PictureComponentSizing>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct J2CLayout {
#[serde(rename = "RGBAComponent", default)]
pub components: Vec<RGBALayoutComponent>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct RGBALayoutComponent {
#[serde(rename = "Code", default)]
pub code: String,
#[serde(rename = "ComponentSize", default)]
pub component_size: u32,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct J2KExtendedCapabilities {
#[serde(rename = "Pcap", default)]
pub pcap: Option<u64>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct PictureComponentSizing {
#[serde(rename = "J2KComponentSizing", default)]
pub components: Vec<J2KComponentSizing>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct J2KComponentSizing {
#[serde(rename = "Ssiz", default)]
pub ssiz: Option<u32>,
#[serde(rename = "XRSiz", default)]
pub xr_siz: Option<u32>,
#[serde(rename = "YRSiz", default)]
pub yr_siz: Option<u32>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct PHDRMetadataTrackSubDescriptor {
#[serde(rename = "InstanceID", default)]
pub instance_id: Option<String>,
#[serde(rename = "PHDRMetadataTrackSubDescriptor_DataDefinition", default)]
pub data_definition: Option<String>,
#[serde(rename = "PHDRMetadataTrackSubDescriptor_SimplePayloadSID", default)]
pub simple_payload_sid: Option<u32>,
#[serde(rename = "PHDRMetadataTrackSubDescriptor_SourceTrackID", default)]
pub source_track_id: Option<u32>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct WAVEPCMDescriptor {
#[serde(rename = "InstanceID", alias = "InstanceUID", default)]
pub instance_id: Option<String>,
#[serde(rename = "SampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub sample_rate: Option<EditRate>,
#[serde(rename = "AudioSampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub audio_sample_rate: Option<EditRate>,
#[serde(rename = "ChannelCount", default)]
pub channel_count: Option<u32>,
#[serde(rename = "QuantizationBits", default)]
pub quantization_bits: Option<u32>,
#[serde(rename = "LinkedTrackID", default)]
pub linked_track_id: Option<u32>,
#[serde(rename = "SubDescriptors", default)]
pub sub_descriptors: Option<AudioSubDescriptors>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct AudioSubDescriptors {
#[serde(rename = "SoundfieldGroupLabelSubDescriptor", default)]
pub soundfield_group_label_sub_descriptor: Option<SoundfieldGroupLabelSubDescriptor>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct SoundfieldGroupLabelSubDescriptor {
#[serde(rename = "MCATagSymbol", default, deserialize_with = "de_helpers::de_optional_mca_tag_symbol")]
pub mca_tag_symbol: Option<McaTagSymbol>,
#[serde(rename = "MCATagName", default)]
pub mca_tag_name: Option<String>,
#[serde(rename = "MCAAudioContentKind", default)]
pub mca_audio_content_kind: Option<String>,
#[serde(rename = "RFC5646SpokenLanguage", alias = "RFC5646AudioLanguageCode", default, deserialize_with = "de_helpers::de_optional_language_tag")]
pub rfc5646_spoken_language: Option<LanguageTag>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct DCTimedTextDescriptor {
#[serde(rename = "InstanceID", alias = "InstanceUID", default)]
pub instance_id: Option<String>,
#[serde(rename = "LinkedTrackID", default)]
pub linked_track_id: Option<u32>,
#[serde(rename = "SampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub sample_rate: Option<EditRate>,
#[serde(rename = "RFC5646LanguageTagList", default, deserialize_with = "de_helpers::de_language_tag_list")]
pub rfc5646_language_tag_list: Vec<LanguageTag>,
#[serde(rename = "NamespaceURI", default)]
pub namespace_uri: Option<String>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct IABEssenceDescriptor {
#[serde(rename = "InstanceID", alias = "InstanceUID", default)]
pub instance_id: Option<String>,
#[serde(rename = "LinkedTrackID", default)]
pub linked_track_id: Option<u32>,
#[serde(rename = "SampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub sample_rate: Option<EditRate>,
#[serde(rename = "AudioSampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub audio_sample_rate: Option<EditRate>,
#[serde(rename = "ChannelCount", default)]
pub channel_count: Option<u32>,
#[serde(rename = "QuantizationBits", default)]
pub quantization_bits: Option<u32>,
#[serde(rename = "ContainerFormat", default)]
pub container_format: Option<String>,
#[serde(rename = "SoundCompression", default)]
pub sound_compression: Option<String>,
#[serde(rename = "Codec", default)]
pub codec: Option<String>,
#[serde(rename = "ElectrospatialFormulation", default)]
pub electrospatial_formulation: Option<u32>,
#[serde(rename = "SubDescriptors", default)]
pub sub_descriptors: Option<IABSubDescriptors>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct IABSubDescriptors {
#[serde(rename = "IABSoundfieldLabelSubDescriptor", default)]
pub iab_soundfield_label_sub_descriptor: Option<IABSoundfieldLabelSubDescriptor>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct IABSoundfieldLabelSubDescriptor {
#[serde(rename = "InstanceID", alias = "InstanceUID", default)]
pub instance_id: Option<String>,
#[serde(rename = "MCATagSymbol", default, deserialize_with = "de_helpers::de_optional_mca_tag_symbol")]
pub mca_tag_symbol: Option<McaTagSymbol>,
#[serde(rename = "MCATagName", default)]
pub mca_tag_name: Option<String>,
#[serde(rename = "MCALabelDictionaryID", default)]
pub mca_label_dictionary_id: Option<String>,
#[serde(rename = "RFC5646SpokenLanguage", alias = "RFC5646AudioLanguageCode", default, deserialize_with = "de_helpers::de_optional_language_tag")]
pub rfc5646_spoken_language: Option<LanguageTag>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct IsxdSubDescriptors {
#[serde(rename = "ContainerConstraintsSubDescriptor", default)]
pub container_constraints_sub_descriptor: Option<ContainerConstraintsSubDescriptor>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContainerConstraintsSubDescriptor {
#[serde(rename = "InstanceID", alias = "InstanceUID", default)]
pub instance_id: Option<String>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ISXDDataEssenceDescriptor {
#[serde(rename = "InstanceID", alias = "InstanceUID", default)]
pub instance_id: Option<String>,
#[serde(rename = "LinkedTrackID", default)]
pub linked_track_id: Option<u32>,
#[serde(rename = "SampleRate", default, deserialize_with = "de_helpers::de_optional_edit_rate")]
pub sample_rate: Option<EditRate>,
#[serde(rename = "DataEssenceCoding", default)]
pub data_essence_coding: Option<String>,
#[serde(rename = "NamespaceURI", default)]
pub namespace_uri: Option<String>,
#[serde(rename = "SubDescriptors", default)]
pub sub_descriptors: Option<IsxdSubDescriptors>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct CompositionPlaylist {
#[serde(skip)]
pub namespace: CplNamespace,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
#[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
#[cfg_attr(feature = "typescript", ts(rename = "id"))]
pub id: ImfUuid,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
#[cfg_attr(feature = "wasm", serde(rename = "annotation", alias = "Annotation", default))]
#[cfg_attr(feature = "typescript", ts(rename = "annotation"))]
pub annotation: Option<LanguageString>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "IssueDate"))]
#[cfg_attr(feature = "wasm", serde(rename = "issueDate", alias = "IssueDate"))]
#[cfg_attr(feature = "typescript", ts(rename = "issueDate"))]
pub issue_date: String,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Issuer", default))]
#[cfg_attr(feature = "wasm", serde(rename = "issuer", alias = "Issuer", default))]
#[cfg_attr(feature = "typescript", ts(rename = "issuer"))]
pub issuer: Option<LanguageString>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Creator", default))]
#[cfg_attr(feature = "wasm", serde(rename = "creator", alias = "Creator", default))]
#[cfg_attr(feature = "typescript", ts(rename = "creator"))]
pub creator: Option<LanguageString>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentOriginator", default))]
#[cfg_attr(feature = "wasm", serde(rename = "contentOriginator", alias = "ContentOriginator", default))]
#[cfg_attr(feature = "typescript", ts(rename = "contentOriginator"))]
pub content_originator: Option<LanguageString>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentTitle"))]
#[cfg_attr(feature = "wasm", serde(rename = "contentTitle", alias = "ContentTitle"))]
#[cfg_attr(feature = "typescript", ts(rename = "contentTitle"))]
pub content_title: LanguageString,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentKind", default = "default_content_kind"))]
#[cfg_attr(feature = "wasm", serde(rename = "contentKind", alias = "ContentKind", default = "default_content_kind"))]
#[cfg_attr(feature = "typescript", ts(rename = "contentKind"))]
pub content_kind: ContentKindElement,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentVersionList", default))]
#[cfg_attr(feature = "wasm", serde(rename = "contentVersionList", alias = "ContentVersionList", default))]
#[cfg_attr(feature = "typescript", ts(rename = "contentVersionList"))]
pub content_version_list: Option<ContentVersionList>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "EssenceDescriptorList", default))]
#[cfg_attr(feature = "wasm", serde(rename = "essenceDescriptorList", alias = "EssenceDescriptorList", default))]
#[cfg_attr(feature = "typescript", ts(rename = "essenceDescriptorList"))]
pub essence_descriptor_list: Option<EssenceDescriptorList>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "EditRate", default, deserialize_with = "de_helpers::de_optional_edit_rate"))]
#[cfg_attr(feature = "wasm", serde(rename = "editRate", alias = "EditRate", default, deserialize_with = "de_helpers::de_optional_edit_rate"))]
#[cfg_attr(feature = "typescript", ts(rename = "editRate"))]
pub edit_rate: Option<EditRate>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "TotalRunningTime", default))]
#[cfg_attr(feature = "wasm", serde(rename = "totalRunningTime", alias = "TotalRunningTime", default))]
#[cfg_attr(feature = "typescript", ts(rename = "totalRunningTime"))]
pub total_running_time: Option<String>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "LocaleList", default))]
#[cfg_attr(feature = "wasm", serde(rename = "localeList", alias = "LocaleList", default))]
#[cfg_attr(feature = "typescript", ts(rename = "localeList"))]
pub locale_list: Option<LocaleList>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ExtensionProperties", default))]
#[cfg_attr(feature = "wasm", serde(rename = "extensionProperties", alias = "ExtensionProperties", default))]
#[cfg_attr(feature = "typescript", ts(rename = "extensionProperties"))]
pub extension_properties: Option<ExtensionProperties>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "CompositionTimecode", default))]
#[cfg_attr(feature = "wasm", serde(rename = "compositionTimecode", alias = "CompositionTimecode", default))]
#[cfg_attr(feature = "typescript", ts(rename = "compositionTimecode"))]
pub composition_timecode: Option<CompositionTimecode>,
#[serde(skip)]
pub has_signer: bool,
#[serde(skip)]
pub has_signature: bool,
#[cfg_attr(not(feature = "wasm"), serde(rename = "SegmentList"))]
#[cfg_attr(feature = "wasm", serde(rename = "segmentList", alias = "SegmentList"))]
#[cfg_attr(feature = "typescript", ts(rename = "segmentList"))]
pub segment_list: SegmentList,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(not(feature = "wasm"), serde(rename_all = "PascalCase"))]
#[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct CompositionTimecode {
#[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeDropFrame"))]
#[cfg_attr(feature = "wasm", serde(rename = "timecodeDropFrame", alias = "TimecodeDropFrame"))]
pub timecode_drop_frame: Option<bool>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeRate"))]
#[cfg_attr(feature = "wasm", serde(rename = "timecodeRate", alias = "TimecodeRate"))]
pub timecode_rate: Option<u32>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeStartAddress"))]
#[cfg_attr(feature = "wasm", serde(rename = "timecodeStartAddress", alias = "TimecodeStartAddress"))]
pub timecode_start_address: Option<String>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContentVersionList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "ContentVersion"))]
#[cfg_attr(feature = "wasm", serde(rename = "contentVersion", alias = "ContentVersion"))]
pub content_versions: Vec<ContentVersion>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContentVersion {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
#[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
pub id: String,
#[cfg_attr(not(feature = "wasm"), serde(rename = "LabelText", default))]
#[cfg_attr(feature = "wasm", serde(rename = "labelText", alias = "LabelText", default))]
pub label_text: Option<LanguageString>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct SegmentList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Segment"))]
#[cfg_attr(feature = "wasm", serde(rename = "segment", alias = "Segment"))]
pub segments: Vec<Segment>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Segment {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
#[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
pub id: ImfUuid,
#[cfg_attr(not(feature = "wasm"), serde(rename = "SequenceList"))]
#[cfg_attr(feature = "wasm", serde(rename = "sequenceList", alias = "SequenceList"))]
pub sequence_list: SequenceList,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct SequenceList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "MarkerSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "markerSequence", alias = "MarkerSequence", default))]
pub marker_sequences: Vec<MarkerSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "MainImageSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "mainImageSequence", alias = "MainImageSequence", default))]
pub main_image_sequences: Vec<MainImageSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "MainAudioSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "mainAudioSequence", alias = "MainAudioSequence", default))]
pub main_audio_sequences: Vec<MainAudioSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "SubtitlesSequence", alias = "MainSubtitleSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "subtitlesSequence", alias = "SubtitlesSequence", alias = "MainSubtitleSequence", default))]
pub subtitles_sequences: Vec<SubtitlesSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "HearingImpairedCaptionsSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "hearingImpairedCaptionsSequence", alias = "HearingImpairedCaptionsSequence", default))]
pub hearing_impaired_captions_sequences: Vec<HearingImpairedCaptionsSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ForcedNarrativeSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "forcedNarrativeSequence", alias = "ForcedNarrativeSequence", default))]
pub forced_narrative_sequences: Vec<ForcedNarrativeSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "IABSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "iabSequence", alias = "IABSequence", default))]
pub iab_sequences: Vec<IABSequence>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ISXDSequence", default))]
#[cfg_attr(feature = "wasm", serde(rename = "isxdSequence", alias = "ISXDSequence", default))]
pub isxd_sequences: Vec<ISXDSequence>,
}
pub trait SequenceAccess {
fn id(&self) -> &ImfUuid;
fn track_id(&self) -> &ImfUuid;
fn resource_list(&self) -> &ResourceList;
}
macro_rules! define_sequence_type {
($name:ident) => {
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct $name {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
#[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
pub id: ImfUuid,
#[cfg_attr(not(feature = "wasm"), serde(rename = "TrackId"))]
#[cfg_attr(feature = "wasm", serde(rename = "trackId", alias = "TrackId"))]
pub track_id: ImfUuid,
#[cfg_attr(not(feature = "wasm"), serde(rename = "ResourceList"))]
#[cfg_attr(feature = "wasm", serde(rename = "resourceList", alias = "ResourceList"))]
pub resource_list: ResourceList,
}
impl SequenceAccess for $name {
fn id(&self) -> &ImfUuid { &self.id }
fn track_id(&self) -> &ImfUuid { &self.track_id }
fn resource_list(&self) -> &ResourceList { &self.resource_list }
}
};
}
define_sequence_type!(MarkerSequence);
define_sequence_type!(MainImageSequence);
define_sequence_type!(MainAudioSequence);
define_sequence_type!(SubtitlesSequence);
define_sequence_type!(HearingImpairedCaptionsSequence);
define_sequence_type!(ForcedNarrativeSequence);
define_sequence_type!(IABSequence);
define_sequence_type!(ISXDSequence);
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ResourceList {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Resource", default))]
#[cfg_attr(feature = "wasm", serde(rename = "resource", alias = "Resource", default))]
pub resources: Vec<Resource>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Resource {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
#[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
pub id: ImfUuid,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
#[cfg_attr(feature = "wasm", serde(rename = "annotation", alias = "Annotation", default))]
pub annotation: Option<LanguageString>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "EditRate", default, deserialize_with = "de_helpers::de_optional_edit_rate"))]
#[cfg_attr(feature = "wasm", serde(rename = "editRate", alias = "EditRate", default, deserialize_with = "de_helpers::de_optional_edit_rate"))]
pub edit_rate: Option<EditRate>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "IntrinsicDuration"))]
#[cfg_attr(feature = "wasm", serde(rename = "intrinsicDuration", alias = "IntrinsicDuration"))]
pub intrinsic_duration: u64,
#[cfg_attr(not(feature = "wasm"), serde(rename = "EntryPoint", default))]
#[cfg_attr(feature = "wasm", serde(rename = "entryPoint", alias = "EntryPoint", default))]
pub entry_point: Option<u64>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "SourceDuration", default))]
#[cfg_attr(feature = "wasm", serde(rename = "sourceDuration", alias = "SourceDuration", default))]
pub source_duration: Option<u64>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "SourceEncoding", default))]
#[cfg_attr(feature = "wasm", serde(rename = "sourceEncoding", alias = "SourceEncoding", default))]
pub source_encoding: Option<ImfUuid>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "TrackFileId", default))]
#[cfg_attr(feature = "wasm", serde(rename = "trackFileId", alias = "TrackFileId", default))]
pub track_file_id: Option<ImfUuid>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "RepeatCount", default))]
#[cfg_attr(feature = "wasm", serde(rename = "repeatCount", alias = "RepeatCount", default))]
pub repeat_count: Option<u64>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "KeyId", default))]
#[cfg_attr(feature = "wasm", serde(rename = "keyId", alias = "KeyId", default))]
pub key_id: Option<ImfUuid>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Hash", default))]
#[cfg_attr(feature = "wasm", serde(rename = "hash", alias = "Hash", default))]
pub hash: Option<String>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Marker", default))]
#[cfg_attr(feature = "wasm", serde(rename = "marker", alias = "Marker", default))]
pub markers: Vec<MarkerInfo>,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct MarkerInfo {
#[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
#[cfg_attr(feature = "wasm", serde(rename = "annotation", alias = "Annotation", default))]
pub annotation: Option<String>,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Label"))]
#[cfg_attr(feature = "wasm", serde(rename = "label", alias = "Label"))]
pub label: MarkerLabelElement,
#[cfg_attr(not(feature = "wasm"), serde(rename = "Offset"))]
#[cfg_attr(feature = "wasm", serde(rename = "offset", alias = "Offset"))]
pub offset: u64,
}
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct TrackInfo {
pub track_id: String,
pub track_type: String, pub codec: String,
pub language: Option<String>,
pub channels: Option<String>,
pub format_details: Option<String>,
pub resolution: Option<String>,
pub framerate: Option<String>,
pub bit_depth: Option<String>,
pub subtitle_type: Option<String>,
}
pub fn parse_cpl(xml_content: &str) -> Result<CompositionPlaylist, CplParseError> {
parse_cpl_with_options(xml_content, &CplParseOptions::default())
}
pub fn parse_cpl_with_options(
xml_content: &str,
options: &CplParseOptions<'_>,
) -> Result<CompositionPlaylist, CplParseError> {
let namespace = crate::assetmap::detect_root_namespace(xml_content)
.map(|uri| CplNamespace::from_uri(&uri))
.unwrap_or_default();
let has_signer = xml_content.contains("<Signer") || xml_content.contains(":Signer");
let has_signature = xml_content.contains("<Signature") || xml_content.contains(":Signature");
match options.signature_validation_mode {
SignatureValidationMode::Ignore => {}
SignatureValidationMode::RequirePresence => {
if !has_signature {
return Err(CplParseError::StrictSchema(
"Signature element is required by selected signature mode".to_string(),
));
}
}
SignatureValidationMode::VerifyIfPresent => {
if has_signature {
let verifier = options
.signature_verifier
.ok_or(CplParseError::SignatureVerifierRequired)?;
verifier
.verify(xml_content)
.map_err(CplParseError::SignatureVerificationFailed)?;
}
}
SignatureValidationMode::RequireValid => {
if !has_signature {
return Err(CplParseError::StrictSchema(
"Signature element is required by selected signature mode".to_string(),
));
}
let verifier = options
.signature_verifier
.ok_or(CplParseError::SignatureVerifierRequired)?;
verifier
.verify(xml_content)
.map_err(CplParseError::SignatureVerificationFailed)?;
}
}
let stripped = strip_xml_namespaces(xml_content);
if options.unknown_field_mode == UnknownFieldMode::Error {
let unknown = collect_unknown_xml_tokens(&stripped).map_err(|e| {
CplParseError::StrictUnknownXml(format!("unknown token scan failed: {}", e))
})?;
if !unknown.is_empty() {
let list = unknown.into_iter().collect::<Vec<_>>().join(", ");
return Err(CplParseError::StrictUnknownXml(list));
}
}
let mut cpl: CompositionPlaylist = quick_xml::de::from_str(&stripped)?;
if options.schema_strict_mode == SchemaStrictMode::Basic {
validate_basic_schema_constraints(&cpl)?;
}
cpl.namespace = namespace;
cpl.has_signer = has_signer;
cpl.has_signature = has_signature;
Ok(cpl)
}
fn validate_basic_schema_constraints(cpl: &CompositionPlaylist) -> Result<(), CplParseError> {
if cpl.segment_list.segments.is_empty() {
return Err(CplParseError::StrictSchema(
"SegmentList must contain at least one Segment".to_string(),
));
}
for (segment_index, segment) in cpl.segment_list.segments.iter().enumerate() {
let sequence_count = segment.sequence_list.marker_sequences.len()
+ segment.sequence_list.main_image_sequences.len()
+ segment.sequence_list.main_audio_sequences.len()
+ segment.sequence_list.subtitles_sequences.len()
+ segment.sequence_list.hearing_impaired_captions_sequences.len()
+ segment.sequence_list.forced_narrative_sequences.len()
+ segment.sequence_list.iab_sequences.len()
+ segment.sequence_list.isxd_sequences.len();
if sequence_count == 0 {
return Err(CplParseError::StrictSchema(format!(
"Segment[{}] must contain at least one sequence",
segment_index
)));
}
}
Ok(())
}
fn collect_unknown_xml_tokens(xml: &str) -> Result<BTreeSet<String>, String> {
let mut reader = quick_xml::Reader::from_str(xml);
reader.trim_text(true);
let allowed_elements: BTreeSet<&'static str> = [
"CompositionPlaylist", "Id", "Annotation", "IssueDate", "Issuer", "Creator", "ContentOriginator",
"ContentTitle", "ContentKind", "ContentVersionList", "ContentVersion", "LabelText",
"EssenceDescriptorList", "EssenceDescriptor", "EditRate", "TotalRunningTime", "LocaleList", "Locale", "LanguageList",
"Language", "RegionList", "Region", "ContentMaturityRatingList", "ContentMaturityRating", "Agency",
"Rating", "Audience", "ExtensionProperties", "ApplicationIdentification", "MaxCLL", "MaxFALL",
"CompositionTimecode", "TimecodeDropFrame", "TimecodeRate", "TimecodeStartAddress", "SegmentList",
"Segment", "SequenceList", "MarkerSequence", "MainImageSequence", "MainAudioSequence", "SubtitlesSequence",
"MainSubtitleSequence", "HearingImpairedCaptionsSequence", "ForcedNarrativeSequence", "IABSequence",
"ISXDSequence", "TrackId", "ResourceList", "Resource", "IntrinsicDuration", "EntryPoint",
"SourceDuration", "SourceEncoding", "TrackFileId", "RepeatCount", "KeyId", "Hash", "Marker",
"Label", "Offset", "RGBADescriptor", "CDCIDescriptor", "WAVEPCMDescriptor", "DCTimedTextDescriptor",
"IABEssenceDescriptor", "ISXDDataEssenceDescriptor", "InstanceID", "InstanceUID", "DisplayWidth",
"DisplayHeight", "StoredWidth", "StoredHeight", "SampleRate", "ImageAspectRatio", "ColorPrimaries",
"TransferCharacteristic", "CodingEquations", "PictureCompression", "FrameLayout", "DisplayF2Offset",
"ComponentMaxRef", "ComponentMinRef", "ScanningDirection", "StoredF2Offset", "SampledWidth",
"SampledHeight", "SampledXOffset", "SampledYOffset", "AlphaTransparency", "ImageAlignmentOffset",
"ImageStartOffset", "ImageEndOffset", "FieldDominance", "AlphaMaxRef", "AlphaMinRef", "Palette",
"PaletteLayout", "LinkedTrackID", "SubDescriptors", "ActiveWidth", "ActiveHeight", "ComponentDepth",
"HorizontalSubsampling", "VerticalSubsampling", "ColorSiting", "BlackRefLevel", "WhiteRefLevel",
"ColorRange", "ReversedByteOrder", "PaddingBits", "AlphaSampleDepth", "PHDRMetadataTrackSubDescriptor",
"JPEG2000SubDescriptor", "Rsiz", "Xsiz", "Ysiz", "XOsiz", "YOsiz", "XTsiz", "YTsiz", "XTOsiz",
"YTOsiz", "Csiz", "CodingStyleDefault", "QuantizationDefault", "J2CLayout", "J2KExtendedCapabilities",
"PictureComponentSizing", "RGBAComponent", "Code", "ComponentSize", "Pcap", "J2KComponentSizing", "Ssiz",
"XRSiz", "YRSiz", "PHDRMetadataTrackSubDescriptor_DataDefinition",
"PHDRMetadataTrackSubDescriptor_SimplePayloadSID", "PHDRMetadataTrackSubDescriptor_SourceTrackID",
"AudioSampleRate", "ChannelCount", "QuantizationBits", "SoundfieldGroupLabelSubDescriptor",
"MCATagSymbol", "MCATagName", "MCAAudioContentKind", "RFC5646SpokenLanguage", "RFC5646AudioLanguageCode",
"RFC5646LanguageTagList", "NamespaceURI", "SoundCompression", "IABSoundfieldLabelSubDescriptor",
"ContainerFormat", "Codec", "ElectrospatialFormulation", "MCALabelDictionaryID",
"EssenceLength", "Locked", "MCALinkID", "MCAChannelID", "AudioChannelLabelSubDescriptor",
"MCATitle", "MCATitleVersion", "MCAAudioElementKind", "SoundfieldGroupLinkID",
"DataEssenceCoding", "ContainerConstraintsSubDescriptor", "Signer", "Signature",
]
.into_iter()
.collect();
let allowed_attributes: BTreeSet<&'static str> = ["xmlns", "scope", "language"].into_iter().collect();
let mut unknown = BTreeSet::new();
loop {
match reader.read_event() {
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
let name = std::str::from_utf8(e.name().as_ref())
.map_err(|e| e.to_string())?
.to_string();
if !allowed_elements.contains(name.as_str()) {
unknown.insert(format!("element:{}", name));
}
for attr in e.attributes() {
let attr = attr.map_err(|e| e.to_string())?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| e.to_string())?
.to_string();
if !(allowed_attributes.contains(key.as_str()) || key.starts_with("xmlns:")) {
unknown.insert(format!("attribute:{}@{}", key, name));
}
}
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => return Err(e.to_string()),
}
}
Ok(unknown)
}
pub fn extract_cpl_languages(cpl: &CompositionPlaylist) -> Vec<LanguageTag> {
let mut languages: Vec<LanguageTag> = Vec::new();
let add_lang = |languages: &mut Vec<LanguageTag>, lang_opt: &Option<LanguageTag>| {
if let Some(lang) = lang_opt {
if !lang.0.is_empty() && !languages.contains(lang) {
languages.push(lang.clone());
}
}
};
let add_lang_string = |languages: &mut Vec<LanguageTag>, lang_string: &Option<LanguageString>| {
if let Some(ls) = lang_string {
add_lang(languages, &ls.language);
}
};
let add_required_lang_string = |languages: &mut Vec<LanguageTag>, lang_string: &LanguageString| {
add_lang(languages, &lang_string.language);
};
add_lang_string(&mut languages, &cpl.annotation);
add_lang_string(&mut languages, &cpl.issuer);
add_lang_string(&mut languages, &cpl.creator);
add_lang_string(&mut languages, &cpl.content_originator);
add_required_lang_string(&mut languages, &cpl.content_title);
if let Some(content_version_list) = &cpl.content_version_list {
for version in &content_version_list.content_versions {
add_lang_string(&mut languages, &version.label_text);
}
}
if let Some(locale_list) = &cpl.locale_list {
for locale in &locale_list.locales {
if let Some(language_list) = &locale.language_list {
for lang in &language_list.languages {
if !lang.0.is_empty() && !languages.contains(lang) {
languages.push(lang.clone());
}
}
}
}
}
if let Some(edl) = &cpl.essence_descriptor_list {
for ed in &edl.essence_descriptors {
if let Some(wave) = &ed.wave_pcm_descriptor {
if let Some(subs) = &wave.sub_descriptors {
if let Some(sf) = &subs.soundfield_group_label_sub_descriptor {
add_lang(&mut languages, &sf.rfc5646_spoken_language);
}
}
}
if let Some(iab) = &ed.iab_essence_descriptor {
if let Some(subs) = &iab.sub_descriptors {
if let Some(sf) = &subs.iab_soundfield_label_sub_descriptor {
add_lang(&mut languages, &sf.rfc5646_spoken_language);
}
}
}
if let Some(tt) = &ed.dc_timed_text_descriptor {
for lang in &tt.rfc5646_language_tag_list {
if !lang.0.is_empty() && !languages.contains(lang) {
languages.push(lang.clone());
}
}
}
}
}
languages.sort_by(|a, b| a.0.cmp(&b.0));
languages.dedup();
languages
}
pub fn extract_cpl_track_codecs_from_xml(xml_content: &str) -> Vec<TrackInfo> {
if let Ok(cpl) = parse_cpl(xml_content) {
return extract_tracks_from_cpl(&cpl, xml_content);
}
Vec::new()
}
fn extract_tracks_from_cpl(cpl: &CompositionPlaylist, _raw_xml: &str) -> Vec<TrackInfo> {
let mut tracks = Vec::new();
let descriptors: std::collections::HashMap<ImfUuid, &EssenceDescriptor> =
if let Some(edl) = &cpl.essence_descriptor_list {
edl.essence_descriptors.iter().map(|ed| (ed.id, ed)).collect()
} else {
std::collections::HashMap::new()
};
for segment in &cpl.segment_list.segments {
let seq_list = &segment.sequence_list;
for seq in &seq_list.main_image_sequences {
for resource in &seq.resource_list.resources {
if let Some(source_encoding) = &resource.source_encoding {
if let Some(ed) = descriptors.get(source_encoding) {
let (codec, resolution, bit_depth) = extract_video_info_from_descriptor(ed);
let framerate = resource.edit_rate.as_ref()
.or(cpl.edit_rate.as_ref())
.map(format_framerate);
tracks.push(TrackInfo {
track_id: seq.track_id.to_string(),
track_type: "video".to_string(),
codec,
language: None,
channels: None,
format_details: None,
resolution,
framerate,
bit_depth,
subtitle_type: None,
});
}
}
}
}
for seq in &seq_list.main_audio_sequences {
for resource in &seq.resource_list.resources {
if let Some(source_encoding) = &resource.source_encoding {
if let Some(ed) = descriptors.get(source_encoding) {
let (codec, channels, format_details, language) = extract_audio_info_from_descriptor(ed);
tracks.push(TrackInfo {
track_id: seq.track_id.to_string(),
track_type: "audio".to_string(),
codec,
language,
channels,
format_details,
resolution: None,
framerate: None,
bit_depth: None,
subtitle_type: None,
});
}
}
}
}
for seq in &seq_list.iab_sequences {
for resource in &seq.resource_list.resources {
if let Some(source_encoding) = &resource.source_encoding {
if let Some(ed) = descriptors.get(source_encoding) {
let language = ed.iab_essence_descriptor.as_ref()
.and_then(|iab| iab.sub_descriptors.as_ref())
.and_then(|sd| sd.iab_soundfield_label_sub_descriptor.as_ref())
.and_then(|sf| sf.rfc5646_spoken_language.as_ref())
.map(|lt| lt.0.clone());
tracks.push(TrackInfo {
track_id: seq.track_id.to_string(),
track_type: "audio".to_string(),
codec: "IAB (Dolby Atmos)".to_string(),
language,
channels: Some("Object-based".to_string()),
format_details: Some("Immersive Audio".to_string()),
resolution: None,
framerate: None,
bit_depth: None,
subtitle_type: None,
});
}
}
}
}
let subtitle_sequences: Vec<(&str, &[SubtitlesSequence])> = vec![
];
let _ = subtitle_sequences;
for seq in &seq_list.subtitles_sequences {
if let Some(track) = extract_timed_text_track(seq.track_id, "standard", &seq.resource_list, &descriptors) {
tracks.push(track);
}
}
for seq in &seq_list.hearing_impaired_captions_sequences {
if let Some(track) = extract_timed_text_track(seq.track_id, "hi", &seq.resource_list, &descriptors) {
tracks.push(track);
}
}
for seq in &seq_list.forced_narrative_sequences {
if let Some(track) = extract_timed_text_track(seq.track_id, "forced", &seq.resource_list, &descriptors) {
tracks.push(track);
}
}
}
tracks
}
fn extract_video_info_from_descriptor(ed: &EssenceDescriptor) -> (String, Option<String>, Option<String>) {
if let Some(rgba) = &ed.rgba_descriptor {
let width = rgba.display_width.or(rgba.stored_width);
let height = rgba.display_height.or(rgba.stored_height);
let resolution = match (width, height) {
(Some(w), Some(h)) => Some(format!("{}x{}", w, h)),
_ => None,
};
let codec = rgba.picture_compression.as_ref()
.map(|c| c.to_string())
.unwrap_or_else(|| "JPEG 2000".to_string());
return (codec, resolution, None);
}
if let Some(cdci) = &ed.cdci_descriptor {
let width = cdci.active_width.or(cdci.display_width).or(cdci.stored_width);
let height = cdci.active_height.or(cdci.display_height).or(cdci.stored_height);
let resolution = match (width, height) {
(Some(w), Some(h)) => Some(format!("{}x{}", w, h)),
_ => None,
};
let codec = cdci.picture_compression.as_ref()
.map(|c| c.to_string())
.unwrap_or_else(|| "JPEG 2000".to_string());
let bit_depth = cdci.component_depth.map(|d| format!("{}-bit", d));
return (codec, resolution, bit_depth);
}
("Unknown".to_string(), None, None)
}
fn extract_audio_info_from_descriptor(ed: &EssenceDescriptor) -> (String, Option<String>, Option<String>, Option<String>) {
if let Some(wave) = &ed.wave_pcm_descriptor {
let codec = wave.quantization_bits.map(|b| format!("PCM {}-bit", b))
.unwrap_or_else(|| "PCM".to_string());
let (channels, format_details) = match wave.channel_count {
Some(1) => (Some("1.0".to_string()), Some("Mono".to_string())),
Some(2) => (Some("2.0".to_string()), Some("Stereo".to_string())),
Some(6) => (Some("5.1".to_string()), Some("Surround".to_string())),
Some(8) => (Some("7.1".to_string()), Some("Surround".to_string())),
Some(n) => (Some(format!("{}.0", n)), Some(format!("{} Channel", n))),
None => (None, None),
};
let language = wave.sub_descriptors.as_ref()
.and_then(|sd| sd.soundfield_group_label_sub_descriptor.as_ref())
.and_then(|sf| sf.rfc5646_spoken_language.as_ref())
.map(|lt| lt.0.clone());
return (codec, channels, format_details, language);
}
("Unknown".to_string(), None, None, None)
}
fn extract_timed_text_track(
track_id: ImfUuid,
subtitle_type: &str,
resource_list: &ResourceList,
descriptors: &std::collections::HashMap<ImfUuid, &EssenceDescriptor>,
) -> Option<TrackInfo> {
for resource in &resource_list.resources {
if let Some(source_encoding) = &resource.source_encoding {
if let Some(ed) = descriptors.get(source_encoding) {
let language = ed.dc_timed_text_descriptor.as_ref()
.map(|tt| {
tt.rfc5646_language_tag_list.iter()
.map(|lt| lt.as_str())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(",")
})
.filter(|s| !s.is_empty());
return Some(TrackInfo {
track_id: track_id.to_string(),
track_type: "subtitle".to_string(),
codec: "IMSC1 (Timed Text)".to_string(),
language,
channels: None,
format_details: None,
resolution: None,
framerate: None,
bit_depth: None,
subtitle_type: Some(subtitle_type.to_string()),
});
}
}
}
None
}
pub fn format_framerate(edit_rate: &EditRate) -> String {
let fps = edit_rate.as_f64();
if (fps - 23.976).abs() < 0.01 {
"23.976".to_string()
} else if (fps - 29.97).abs() < 0.01 {
"29.97".to_string()
} else if (fps - 59.94).abs() < 0.01 {
"59.94".to_string()
} else if fps == fps.round() {
format!("{}", fps as u32)
} else {
format!("{:.3}", fps)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct AcceptAllSignatureVerifier;
impl XmlSignatureVerifier for AcceptAllSignatureVerifier {
fn verify(&self, _xml_content: &str) -> Result<(), String> {
Ok(())
}
}
struct RejectingSignatureVerifier;
impl XmlSignatureVerifier for RejectingSignatureVerifier {
fn verify(&self, _xml_content: &str) -> Result<(), String> {
Err("bad signature".to_string())
}
}
#[test]
fn strict_production_options_enable_all_strict_checks() {
let verifier = ReferenceDigestXmlDsigVerifier;
let options = strict_production_parse_options(&verifier);
assert_eq!(options.unknown_field_mode, UnknownFieldMode::Error);
assert_eq!(options.schema_strict_mode, SchemaStrictMode::Basic);
assert_eq!(
options.signature_validation_mode,
SignatureValidationMode::RequireValid
);
assert!(options.signature_verifier.is_some());
}
#[test]
fn recommended_signature_verifier_rejects_unsigned_xml() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
let verifier = recommended_signature_verifier();
assert!(verifier.verify(&xml).is_err());
}
#[test]
fn test_strip_xml_namespaces() {
let input = r#"<r0:RGBADescriptor xmlns:r0="http://example.com"><r1:DisplayWidth>3840</r1:DisplayWidth></r0:RGBADescriptor>"#;
let result = strip_xml_namespaces(input);
assert!(result.contains("<RGBADescriptor"));
assert!(result.contains("<DisplayWidth>3840</DisplayWidth>"));
assert!(result.contains("</RGBADescriptor>"));
assert!(!result.contains("xmlns:r0"));
}
#[test]
fn test_strip_preserves_content_with_colons() {
let input = r#"<PictureCompression>urn:smpte:ul:060e2b34</PictureCompression>"#;
let result = strip_xml_namespaces(input);
assert_eq!(result, input); }
#[test]
fn test_parse_simple_cpl() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<Annotation>Test CPL</Annotation>
<IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
<ContentTitle>Test Content</ContentTitle>
<ContentKind>Test</ContentKind>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList>
</SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#;
let result = parse_cpl(xml);
match result {
Ok(cpl) => {
assert_eq!(cpl.id, ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap());
assert_eq!(cpl.content_title.text, "Test Content");
assert_eq!(cpl.content_kind, ContentKind::Test);
assert!(!cpl.segment_list.segments.is_empty());
}
Err(e) => panic!("Failed to parse CPL: {:?}", e),
}
}
#[test]
fn test_content_kind_scope_attribute() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Test</ContentTitle>
<ContentKind scope="http://www.smpte-ra.org/schemas/2067-3/2013#content-kind">feature</ContentKind>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList>
</SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#;
let cpl = parse_cpl(xml).expect("Failed to parse CPL with ContentKind scope");
assert_eq!(cpl.content_kind.kind, ContentKind::Feature);
assert_eq!(
cpl.content_kind.scope.as_deref(),
Some("http://www.smpte-ra.org/schemas/2067-3/2013#content-kind")
);
assert_eq!(
cpl.content_kind.effective_scope(),
"http://www.smpte-ra.org/schemas/2067-3/2013#content-kind"
);
}
#[test]
fn test_content_kind_custom_scope() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Test</ContentTitle>
<ContentKind scope="http://example.com/custom-kinds">my-custom-kind</ContentKind>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList>
</SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#;
let cpl = parse_cpl(xml).expect("Failed to parse CPL with custom scope");
assert_eq!(cpl.content_kind.kind, ContentKind::Other("my-custom-kind".to_string()));
assert_eq!(
cpl.content_kind.scope.as_deref(),
Some("http://example.com/custom-kinds")
);
}
#[test]
fn test_content_kind_no_scope_uses_default() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Test</ContentTitle>
<ContentKind>Test</ContentKind>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList>
</SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#;
let cpl = parse_cpl(xml).expect("Failed to parse CPL without scope");
assert_eq!(cpl.content_kind.kind, ContentKind::Test);
assert!(cpl.content_kind.scope.is_none());
assert_eq!(
cpl.content_kind.effective_scope(),
CONTENT_KIND_DEFAULT_SCOPE
);
}
#[test]
fn test_malformed_xml_handling() {
let malformed_xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:test</Id>
<ContentTitle>Broken XML"#;
let result: Result<CompositionPlaylist, CplParseError> = parse_cpl(malformed_xml);
assert!(result.is_err(), "Should fail with malformed XML");
}
fn minimal_cpl_with_ns(ns: &str) -> String {
format!(r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="{ns}">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>NS Test</ContentTitle>
<ContentKind>Test</ContentKind>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList></SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#)
}
#[test]
fn cpl_parses_with_2067_3_2013_namespace() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
let cpl = parse_cpl(&xml).expect("2013 namespace should parse");
assert_eq!(cpl.content_title.text, "NS Test");
assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2013);
assert_eq!(cpl.namespace.spec_id(), "ST 2067-3:2013");
assert_eq!(cpl.namespace.year(), Some(2013));
}
#[test]
fn cpl_parses_with_2067_3_2016_namespace() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2016");
let cpl = parse_cpl(&xml).expect("2016 namespace should parse");
assert_eq!(cpl.content_title.text, "NS Test");
assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2016);
assert_eq!(cpl.namespace.year(), Some(2016));
}
#[test]
fn cpl_parses_with_2067_3_2020_namespace() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/ns/2067-3/2020");
let cpl = parse_cpl(&xml).expect("2020 namespace should parse");
assert_eq!(cpl.content_title.text, "NS Test");
assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2020);
assert_eq!(cpl.namespace.year(), Some(2020));
}
#[test]
fn cpl_parses_with_dci_429_7_namespace() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/429-7/2006/CPL");
let cpl = parse_cpl(&xml).expect("DCI 429-7 namespace should parse");
assert_eq!(cpl.content_title.text, "NS Test");
assert_eq!(cpl.namespace, CplNamespace::Dci429_7);
assert_eq!(cpl.namespace.year(), Some(2006));
}
#[test]
fn cpl_meridian_detects_2013_namespace() {
let xml = include_str!("../../../../test-data/MERIDIAN_Netflix_Photon_161006/CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml");
let cpl = parse_cpl(xml).expect("MERIDIAN CPL should parse");
assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2013);
}
#[test]
fn strict_unknown_mode_rejects_unknown_elements() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Strict Unknown</ContentTitle>
<ContentKind>Test</ContentKind>
<UnknownElement>oops</UnknownElement>
<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
</CompositionPlaylist>"#;
let options = CplParseOptions {
unknown_field_mode: UnknownFieldMode::Error,
..Default::default()
};
let result = parse_cpl_with_options(xml, &options);
assert!(matches!(result, Err(CplParseError::StrictUnknownXml(_))));
}
#[test]
fn strict_schema_mode_rejects_empty_sequence_list_per_segment() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Strict Schema</ContentTitle>
<ContentKind>Test</ContentKind>
<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
</CompositionPlaylist>"#;
let options = CplParseOptions {
schema_strict_mode: SchemaStrictMode::Basic,
..Default::default()
};
let result = parse_cpl_with_options(xml, &options);
assert!(matches!(result, Err(CplParseError::StrictSchema(_))));
}
#[test]
fn signature_mode_require_presence_rejects_unsigned_cpl() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
let options = CplParseOptions {
signature_validation_mode: SignatureValidationMode::RequirePresence,
..Default::default()
};
let result = parse_cpl_with_options(&xml, &options);
assert!(matches!(result, Err(CplParseError::StrictSchema(_))));
}
#[test]
fn signature_mode_require_valid_needs_verifier() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Signed</ContentTitle>
<ContentKind>Test</ContentKind>
<Signature>dummy</Signature>
<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
</CompositionPlaylist>"#;
let options = CplParseOptions {
signature_validation_mode: SignatureValidationMode::RequireValid,
..Default::default()
};
let result = parse_cpl_with_options(xml, &options);
assert!(matches!(result, Err(CplParseError::SignatureVerifierRequired)));
}
#[test]
fn signature_mode_require_valid_uses_verifier() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Signed</ContentTitle>
<ContentKind>Test</ContentKind>
<Signature>dummy</Signature>
<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList><MainImageSequence><Id>urn:uuid:11111111-1111-1111-1111-111111111111</Id><TrackId>urn:uuid:22222222-2222-2222-2222-222222222222</TrackId><ResourceList><Resource><Id>urn:uuid:33333333-3333-3333-3333-333333333333</Id><IntrinsicDuration>1</IntrinsicDuration></Resource></ResourceList></MainImageSequence></SequenceList></Segment></SegmentList>
</CompositionPlaylist>"#;
let verifier = AcceptAllSignatureVerifier;
let options = CplParseOptions {
signature_validation_mode: SignatureValidationMode::RequireValid,
signature_verifier: Some(&verifier),
..Default::default()
};
let result = parse_cpl_with_options(xml, &options);
assert!(result.is_ok(), "signature verifier should allow parse: {result:?}");
}
#[test]
fn signature_mode_require_valid_surfaces_verification_failure() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Signed</ContentTitle>
<ContentKind>Test</ContentKind>
<Signature>dummy</Signature>
<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
</CompositionPlaylist>"#;
let verifier = RejectingSignatureVerifier;
let options = CplParseOptions {
signature_validation_mode: SignatureValidationMode::RequireValid,
signature_verifier: Some(&verifier),
..Default::default()
};
let result = parse_cpl_with_options(xml, &options);
assert!(matches!(result, Err(CplParseError::SignatureVerificationFailed(_))));
}
fn build_signed_cpl_with_reference_digest(tamper_digest: bool) -> String {
let unsigned_xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Signed Digest CPL</ContentTitle>
<ContentKind>Test</ContentKind>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList></SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#;
let normalized = normalize_xml_for_digest(unsigned_xml);
let digest = compute_hash(HashAlgorithm::Sha256, normalized.as_bytes());
let mut digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
if tamper_digest {
digest_b64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string();
}
format!(
r#"<?xml version="1.0" encoding="UTF-8" ?>
<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<ContentTitle>Signed Digest CPL</ContentTitle>
<ContentKind>Test</ContentKind>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>{}</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>AQ==</SignatureValue>
</Signature>
<SegmentList>
<Segment>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<SequenceList></SequenceList>
</Segment>
</SegmentList>
</CompositionPlaylist>"#,
digest_b64
)
}
#[test]
fn reference_digest_verifier_accepts_valid_uri_empty_digest() {
let xml = build_signed_cpl_with_reference_digest(false);
let verifier = ReferenceDigestXmlDsigVerifier;
assert!(verifier.verify(&xml).is_ok());
}
#[test]
fn reference_digest_verifier_rejects_mismatched_digest() {
let xml = build_signed_cpl_with_reference_digest(true);
let verifier = ReferenceDigestXmlDsigVerifier;
let result = verifier.verify(&xml);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.contains("DigestValue mismatch"), "unexpected error: {error}");
}
#[test]
fn signature_mode_require_valid_with_reference_digest_verifier() {
let xml = build_signed_cpl_with_reference_digest(false);
let verifier = ReferenceDigestXmlDsigVerifier;
let options = CplParseOptions {
signature_validation_mode: SignatureValidationMode::RequireValid,
signature_verifier: Some(&verifier),
..Default::default()
};
let result = parse_cpl_with_options(&xml, &options);
assert!(result.is_ok(), "expected valid signature digest path to parse: {result:?}");
}
#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
#[test]
fn xmlsec_verifier_surfaces_missing_binary() {
let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
let verifier = XmlSec1Verifier::new().with_binary_path("xmlsec1-definitely-not-installed");
let error = verifier.verify(&xml).expect_err("expected missing binary error");
assert!(
error.contains("failed to execute"),
"unexpected error message: {error}"
);
}
#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
#[test]
fn xmlsec_crate_verifier_rejects_invalid_key_material() {
let xml = build_signed_cpl_with_reference_digest(false);
let verifier = XmlSecCrateVerifier::from_pem("not-a-valid-key");
let error = verifier.verify(&xml).expect_err("expected xmlsec key load error");
assert!(
error.contains("xmlsec key load failed") || error.contains("xmlsec verify failed"),
"unexpected error message: {error}"
);
}
}