1pub mod types;
11pub use types::{
12 CodingEquations, ColorPrimaries, ContentKind, CplNamespace, EditRate, LanguageTag, MarkerLabel,
13 McaTagSymbol, Resolution, TransferCharacteristic, VideoCodec,
14};
15
16pub mod validate;
17pub use validate::validate_cpl as validate_cpl_constraints;
18
19pub mod codes;
20
21use crate::assetmap::{HashAlgorithm, ImfUuid};
22use base64::Engine;
23use quick_xml::events::Event;
24use serde::{Deserialize, Serialize};
25use std::collections::BTreeSet;
26use thiserror::Error;
27
28#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
29use libxml::parser::Parser as XmlParser;
30#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
31use std::io::Write;
32#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
33use std::process::Command;
34#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
35use xmlsec::{XmlSecKey, XmlSecKeyFormat, XmlSecSignatureContext};
36
37#[cfg(feature = "typescript")]
38use ts_rs::TS;
39
40#[cfg(feature = "wasm")]
41use tsify::Tsify;
42
43#[derive(Debug, Error)]
48pub enum CplParseError {
49 #[error("XML parse error: {0}")]
50 Xml(#[from] quick_xml::DeError),
51
52 #[error("strict unknown XML token(s): {0}")]
53 StrictUnknownXml(String),
54
55 #[error("strict schema violation: {0}")]
56 StrictSchema(String),
57
58 #[error("XMLDSIG verifier is required for selected signature mode")]
59 SignatureVerifierRequired,
60
61 #[error("XMLDSIG verification failed: {0}")]
62 SignatureVerificationFailed(String),
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum UnknownFieldMode {
67 Ignore,
68 Error,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum SchemaStrictMode {
73 Off,
74 Basic,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum SignatureValidationMode {
79 Ignore,
80 RequirePresence,
81 VerifyIfPresent,
82 RequireValid,
83}
84
85pub trait XmlSignatureVerifier {
86 fn verify(&self, xml_content: &str) -> Result<(), String>;
87}
88
89#[derive(Debug, Default, Clone, Copy)]
99pub struct ReferenceDigestXmlDsigVerifier;
100
101impl XmlSignatureVerifier for ReferenceDigestXmlDsigVerifier {
102 fn verify(&self, xml_content: &str) -> Result<(), String> {
103 validate_signature_structure(xml_content)?;
104 validate_reference_digests(xml_content)
105 }
106}
107
108#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
113#[derive(Debug, Clone)]
114pub struct XmlSecCrateVerifier {
115 key_data: Vec<u8>,
116 key_format: XmlSecKeyFormat,
117 key_password: Option<String>,
118}
119
120#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
121impl XmlSecCrateVerifier {
122 pub fn from_key_data(key_data: Vec<u8>, key_format: XmlSecKeyFormat) -> Self {
123 Self {
124 key_data,
125 key_format,
126 key_password: None,
127 }
128 }
129
130 pub fn from_pem(key_data: impl AsRef<[u8]>) -> Self {
131 Self::from_key_data(key_data.as_ref().to_vec(), XmlSecKeyFormat::Pem)
132 }
133
134 pub fn with_password(mut self, password: impl Into<String>) -> Self {
135 self.key_password = Some(password.into());
136 self
137 }
138}
139
140#[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
141impl XmlSignatureVerifier for XmlSecCrateVerifier {
142 fn verify(&self, xml_content: &str) -> Result<(), String> {
143 validate_signature_structure(xml_content)?;
144
145 let mut tmp = tempfile::NamedTempFile::new()
146 .map_err(|e| format!("failed to create temp xml file: {}", e))?;
147 tmp.write_all(xml_content.as_bytes())
148 .map_err(|e| format!("failed to write temp xml file: {}", e))?;
149 tmp.flush()
150 .map_err(|e| format!("failed to flush temp xml file: {}", e))?;
151
152 let doc = XmlParser::default()
153 .parse_file(tmp.path().to_string_lossy().as_ref())
154 .map_err(|e| format!("xml parse failed for xmlsec verifier: {}", e))?;
155
156 let key = XmlSecKey::from_memory(
157 &self.key_data,
158 self.key_format,
159 self.key_password.as_deref(),
160 )
161 .map_err(|e| format!("xmlsec key load failed: {}", e))?;
162
163 let mut ctx = XmlSecSignatureContext::new();
164 ctx.insert_key(key);
165
166 let valid = ctx
167 .verify_document(&doc)
168 .map_err(|e| format!("xmlsec verify failed: {}", e))?;
169
170 if valid {
171 Ok(())
172 } else {
173 Err("xmlsec signature verification returned invalid".to_string())
174 }
175 }
176}
177
178#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
185#[derive(Debug, Clone)]
186pub struct XmlSec1Verifier {
187 binary_path: String,
188 extra_args: Vec<String>,
189}
190
191#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
192impl Default for XmlSec1Verifier {
193 fn default() -> Self {
194 let binary_path =
195 std::env::var("IMF_XMLSEC1_BIN").unwrap_or_else(|_| "xmlsec1".to_string());
196 Self {
197 binary_path,
198 extra_args: Vec::new(),
199 }
200 }
201}
202
203#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
204impl XmlSec1Verifier {
205 pub fn new() -> Self {
206 Self::default()
207 }
208
209 pub fn with_binary_path(mut self, binary_path: impl Into<String>) -> Self {
210 self.binary_path = binary_path.into();
211 self
212 }
213
214 pub fn with_extra_args<I, S>(mut self, args: I) -> Self
215 where
216 I: IntoIterator<Item = S>,
217 S: Into<String>,
218 {
219 self.extra_args = args.into_iter().map(Into::into).collect();
220 self
221 }
222}
223
224#[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
225impl XmlSignatureVerifier for XmlSec1Verifier {
226 fn verify(&self, xml_content: &str) -> Result<(), String> {
227 let mut tmp = tempfile::NamedTempFile::new()
228 .map_err(|e| format!("failed to create temp xml file: {}", e))?;
229 tmp.write_all(xml_content.as_bytes())
230 .map_err(|e| format!("failed to write temp xml file: {}", e))?;
231 tmp.flush()
232 .map_err(|e| format!("failed to flush temp xml file: {}", e))?;
233
234 let mut command = Command::new(&self.binary_path);
235 command.arg("--verify");
236 for arg in &self.extra_args {
237 command.arg(arg);
238 }
239 command.arg(tmp.path());
240
241 let output = command
242 .output()
243 .map_err(|e| format!("failed to execute '{} --verify': {}", self.binary_path, e))?;
244
245 if output.status.success() {
246 return Ok(());
247 }
248
249 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
250 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
251 let message = if !stderr.is_empty() {
252 stderr
253 } else if !stdout.is_empty() {
254 stdout
255 } else {
256 format!("xmlsec1 exited with status {}", output.status)
257 };
258
259 Err(message)
260 }
261}
262
263pub struct CplParseOptions<'a> {
264 pub unknown_field_mode: UnknownFieldMode,
265 pub schema_strict_mode: SchemaStrictMode,
266 pub signature_validation_mode: SignatureValidationMode,
267 pub signature_verifier: Option<&'a dyn XmlSignatureVerifier>,
268}
269
270impl Default for CplParseOptions<'_> {
271 fn default() -> Self {
272 Self {
273 unknown_field_mode: UnknownFieldMode::Ignore,
274 schema_strict_mode: SchemaStrictMode::Off,
275 signature_validation_mode: SignatureValidationMode::Ignore,
276 signature_verifier: None,
277 }
278 }
279}
280
281pub fn strict_production_parse_options<'a>(
286 signature_verifier: &'a dyn XmlSignatureVerifier,
287) -> CplParseOptions<'a> {
288 CplParseOptions {
289 unknown_field_mode: UnknownFieldMode::Error,
290 schema_strict_mode: SchemaStrictMode::Basic,
291 signature_validation_mode: SignatureValidationMode::RequireValid,
292 signature_verifier: Some(signature_verifier),
293 }
294}
295
296pub fn recommended_signature_verifier() -> Box<dyn XmlSignatureVerifier> {
302 #[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
303 {
304 Box::new(XmlSec1Verifier::default())
305 }
306
307 #[cfg(not(all(feature = "xmlsec1", not(target_arch = "wasm32"))))]
308 {
309 Box::new(ReferenceDigestXmlDsigVerifier)
310 }
311}
312
313fn validate_signature_structure(xml_content: &str) -> Result<(), String> {
314 let signature_xml = extract_first_element(xml_content, "Signature")
315 .ok_or_else(|| "missing Signature element".to_string())?;
316
317 if extract_first_element(&signature_xml, "SignedInfo").is_none() {
318 return Err("missing SignedInfo element".to_string());
319 }
320
321 let signature_value_raw = extract_first_element_text(&signature_xml, "SignatureValue")
322 .ok_or_else(|| "missing SignatureValue element".to_string())?;
323 let signature_value = collapse_xml_text(&signature_value_raw);
324 if signature_value.is_empty() {
325 return Err("SignatureValue is empty".to_string());
326 }
327 let decoded = base64::engine::general_purpose::STANDARD
328 .decode(signature_value.as_bytes())
329 .map_err(|e| format!("invalid SignatureValue base64: {}", e))?;
330 if decoded.is_empty() {
331 return Err("SignatureValue decodes to zero bytes".to_string());
332 }
333
334 if let Some(signature_method_alg) = extract_signature_method_algorithm(&signature_xml) {
335 let is_supported = matches!(
336 signature_method_alg.as_str(),
337 "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
338 | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
339 | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
340 | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
341 );
342 if !is_supported {
343 return Err(format!(
344 "unsupported SignatureMethod algorithm: {}",
345 signature_method_alg
346 ));
347 }
348 }
349
350 Ok(())
351}
352
353fn validate_reference_digests(xml_content: &str) -> Result<(), String> {
354 let signature_xml = extract_first_element(xml_content, "Signature")
355 .ok_or_else(|| "missing Signature element".to_string())?;
356
357 let references = extract_reference_entries(&signature_xml)?;
358 if references.is_empty() {
359 return Err("SignedInfo contains no Reference elements".to_string());
360 }
361
362 for reference in references {
363 let digest_algorithm = HashAlgorithm::from_uri(&reference.digest_method_algorithm)
364 .ok_or_else(|| {
365 format!(
366 "unsupported DigestMethod algorithm: {}",
367 reference.digest_method_algorithm
368 )
369 })?;
370
371 let expected_digest = base64::engine::general_purpose::STANDARD
372 .decode(reference.digest_value.as_bytes())
373 .map_err(|e| format!("invalid DigestValue base64: {}", e))?;
374
375 if expected_digest.len() != digest_algorithm.digest_len() {
376 return Err(format!(
377 "DigestValue length {} does not match {} digest length {}",
378 expected_digest.len(),
379 digest_algorithm,
380 digest_algorithm.digest_len()
381 ));
382 }
383
384 match reference.uri.as_deref().unwrap_or("") {
385 "" => {
386 let unsigned_xml = strip_first_signature_element(xml_content)
387 .ok_or_else(|| "failed to remove Signature element for URI=\"\"".to_string())?;
388 let normalized = normalize_xml_for_digest(&unsigned_xml);
389 let actual_digest = compute_hash(digest_algorithm, normalized.as_bytes());
390 if actual_digest != expected_digest {
391 return Err(format!(
392 "DigestValue mismatch for Reference URI=\"\" (algorithm {})",
393 digest_algorithm
394 ));
395 }
396 }
397 uri if uri.starts_with('#') => {}
398 uri => {
399 return Err(format!(
400 "unsupported Reference URI '{}'; only empty or fragment URIs are supported",
401 uri
402 ));
403 }
404 }
405 }
406
407 Ok(())
408}
409
410fn compute_hash(algorithm: HashAlgorithm, bytes: &[u8]) -> Vec<u8> {
411 match algorithm {
412 HashAlgorithm::Sha1 => {
413 use sha1::Digest;
414 let mut hasher = sha1::Sha1::new();
415 hasher.update(bytes);
416 hasher.finalize().to_vec()
417 }
418 HashAlgorithm::Sha256 => {
419 use sha2::Digest;
420 let mut hasher = sha2::Sha256::new();
421 hasher.update(bytes);
422 hasher.finalize().to_vec()
423 }
424 }
425}
426
427#[derive(Debug, Clone)]
428struct SignatureReferenceEntry {
429 uri: Option<String>,
430 digest_method_algorithm: String,
431 digest_value: String,
432}
433
434fn extract_signature_method_algorithm(signature_xml: &str) -> Option<String> {
435 use std::sync::LazyLock;
436 static RE: LazyLock<regex::Regex> = LazyLock::new(|| {
437 regex::Regex::new(
438 r#"<(?:(?:\w+):)?SignatureMethod\b[^>]*\bAlgorithm\s*=\s*\"([^\"]+)\"[^>]*/?>"#,
439 )
440 .unwrap()
441 });
442 RE.captures(signature_xml)
443 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()))
444}
445
446fn extract_reference_entries(signature_xml: &str) -> Result<Vec<SignatureReferenceEntry>, String> {
447 use std::sync::LazyLock;
448 static REFERENCE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
449 regex::Regex::new(r#"(?s)<(?:(?:\w+):)?Reference\b([^>]*)>(.*?)</(?:(?:\w+):)?Reference>"#)
450 .unwrap()
451 });
452 static URI_RE: LazyLock<regex::Regex> =
453 LazyLock::new(|| regex::Regex::new(r#"\bURI\s*=\s*\"([^\"]*)\""#).unwrap());
454 static DIGEST_METHOD_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
455 regex::Regex::new(
456 r#"<(?:(?:\w+):)?DigestMethod\b[^>]*\bAlgorithm\s*=\s*\"([^\"]+)\"[^>]*/?>"#,
457 )
458 .unwrap()
459 });
460 static DIGEST_VALUE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
461 regex::Regex::new(
462 r#"(?s)<(?:(?:\w+):)?DigestValue\b[^>]*>(.*?)</(?:(?:\w+):)?DigestValue>"#,
463 )
464 .unwrap()
465 });
466
467 let mut out = Vec::new();
468 for captures in REFERENCE_RE.captures_iter(signature_xml) {
469 let attrs = captures
470 .get(1)
471 .map(|m| m.as_str())
472 .ok_or_else(|| "internal parse error while reading Reference attributes".to_string())?;
473 let inner = captures
474 .get(2)
475 .map(|m| m.as_str())
476 .ok_or_else(|| "internal parse error while reading Reference body".to_string())?;
477
478 let uri = URI_RE
479 .captures(attrs)
480 .and_then(|c| c.get(1).map(|m| m.as_str().to_string()));
481 let digest_method_algorithm = DIGEST_METHOD_RE
482 .captures(inner)
483 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()))
484 .ok_or_else(|| "Reference missing DigestMethod/@Algorithm".to_string())?;
485 let digest_value = DIGEST_VALUE_RE
486 .captures(inner)
487 .and_then(|c| c.get(1).map(|m| collapse_xml_text(m.as_str())))
488 .ok_or_else(|| "Reference missing DigestValue".to_string())?;
489
490 out.push(SignatureReferenceEntry {
491 uri,
492 digest_method_algorithm,
493 digest_value,
494 });
495 }
496
497 Ok(out)
498}
499
500fn strip_first_signature_element(xml: &str) -> Option<String> {
501 use std::sync::LazyLock;
502 static RE: LazyLock<regex::Regex> = LazyLock::new(|| {
503 regex::Regex::new(r#"(?s)<(?:(?:\w+):)?Signature\b[^>]*>.*?</(?:(?:\w+):)?Signature\s*>"#)
504 .unwrap()
505 });
506 let m = RE.find(xml)?;
507 let mut out = String::with_capacity(xml.len() - (m.end() - m.start()));
508 out.push_str(&xml[..m.start()]);
509 out.push_str(&xml[m.end()..]);
510 Some(out)
511}
512
513fn normalize_xml_for_digest(xml: &str) -> String {
514 use std::sync::LazyLock;
515 static DECL_RE: LazyLock<regex::Regex> =
516 LazyLock::new(|| regex::Regex::new(r#"(?s)^\s*<\?xml[^>]*\?>"#).unwrap());
517 static INTER_TAG_WS_RE: LazyLock<regex::Regex> =
518 LazyLock::new(|| regex::Regex::new(r#">\s+<"#).unwrap());
519
520 let no_decl = xml.strip_prefix("\u{FEFF}").unwrap_or(xml).trim();
521 let without_decl = DECL_RE.replace(no_decl, "").to_string();
522 INTER_TAG_WS_RE
523 .replace_all(without_decl.trim(), "><")
524 .to_string()
525}
526
527fn extract_first_element(xml: &str, local_name: &str) -> Option<String> {
528 let escaped = regex::escape(local_name);
529 let pattern = format!(
530 r#"(?s)<(?:(?:\w+):)?{name}\b[^>]*>.*?</(?:(?:\w+):)?{name}\s*>"#,
531 name = escaped
532 );
533 let re = regex::Regex::new(&pattern).expect("valid regex pattern");
535 re.find(xml).map(|m| m.as_str().to_string())
536}
537
538fn extract_first_element_text(xml: &str, local_name: &str) -> Option<String> {
539 let escaped = regex::escape(local_name);
540 let pattern = format!(
541 r#"(?s)<(?:(?:\w+):)?{name}\b[^>]*>(.*?)</(?:(?:\w+):)?{name}\s*>"#,
542 name = escaped
543 );
544 let re = regex::Regex::new(&pattern).expect("valid regex pattern");
546 re.captures(xml)
547 .and_then(|c| c.get(1).map(|m| m.as_str().to_string()))
548}
549
550fn collapse_xml_text(text: &str) -> String {
551 text.chars().filter(|c| !c.is_whitespace()).collect()
552}
553
554mod de_helpers {
559 use crate::cpl::{
560 CodingEquations, ColorPrimaries, EditRate, LanguageTag, McaTagSymbol,
561 TransferCharacteristic, VideoCodec,
562 };
563 use serde::{Deserialize, Deserializer};
564
565 pub fn de_optional_edit_rate<'de, D: Deserializer<'de>>(
566 d: D,
567 ) -> Result<Option<EditRate>, D::Error> {
568 let s = String::deserialize(d)?;
569 let trimmed = s.trim();
570 if trimmed.is_empty() {
571 Ok(None)
572 } else {
573 let normalized = trimmed.replace('/', " ");
575 EditRate::parse(&normalized)
576 .map(Some)
577 .map_err(serde::de::Error::custom)
578 }
579 }
580
581 fn de_optional_ul_type<'de, D, T, F>(d: D, from_ul: F) -> Result<Option<T>, D::Error>
583 where
584 D: Deserializer<'de>,
585 F: FnOnce(&str) -> T,
586 {
587 let s = String::deserialize(d)?;
588 Ok(if s.trim().is_empty() {
589 None
590 } else {
591 Some(from_ul(s.trim()))
592 })
593 }
594
595 pub fn de_optional_color_primaries<'de, D: Deserializer<'de>>(
596 d: D,
597 ) -> Result<Option<ColorPrimaries>, D::Error> {
598 de_optional_ul_type(d, ColorPrimaries::from_ul)
599 }
600
601 pub fn de_optional_transfer_characteristic<'de, D: Deserializer<'de>>(
602 d: D,
603 ) -> Result<Option<TransferCharacteristic>, D::Error> {
604 de_optional_ul_type(d, TransferCharacteristic::from_ul)
605 }
606
607 pub fn de_optional_video_codec<'de, D: Deserializer<'de>>(
608 d: D,
609 ) -> Result<Option<VideoCodec>, D::Error> {
610 de_optional_ul_type(d, VideoCodec::from_ul)
611 }
612
613 pub fn de_optional_coding_equations<'de, D: Deserializer<'de>>(
614 d: D,
615 ) -> Result<Option<CodingEquations>, D::Error> {
616 de_optional_ul_type(d, CodingEquations::from_ul)
617 }
618
619 pub fn de_optional_mca_tag_symbol<'de, D: Deserializer<'de>>(
620 d: D,
621 ) -> Result<Option<McaTagSymbol>, D::Error> {
622 let s = String::deserialize(d)?;
623 Ok(if s.trim().is_empty() {
624 None
625 } else {
626 Some(McaTagSymbol::parse(s.trim()))
627 })
628 }
629
630 pub fn de_optional_language_tag<'de, D: Deserializer<'de>>(
631 d: D,
632 ) -> Result<Option<LanguageTag>, D::Error> {
633 let s = String::deserialize(d)?;
634 let trimmed = s.trim();
635 if trimmed.is_empty() {
636 Ok(None)
637 } else {
638 LanguageTag::parse(trimmed)
639 .map(Some)
640 .map_err(serde::de::Error::custom)
641 }
642 }
643
644 pub fn de_optional_color_siting<'de, D: Deserializer<'de>>(
647 d: D,
648 ) -> Result<Option<u32>, D::Error> {
649 let s = String::deserialize(d)?;
650 let s = s.trim();
651 if s.is_empty() {
652 return Ok(None);
653 }
654 if let Ok(n) = s.parse::<u32>() {
655 return Ok(Some(n));
656 }
657 let v = match s.to_lowercase().as_str() {
658 "cositing" => 0,
659 "horizcositing" => 1,
660 "threetap" => 2,
661 "quincunx" => 3,
662 "rec709" => 4,
663 "rec601" => 6,
664 _ => return Ok(None),
665 };
666 Ok(Some(v))
667 }
668
669 pub fn de_language_tag_list<'de, D: Deserializer<'de>>(
670 d: D,
671 ) -> Result<Vec<LanguageTag>, D::Error> {
672 let s = String::deserialize(d)?;
673 s.split(',')
674 .map(|part| part.trim())
675 .filter(|part| !part.is_empty())
676 .map(|part| LanguageTag::parse(part).map_err(serde::de::Error::custom))
677 .collect()
678 }
679}
680
681fn default_content_kind() -> ContentKindElement {
683 ContentKindElement {
684 kind: ContentKind::Other("unknown".to_string()),
685 scope: None,
686 }
687}
688
689pub const CONTENT_KIND_DEFAULT_SCOPE: &str =
695 "http://www.smpte-ra.org/schemas/2067-3/2013#content-kind";
696
697#[derive(Debug, Clone, PartialEq, Eq)]
705#[cfg_attr(feature = "typescript", derive(TS))]
706#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
707#[cfg_attr(feature = "wasm", derive(Tsify))]
708#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
709pub struct ContentKindElement {
710 pub kind: ContentKind,
711 pub scope: Option<String>,
713}
714
715impl ContentKindElement {
716 pub fn effective_scope(&self) -> &str {
718 self.scope.as_deref().unwrap_or(CONTENT_KIND_DEFAULT_SCOPE)
719 }
720}
721
722impl std::fmt::Display for ContentKindElement {
723 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
724 self.kind.fmt(f)
725 }
726}
727
728impl PartialEq<ContentKind> for ContentKindElement {
729 fn eq(&self, other: &ContentKind) -> bool {
730 self.kind == *other
731 }
732}
733
734impl From<ContentKind> for ContentKindElement {
735 fn from(kind: ContentKind) -> Self {
736 Self { kind, scope: None }
737 }
738}
739
740impl<'de> Deserialize<'de> for ContentKindElement {
741 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
742 where
743 D: serde::Deserializer<'de>,
744 {
745 use serde::de::{self, MapAccess, Visitor};
746 use std::fmt;
747
748 struct ContentKindElementVisitor;
749
750 impl<'de> Visitor<'de> for ContentKindElementVisitor {
751 type Value = ContentKindElement;
752
753 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
754 formatter.write_str(
755 "a string or an object with text content and optional @scope attribute",
756 )
757 }
758
759 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
760 where
761 E: de::Error,
762 {
763 Ok(ContentKindElement {
764 kind: ContentKind::parse(value),
765 scope: None,
766 })
767 }
768
769 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
770 where
771 M: MapAccess<'de>,
772 {
773 let mut text = None;
774 let mut scope = None;
775
776 while let Some(key) = map.next_key::<String>()? {
777 match key.as_str() {
778 "$text" | "#text" | "$value" => {
779 if text.is_some() {
780 return Err(de::Error::duplicate_field("text"));
781 }
782 text = Some(map.next_value::<String>()?);
783 }
784 "@scope" | "scope" => {
785 if scope.is_some() {
786 return Err(de::Error::duplicate_field("scope"));
787 }
788 let raw: String = map.next_value()?;
789 let trimmed = raw.trim();
790 if !trimmed.is_empty() {
791 scope = Some(trimmed.to_string());
792 }
793 }
794 _ => {
795 let _ = map.next_value::<serde::de::IgnoredAny>()?;
796 }
797 }
798 }
799
800 let kind = ContentKind::parse(text.as_deref().unwrap_or(""));
801 Ok(ContentKindElement { kind, scope })
802 }
803 }
804
805 deserializer.deserialize_any(ContentKindElementVisitor)
806 }
807}
808
809impl Serialize for ContentKindElement {
810 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
811 where
812 S: serde::Serializer,
813 {
814 use serde::ser::SerializeStruct;
815
816 if self.scope.is_some() {
817 let mut state = serializer.serialize_struct("ContentKindElement", 2)?;
818 state.serialize_field("$text", &self.kind.to_string())?;
819 state.serialize_field("@scope", &self.scope)?;
820 state.end()
821 } else {
822 serializer.serialize_str(&self.kind.to_string())
823 }
824 }
825}
826
827#[cfg(feature = "jsonschema")]
828impl schemars::JsonSchema for ContentKindElement {
829 fn schema_name() -> String {
830 "ContentKindElement".to_owned()
831 }
832
833 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
834 use schemars::schema::*;
835
836 let string_schema = gen.subschema_for::<String>();
837 let mut obj = SchemaObject {
838 instance_type: Some(InstanceType::Object.into()),
839 ..Default::default()
840 };
841 let obj_validation = obj.object();
842 obj_validation
843 .properties
844 .insert("$text".to_owned(), gen.subschema_for::<String>());
845 obj_validation
846 .properties
847 .insert("@scope".to_owned(), gen.subschema_for::<Option<String>>());
848 obj_validation.required.insert("$text".to_owned());
849
850 SchemaObject {
851 subschemas: Some(Box::new(SubschemaValidation {
852 any_of: Some(vec![string_schema, obj.into()]),
853 ..Default::default()
854 })),
855 ..Default::default()
856 }
857 .into()
858 }
859}
860
861pub const MARKER_LABEL_DEFAULT_SCOPE: &str =
867 "http://www.smpte-ra.org/schemas/2067-3/2013#standard-markers";
868
869#[derive(Debug, Clone, PartialEq, Eq)]
877#[cfg_attr(feature = "typescript", derive(TS))]
878#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
879#[cfg_attr(feature = "wasm", derive(Tsify))]
880#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
881pub struct MarkerLabelElement {
882 pub label: MarkerLabel,
883 pub scope: Option<String>,
885}
886
887impl MarkerLabelElement {
888 pub fn effective_scope(&self) -> &str {
890 self.scope.as_deref().unwrap_or(MARKER_LABEL_DEFAULT_SCOPE)
891 }
892}
893
894impl std::fmt::Display for MarkerLabelElement {
895 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
896 self.label.fmt(f)
897 }
898}
899
900impl PartialEq<MarkerLabel> for MarkerLabelElement {
901 fn eq(&self, other: &MarkerLabel) -> bool {
902 self.label == *other
903 }
904}
905
906impl From<MarkerLabel> for MarkerLabelElement {
907 fn from(label: MarkerLabel) -> Self {
908 Self { label, scope: None }
909 }
910}
911
912impl<'de> Deserialize<'de> for MarkerLabelElement {
913 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
914 where
915 D: serde::Deserializer<'de>,
916 {
917 use serde::de::{self, MapAccess, Visitor};
918 use std::fmt;
919
920 struct MarkerLabelElementVisitor;
921
922 impl<'de> Visitor<'de> for MarkerLabelElementVisitor {
923 type Value = MarkerLabelElement;
924
925 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
926 formatter.write_str(
927 "a string or an object with text content and optional @scope attribute",
928 )
929 }
930
931 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
932 where
933 E: de::Error,
934 {
935 Ok(MarkerLabelElement {
936 label: MarkerLabel::parse(value),
937 scope: None,
938 })
939 }
940
941 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
942 where
943 M: MapAccess<'de>,
944 {
945 let mut text = None;
946 let mut scope = None;
947
948 while let Some(key) = map.next_key::<String>()? {
949 match key.as_str() {
950 "$text" | "#text" | "$value" => {
951 if text.is_some() {
952 return Err(de::Error::duplicate_field("text"));
953 }
954 text = Some(map.next_value::<String>()?);
955 }
956 "@scope" | "scope" => {
957 if scope.is_some() {
958 return Err(de::Error::duplicate_field("scope"));
959 }
960 let raw: String = map.next_value()?;
961 let trimmed = raw.trim();
962 if !trimmed.is_empty() {
963 scope = Some(trimmed.to_string());
964 }
965 }
966 _ => {
967 let _ = map.next_value::<serde::de::IgnoredAny>()?;
968 }
969 }
970 }
971
972 let label = MarkerLabel::parse(text.as_deref().unwrap_or(""));
973 Ok(MarkerLabelElement { label, scope })
974 }
975 }
976
977 deserializer.deserialize_any(MarkerLabelElementVisitor)
978 }
979}
980
981impl Serialize for MarkerLabelElement {
982 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
983 where
984 S: serde::Serializer,
985 {
986 use serde::ser::SerializeStruct;
987
988 if self.scope.is_some() {
989 let mut state = serializer.serialize_struct("MarkerLabelElement", 2)?;
990 state.serialize_field("$text", &self.label.to_string())?;
991 state.serialize_field("@scope", &self.scope)?;
992 state.end()
993 } else {
994 serializer.serialize_str(&self.label.to_string())
995 }
996 }
997}
998
999#[cfg(feature = "jsonschema")]
1000impl schemars::JsonSchema for MarkerLabelElement {
1001 fn schema_name() -> String {
1002 "MarkerLabelElement".to_owned()
1003 }
1004
1005 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
1006 use schemars::schema::*;
1007
1008 let string_schema = gen.subschema_for::<String>();
1009 let mut obj = SchemaObject {
1010 instance_type: Some(InstanceType::Object.into()),
1011 ..Default::default()
1012 };
1013 let obj_validation = obj.object();
1014 obj_validation
1015 .properties
1016 .insert("$text".to_owned(), gen.subschema_for::<String>());
1017 obj_validation
1018 .properties
1019 .insert("@scope".to_owned(), gen.subschema_for::<Option<String>>());
1020 obj_validation.required.insert("$text".to_owned());
1021
1022 SchemaObject {
1023 subschemas: Some(Box::new(SubschemaValidation {
1024 any_of: Some(vec![string_schema, obj.into()]),
1025 ..Default::default()
1026 })),
1027 ..Default::default()
1028 }
1029 .into()
1030 }
1031}
1032
1033pub fn strip_xml_namespaces(xml: &str) -> String {
1044 use std::sync::LazyLock;
1045 static TAG_PREFIX_RE: LazyLock<regex::Regex> =
1047 LazyLock::new(|| regex::Regex::new(r"<(/?)(\w+):(\w)").unwrap());
1048 let result = TAG_PREFIX_RE.replace_all(xml, "<$1$3");
1049 static XMLNS_PREFIX_RE: LazyLock<regex::Regex> =
1051 LazyLock::new(|| regex::Regex::new(r#"\s+xmlns:\w+="[^"]*""#).unwrap());
1052 XMLNS_PREFIX_RE.replace_all(&result, "").to_string()
1053}
1054
1055#[derive(Debug, Default, PartialEq, Clone)]
1061#[cfg_attr(feature = "typescript", derive(TS))]
1062#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1063#[cfg_attr(feature = "wasm", derive(Tsify))]
1064#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1065pub struct LanguageString {
1066 pub text: String,
1067 pub language: Option<LanguageTag>, }
1069
1070impl std::fmt::Display for LanguageString {
1071 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1072 if let Some(lang) = &self.language {
1073 write!(f, "{} ({})", self.text, lang)
1074 } else {
1075 write!(f, "{}", self.text)
1076 }
1077 }
1078}
1079
1080impl<'de> Deserialize<'de> for LanguageString {
1082 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1083 where
1084 D: serde::Deserializer<'de>,
1085 {
1086 use serde::de::{self, MapAccess, Visitor};
1087 use std::fmt;
1088
1089 struct LanguageStringVisitor;
1090
1091 impl<'de> Visitor<'de> for LanguageStringVisitor {
1092 type Value = LanguageString;
1093
1094 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1095 formatter.write_str("a string or an object with text and optional language")
1096 }
1097
1098 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1099 where
1100 E: de::Error,
1101 {
1102 Ok(LanguageString {
1103 text: value.to_string(),
1104 language: None,
1105 })
1106 }
1107
1108 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1109 where
1110 M: MapAccess<'de>,
1111 {
1112 let mut text = None;
1113 let mut language = None;
1114
1115 while let Some(key) = map.next_key::<String>()? {
1116 match key.as_str() {
1117 "$text" | "#text" | "$value" => {
1118 if text.is_some() {
1119 return Err(de::Error::duplicate_field("text"));
1120 }
1121 text = Some(map.next_value()?);
1122 }
1123 "@language" | "language" => {
1124 if language.is_some() {
1125 return Err(de::Error::duplicate_field("language"));
1126 }
1127 let raw: String = map.next_value()?;
1128 let trimmed = raw.trim();
1129 if !trimmed.is_empty() {
1130 language = Some(LanguageTag::new(trimmed));
1131 }
1132 }
1133 _ => {
1134 let _ = map.next_value::<serde::de::IgnoredAny>()?;
1136 }
1137 }
1138 }
1139
1140 Ok(LanguageString {
1141 text: text.unwrap_or_default(),
1142 language,
1143 })
1144 }
1145 }
1146
1147 deserializer.deserialize_any(LanguageStringVisitor)
1148 }
1149}
1150
1151impl Serialize for LanguageString {
1152 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1153 where
1154 S: serde::Serializer,
1155 {
1156 use serde::ser::SerializeStruct;
1157
1158 if self.language.is_some() {
1159 let mut state = serializer.serialize_struct("LanguageString", 2)?;
1160 state.serialize_field("$text", &self.text)?;
1161 state.serialize_field("@language", &self.language)?;
1162 state.end()
1163 } else {
1164 serializer.serialize_str(&self.text)
1165 }
1166 }
1167}
1168
1169#[cfg(feature = "jsonschema")]
1170impl schemars::JsonSchema for LanguageString {
1171 fn schema_name() -> String {
1172 "LanguageString".to_owned()
1173 }
1174
1175 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
1176 use schemars::schema::*;
1177
1178 let string_schema = gen.subschema_for::<String>();
1179 let mut obj = SchemaObject {
1180 instance_type: Some(InstanceType::Object.into()),
1181 ..Default::default()
1182 };
1183 let obj_validation = obj.object();
1184 obj_validation
1185 .properties
1186 .insert("$text".to_owned(), gen.subschema_for::<String>());
1187 obj_validation.properties.insert(
1188 "@language".to_owned(),
1189 gen.subschema_for::<Option<String>>(),
1190 );
1191 obj_validation.required.insert("$text".to_owned());
1192
1193 SchemaObject {
1194 subschemas: Some(Box::new(SubschemaValidation {
1195 any_of: Some(vec![string_schema, obj.into()]),
1196 ..Default::default()
1197 })),
1198 ..Default::default()
1199 }
1200 .into()
1201 }
1202}
1203
1204#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1210#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1211#[cfg_attr(feature = "typescript", derive(TS))]
1212#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1213#[cfg_attr(feature = "wasm", derive(Tsify))]
1214#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1215pub struct LocaleList {
1216 #[cfg_attr(not(feature = "wasm"), serde(rename = "Locale", default))]
1217 #[cfg_attr(feature = "wasm", serde(rename = "locales", alias = "Locale", default))]
1218 pub locales: Vec<Locale>,
1219}
1220
1221#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1222#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1223#[cfg_attr(feature = "typescript", derive(TS))]
1224#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1225#[cfg_attr(feature = "wasm", derive(Tsify))]
1226#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1227pub struct Locale {
1228 #[cfg_attr(not(feature = "wasm"), serde(rename = "LanguageList", default))]
1229 #[cfg_attr(
1230 feature = "wasm",
1231 serde(rename = "languageList", alias = "LanguageList", default)
1232 )]
1233 pub language_list: Option<LanguageList>,
1234
1235 #[cfg_attr(not(feature = "wasm"), serde(rename = "RegionList", default))]
1236 #[cfg_attr(
1237 feature = "wasm",
1238 serde(rename = "regionList", alias = "RegionList", default)
1239 )]
1240 pub region_list: Option<RegionList>,
1241
1242 #[cfg_attr(
1243 not(feature = "wasm"),
1244 serde(rename = "ContentMaturityRatingList", default)
1245 )]
1246 #[cfg_attr(
1247 feature = "wasm",
1248 serde(
1249 rename = "contentMaturityRatingList",
1250 alias = "ContentMaturityRatingList",
1251 default
1252 )
1253 )]
1254 pub content_maturity_rating_list: Option<ContentMaturityRatingList>,
1255}
1256
1257#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1258#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1259#[cfg_attr(feature = "typescript", derive(TS))]
1260#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1261#[cfg_attr(feature = "wasm", derive(Tsify))]
1262#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1263pub struct ContentMaturityRatingList {
1264 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentMaturityRating"))]
1265 #[cfg_attr(
1266 feature = "wasm",
1267 serde(rename = "contentMaturityRatings", alias = "ContentMaturityRating")
1268 )]
1269 pub ratings: Vec<ContentMaturityRating>,
1270}
1271
1272#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1274#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1275#[cfg_attr(feature = "typescript", derive(TS))]
1276#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1277#[cfg_attr(feature = "wasm", derive(Tsify))]
1278#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1279pub struct ContentMaturityRating {
1280 #[cfg_attr(not(feature = "wasm"), serde(rename = "Agency"))]
1281 #[cfg_attr(feature = "wasm", serde(rename = "agency", alias = "Agency"))]
1282 pub agency: String,
1283
1284 #[cfg_attr(not(feature = "wasm"), serde(rename = "Rating", default))]
1285 #[cfg_attr(feature = "wasm", serde(rename = "rating", alias = "Rating", default))]
1286 pub rating: Option<String>,
1287
1288 #[cfg_attr(not(feature = "wasm"), serde(rename = "Audience", default))]
1289 #[cfg_attr(
1290 feature = "wasm",
1291 serde(rename = "audience", alias = "Audience", default)
1292 )]
1293 pub audience: Option<AudienceElement>,
1294}
1295
1296#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1298#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1299#[cfg_attr(feature = "typescript", derive(TS))]
1300#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1301#[cfg_attr(feature = "wasm", derive(Tsify))]
1302#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1303pub struct AudienceElement {
1304 #[serde(rename = "@scope", default)]
1305 pub scope: Option<String>,
1306
1307 #[serde(rename = "$text", default)]
1308 pub text: Option<String>,
1309}
1310
1311#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1312#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1313#[cfg_attr(feature = "typescript", derive(TS))]
1314#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1315#[cfg_attr(feature = "wasm", derive(Tsify))]
1316#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1317pub struct LanguageList {
1318 #[cfg_attr(not(feature = "wasm"), serde(rename = "Language"))]
1319 #[cfg_attr(feature = "wasm", serde(rename = "languages", alias = "Language"))]
1320 pub languages: Vec<LanguageTag>, }
1322
1323#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1324#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1325#[cfg_attr(feature = "typescript", derive(TS))]
1326#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1327#[cfg_attr(feature = "wasm", derive(Tsify))]
1328#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1329pub struct RegionList {
1330 #[cfg_attr(not(feature = "wasm"), serde(rename = "Region"))]
1331 #[cfg_attr(feature = "wasm", serde(rename = "regions", alias = "Region"))]
1332 pub regions: Vec<String>, }
1334
1335#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1340#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1341#[cfg_attr(feature = "typescript", derive(TS))]
1342#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1343#[cfg_attr(feature = "wasm", derive(Tsify))]
1344#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1345pub struct ExtensionProperties {
1346 #[cfg_attr(
1347 not(feature = "wasm"),
1348 serde(rename = "ApplicationIdentification", default)
1349 )]
1350 #[cfg_attr(
1351 feature = "wasm",
1352 serde(
1353 rename = "applicationIdentification",
1354 alias = "ApplicationIdentification",
1355 default
1356 )
1357 )]
1358 pub application_identification: Option<String>,
1359
1360 #[cfg_attr(not(feature = "wasm"), serde(rename = "MaxCLL", default))]
1361 #[cfg_attr(feature = "wasm", serde(rename = "maxCLL", alias = "MaxCLL", default))]
1362 pub max_cll: Option<u32>,
1363
1364 #[cfg_attr(not(feature = "wasm"), serde(rename = "MaxFALL", default))]
1365 #[cfg_attr(
1366 feature = "wasm",
1367 serde(rename = "maxFALL", alias = "MaxFALL", default)
1368 )]
1369 pub max_fall: Option<u32>,
1370}
1371
1372#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1377#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1378#[cfg_attr(feature = "typescript", derive(TS))]
1379#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1380#[cfg_attr(feature = "wasm", derive(Tsify))]
1381#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1382pub struct EssenceDescriptorList {
1383 #[cfg_attr(not(feature = "wasm"), serde(rename = "EssenceDescriptor"))]
1384 #[cfg_attr(
1385 feature = "wasm",
1386 serde(rename = "essenceDescriptors", alias = "EssenceDescriptor")
1387 )]
1388 pub essence_descriptors: Vec<EssenceDescriptor>,
1389}
1390
1391#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1392#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1393#[cfg_attr(feature = "typescript", derive(TS))]
1394#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1395#[cfg_attr(feature = "wasm", derive(Tsify))]
1396#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1397pub struct EssenceDescriptor {
1398 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
1399 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
1400 pub id: ImfUuid,
1401
1402 #[cfg_attr(not(feature = "wasm"), serde(rename = "RGBADescriptor", default))]
1403 #[cfg_attr(
1404 feature = "wasm",
1405 serde(rename = "rgbaDescriptor", alias = "RGBADescriptor", default)
1406 )]
1407 pub rgba_descriptor: Option<RGBADescriptor>,
1408
1409 #[cfg_attr(not(feature = "wasm"), serde(rename = "CDCIDescriptor", default))]
1410 #[cfg_attr(
1411 feature = "wasm",
1412 serde(rename = "cdciDescriptor", alias = "CDCIDescriptor", default)
1413 )]
1414 pub cdci_descriptor: Option<CDCIDescriptor>,
1415
1416 #[cfg_attr(not(feature = "wasm"), serde(rename = "WAVEPCMDescriptor", default))]
1417 #[cfg_attr(
1418 feature = "wasm",
1419 serde(rename = "wavePCMDescriptor", alias = "WAVEPCMDescriptor", default)
1420 )]
1421 pub wave_pcm_descriptor: Option<WAVEPCMDescriptor>,
1422
1423 #[cfg_attr(
1424 not(feature = "wasm"),
1425 serde(rename = "DCTimedTextDescriptor", default)
1426 )]
1427 #[cfg_attr(
1428 feature = "wasm",
1429 serde(
1430 rename = "dcTimedTextDescriptor",
1431 alias = "DCTimedTextDescriptor",
1432 default
1433 )
1434 )]
1435 pub dc_timed_text_descriptor: Option<DCTimedTextDescriptor>,
1436
1437 #[cfg_attr(not(feature = "wasm"), serde(rename = "IABEssenceDescriptor", default))]
1438 #[cfg_attr(
1439 feature = "wasm",
1440 serde(
1441 rename = "iabEssenceDescriptor",
1442 alias = "IABEssenceDescriptor",
1443 default
1444 )
1445 )]
1446 pub iab_essence_descriptor: Option<IABEssenceDescriptor>,
1447
1448 #[cfg_attr(
1449 not(feature = "wasm"),
1450 serde(rename = "ISXDDataEssenceDescriptor", default)
1451 )]
1452 #[cfg_attr(
1453 feature = "wasm",
1454 serde(
1455 rename = "isxdDataEssenceDescriptor",
1456 alias = "ISXDDataEssenceDescriptor",
1457 default
1458 )
1459 )]
1460 pub isxd_data_essence_descriptor: Option<ISXDDataEssenceDescriptor>,
1461}
1462
1463#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1465#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1466#[cfg_attr(feature = "typescript", derive(TS))]
1467#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1468#[cfg_attr(feature = "wasm", derive(Tsify))]
1469#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1470pub struct RGBADescriptor {
1471 #[serde(rename = "InstanceID", default)]
1472 pub instance_id: Option<String>,
1473
1474 #[serde(rename = "DisplayWidth", default)]
1475 pub display_width: Option<u32>,
1476
1477 #[serde(rename = "DisplayHeight", default)]
1478 pub display_height: Option<u32>,
1479
1480 #[serde(rename = "StoredWidth", default)]
1481 pub stored_width: Option<u32>,
1482
1483 #[serde(rename = "StoredHeight", default)]
1484 pub stored_height: Option<u32>,
1485
1486 #[serde(
1487 rename = "SampleRate",
1488 default,
1489 deserialize_with = "de_helpers::de_optional_edit_rate"
1490 )]
1491 pub sample_rate: Option<EditRate>,
1492
1493 #[serde(rename = "ImageAspectRatio", default)]
1494 pub image_aspect_ratio: Option<String>,
1495
1496 #[serde(
1497 rename = "ColorPrimaries",
1498 default,
1499 deserialize_with = "de_helpers::de_optional_color_primaries"
1500 )]
1501 pub color_primaries: Option<ColorPrimaries>,
1502
1503 #[serde(
1504 rename = "TransferCharacteristic",
1505 default,
1506 deserialize_with = "de_helpers::de_optional_transfer_characteristic"
1507 )]
1508 pub transfer_characteristic: Option<TransferCharacteristic>,
1509
1510 #[serde(
1511 rename = "CodingEquations",
1512 default,
1513 deserialize_with = "de_helpers::de_optional_coding_equations"
1514 )]
1515 pub coding_equations: Option<CodingEquations>,
1516
1517 #[serde(
1518 rename = "PictureCompression",
1519 default,
1520 deserialize_with = "de_helpers::de_optional_video_codec"
1521 )]
1522 pub picture_compression: Option<VideoCodec>,
1523
1524 #[serde(rename = "FrameLayout", default)]
1527 pub frame_layout: Option<String>,
1528
1529 #[serde(rename = "DisplayF2Offset", default)]
1531 pub display_f2_offset: Option<i32>,
1532
1533 #[serde(rename = "ComponentMaxRef", default)]
1535 pub component_max_ref: Option<u32>,
1536
1537 #[serde(rename = "ComponentMinRef", default)]
1539 pub component_min_ref: Option<u32>,
1540
1541 #[serde(rename = "ScanningDirection", default)]
1544 pub scanning_direction: Option<String>,
1545
1546 #[serde(rename = "StoredF2Offset", default)]
1548 pub stored_f2_offset: Option<i32>,
1549
1550 #[serde(rename = "SampledWidth", default)]
1552 pub sampled_width: Option<u32>,
1553
1554 #[serde(rename = "SampledHeight", default)]
1556 pub sampled_height: Option<u32>,
1557
1558 #[serde(rename = "SampledXOffset", default)]
1560 pub sampled_x_offset: Option<u32>,
1561
1562 #[serde(rename = "SampledYOffset", default)]
1564 pub sampled_y_offset: Option<u32>,
1565
1566 #[serde(rename = "AlphaTransparency", default)]
1568 pub alpha_transparency: Option<String>,
1569
1570 #[serde(rename = "ImageAlignmentOffset", default)]
1572 pub image_alignment_offset: Option<u32>,
1573
1574 #[serde(rename = "ImageStartOffset", default)]
1576 pub image_start_offset: Option<u32>,
1577
1578 #[serde(rename = "ImageEndOffset", default)]
1580 pub image_end_offset: Option<u32>,
1581
1582 #[serde(rename = "FieldDominance", default)]
1584 pub field_dominance: Option<u32>,
1585
1586 #[serde(rename = "AlphaMaxRef", default)]
1588 pub alpha_max_ref: Option<u32>,
1589
1590 #[serde(rename = "AlphaMinRef", default)]
1592 pub alpha_min_ref: Option<u32>,
1593
1594 #[serde(rename = "Palette", default)]
1596 pub palette: Option<String>,
1597
1598 #[serde(rename = "PaletteLayout", default)]
1600 pub palette_layout: Option<String>,
1601
1602 #[serde(rename = "LinkedTrackID", default)]
1603 pub linked_track_id: Option<u32>,
1604
1605 #[serde(rename = "SubDescriptors", default)]
1606 pub sub_descriptors: Option<VideoSubDescriptors>,
1607}
1608
1609#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1611#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1612#[cfg_attr(feature = "typescript", derive(TS))]
1613#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1614#[cfg_attr(feature = "wasm", derive(Tsify))]
1615#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1616pub struct CDCIDescriptor {
1617 #[serde(rename = "InstanceUID", alias = "InstanceID", default)]
1618 pub instance_id: Option<String>,
1619
1620 #[serde(rename = "StoredWidth", default)]
1621 pub stored_width: Option<u32>,
1622
1623 #[serde(rename = "StoredHeight", default)]
1624 pub stored_height: Option<u32>,
1625
1626 #[serde(rename = "DisplayWidth", default)]
1627 pub display_width: Option<u32>,
1628
1629 #[serde(rename = "DisplayHeight", default)]
1630 pub display_height: Option<u32>,
1631
1632 #[serde(rename = "ActiveWidth", default)]
1633 pub active_width: Option<u32>,
1634
1635 #[serde(rename = "ActiveHeight", default)]
1636 pub active_height: Option<u32>,
1637
1638 #[serde(
1639 rename = "SampleRate",
1640 default,
1641 deserialize_with = "de_helpers::de_optional_edit_rate"
1642 )]
1643 pub sample_rate: Option<EditRate>,
1644
1645 #[serde(rename = "ImageAspectRatio", default)]
1646 pub image_aspect_ratio: Option<String>,
1647
1648 #[serde(
1649 rename = "ColorPrimaries",
1650 default,
1651 deserialize_with = "de_helpers::de_optional_color_primaries"
1652 )]
1653 pub color_primaries: Option<ColorPrimaries>,
1654
1655 #[serde(
1656 rename = "TransferCharacteristic",
1657 default,
1658 deserialize_with = "de_helpers::de_optional_transfer_characteristic"
1659 )]
1660 pub transfer_characteristic: Option<TransferCharacteristic>,
1661
1662 #[serde(
1663 rename = "CodingEquations",
1664 default,
1665 deserialize_with = "de_helpers::de_optional_coding_equations"
1666 )]
1667 pub coding_equations: Option<CodingEquations>,
1668
1669 #[serde(
1670 rename = "PictureCompression",
1671 default,
1672 deserialize_with = "de_helpers::de_optional_video_codec"
1673 )]
1674 pub picture_compression: Option<VideoCodec>,
1675
1676 #[serde(rename = "ComponentDepth", default)]
1677 pub component_depth: Option<u32>,
1678
1679 #[serde(rename = "FrameLayout", default)]
1682 pub frame_layout: Option<String>,
1683
1684 #[serde(rename = "DisplayF2Offset", default)]
1686 pub display_f2_offset: Option<i32>,
1687
1688 #[serde(rename = "HorizontalSubsampling", default)]
1691 pub horizontal_subsampling: Option<u32>,
1692
1693 #[serde(rename = "VerticalSubsampling", default)]
1696 pub vertical_subsampling: Option<u32>,
1697
1698 #[serde(
1701 rename = "ColorSiting",
1702 default,
1703 deserialize_with = "de_helpers::de_optional_color_siting"
1704 )]
1705 pub color_siting: Option<u32>,
1706
1707 #[serde(rename = "BlackRefLevel", default)]
1709 pub black_ref_level: Option<u32>,
1710
1711 #[serde(rename = "WhiteRefLevel", default)]
1713 pub white_ref_level: Option<u32>,
1714
1715 #[serde(rename = "ColorRange", default)]
1717 pub color_range: Option<u32>,
1718
1719 #[serde(rename = "StoredF2Offset", default)]
1721 pub stored_f2_offset: Option<i32>,
1722
1723 #[serde(rename = "SampledWidth", default)]
1725 pub sampled_width: Option<u32>,
1726
1727 #[serde(rename = "SampledHeight", default)]
1729 pub sampled_height: Option<u32>,
1730
1731 #[serde(rename = "SampledXOffset", default)]
1733 pub sampled_x_offset: Option<u32>,
1734
1735 #[serde(rename = "SampledYOffset", default)]
1737 pub sampled_y_offset: Option<u32>,
1738
1739 #[serde(rename = "AlphaTransparency", default)]
1741 pub alpha_transparency: Option<String>,
1742
1743 #[serde(rename = "ImageAlignmentOffset", default)]
1745 pub image_alignment_offset: Option<u32>,
1746
1747 #[serde(rename = "ImageStartOffset", default)]
1749 pub image_start_offset: Option<u32>,
1750
1751 #[serde(rename = "ImageEndOffset", default)]
1753 pub image_end_offset: Option<u32>,
1754
1755 #[serde(rename = "FieldDominance", default)]
1757 pub field_dominance: Option<u32>,
1758
1759 #[serde(rename = "ReversedByteOrder", default)]
1761 pub reversed_byte_order: Option<String>,
1762
1763 #[serde(rename = "PaddingBits", default)]
1765 pub padding_bits: Option<i32>,
1766
1767 #[serde(rename = "AlphaSampleDepth", default)]
1769 pub alpha_sample_depth: Option<u32>,
1770
1771 #[serde(rename = "LinkedTrackID", default)]
1772 pub linked_track_id: Option<u32>,
1773
1774 #[serde(rename = "SubDescriptors", default)]
1775 pub sub_descriptors: Option<VideoSubDescriptors>,
1776}
1777
1778#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1780#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1781#[cfg_attr(feature = "typescript", derive(TS))]
1782#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1783#[cfg_attr(feature = "wasm", derive(Tsify))]
1784#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1785pub struct VideoSubDescriptors {
1786 #[serde(rename = "PHDRMetadataTrackSubDescriptor", default)]
1788 pub phdr_metadata_track_sub_descriptor: Option<PHDRMetadataTrackSubDescriptor>,
1789
1790 #[serde(rename = "JPEG2000SubDescriptor", default)]
1792 pub jpeg2000_sub_descriptor: Option<JPEG2000SubDescriptor>,
1793}
1794
1795#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1797#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1798#[cfg_attr(feature = "typescript", derive(TS))]
1799#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1800#[cfg_attr(feature = "wasm", derive(Tsify))]
1801#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1802pub struct JPEG2000SubDescriptor {
1803 #[serde(rename = "InstanceID", default)]
1804 pub instance_id: Option<String>,
1805
1806 #[serde(rename = "Rsiz", default)]
1808 pub rsiz: Option<u32>,
1809
1810 #[serde(rename = "Xsiz", default)]
1812 pub xsiz: Option<u32>,
1813
1814 #[serde(rename = "Ysiz", default)]
1816 pub ysiz: Option<u32>,
1817
1818 #[serde(rename = "XOsiz", default)]
1820 pub xo_siz: Option<u32>,
1821
1822 #[serde(rename = "YOsiz", default)]
1824 pub yo_siz: Option<u32>,
1825
1826 #[serde(rename = "XTsiz", default)]
1828 pub xt_siz: Option<u32>,
1829
1830 #[serde(rename = "YTsiz", default)]
1832 pub yt_siz: Option<u32>,
1833
1834 #[serde(rename = "XTOsiz", default)]
1836 pub xto_siz: Option<u32>,
1837
1838 #[serde(rename = "YTOsiz", default)]
1840 pub yto_siz: Option<u32>,
1841
1842 #[serde(rename = "Csiz", default)]
1844 pub csiz: Option<u32>,
1845
1846 #[serde(rename = "CodingStyleDefault", default)]
1848 pub coding_style_default: Option<String>,
1849
1850 #[serde(rename = "QuantizationDefault", default)]
1852 pub quantization_default: Option<String>,
1853
1854 #[serde(rename = "J2CLayout", default)]
1856 pub j2c_layout: Option<J2CLayout>,
1857
1858 #[serde(rename = "J2KExtendedCapabilities", default)]
1860 pub j2k_extended_capabilities: Option<J2KExtendedCapabilities>,
1861
1862 #[serde(rename = "PictureComponentSizing", default)]
1864 pub picture_component_sizing: Option<PictureComponentSizing>,
1865}
1866
1867#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1869#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1870#[cfg_attr(feature = "typescript", derive(TS))]
1871#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1872#[cfg_attr(feature = "wasm", derive(Tsify))]
1873#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1874pub struct J2CLayout {
1875 #[serde(rename = "RGBAComponent", default)]
1876 pub components: Vec<RGBALayoutComponent>,
1877}
1878
1879#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1881#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1882#[cfg_attr(feature = "typescript", derive(TS))]
1883#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1884#[cfg_attr(feature = "wasm", derive(Tsify))]
1885#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1886pub struct RGBALayoutComponent {
1887 #[serde(rename = "Code", default)]
1890 pub code: String,
1891
1892 #[serde(rename = "ComponentSize", default)]
1893 pub component_size: u32,
1894}
1895
1896#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1898#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1899#[cfg_attr(feature = "typescript", derive(TS))]
1900#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1901#[cfg_attr(feature = "wasm", derive(Tsify))]
1902#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1903pub struct J2KExtendedCapabilities {
1904 #[serde(rename = "Pcap", default)]
1906 pub pcap: Option<u64>,
1907}
1908
1909#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1911#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1912#[cfg_attr(feature = "typescript", derive(TS))]
1913#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1914#[cfg_attr(feature = "wasm", derive(Tsify))]
1915#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1916pub struct PictureComponentSizing {
1917 #[serde(rename = "J2KComponentSizing", default)]
1918 pub components: Vec<J2KComponentSizing>,
1919}
1920
1921#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1923#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1924#[cfg_attr(feature = "typescript", derive(TS))]
1925#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1926#[cfg_attr(feature = "wasm", derive(Tsify))]
1927#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1928pub struct J2KComponentSizing {
1929 #[serde(rename = "Ssiz", default)]
1931 pub ssiz: Option<u32>,
1932
1933 #[serde(rename = "XRSiz", default)]
1935 pub xr_siz: Option<u32>,
1936
1937 #[serde(rename = "YRSiz", default)]
1939 pub yr_siz: Option<u32>,
1940}
1941
1942#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1944#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1945#[cfg_attr(feature = "typescript", derive(TS))]
1946#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1947#[cfg_attr(feature = "wasm", derive(Tsify))]
1948#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1949pub struct PHDRMetadataTrackSubDescriptor {
1950 #[serde(rename = "InstanceID", default)]
1951 pub instance_id: Option<String>,
1952
1953 #[serde(rename = "PHDRMetadataTrackSubDescriptor_DataDefinition", default)]
1954 pub data_definition: Option<String>,
1955
1956 #[serde(rename = "PHDRMetadataTrackSubDescriptor_SimplePayloadSID", default)]
1957 pub simple_payload_sid: Option<u32>,
1958
1959 #[serde(rename = "PHDRMetadataTrackSubDescriptor_SourceTrackID", default)]
1960 pub source_track_id: Option<u32>,
1961}
1962
1963#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1965#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1966#[cfg_attr(feature = "typescript", derive(TS))]
1967#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
1968#[cfg_attr(feature = "wasm", derive(Tsify))]
1969#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1970pub struct WAVEPCMDescriptor {
1971 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
1972 pub instance_id: Option<String>,
1973
1974 #[serde(
1975 rename = "SampleRate",
1976 default,
1977 deserialize_with = "de_helpers::de_optional_edit_rate"
1978 )]
1979 pub sample_rate: Option<EditRate>,
1980
1981 #[serde(
1982 rename = "AudioSampleRate",
1983 default,
1984 deserialize_with = "de_helpers::de_optional_edit_rate"
1985 )]
1986 pub audio_sample_rate: Option<EditRate>,
1987
1988 #[serde(rename = "ChannelCount", default)]
1989 pub channel_count: Option<u32>,
1990
1991 #[serde(rename = "QuantizationBits", default)]
1992 pub quantization_bits: Option<u32>,
1993
1994 #[serde(rename = "LinkedTrackID", default)]
1995 pub linked_track_id: Option<u32>,
1996
1997 #[serde(rename = "SubDescriptors", default)]
1998 pub sub_descriptors: Option<AudioSubDescriptors>,
1999}
2000
2001#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2003#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2004#[cfg_attr(feature = "typescript", derive(TS))]
2005#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2006#[cfg_attr(feature = "wasm", derive(Tsify))]
2007#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2008pub struct AudioSubDescriptors {
2009 #[serde(rename = "SoundfieldGroupLabelSubDescriptor", default)]
2010 pub soundfield_group_label_sub_descriptor: Option<SoundfieldGroupLabelSubDescriptor>,
2011}
2012
2013#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2015#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2016#[cfg_attr(feature = "typescript", derive(TS))]
2017#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2018#[cfg_attr(feature = "wasm", derive(Tsify))]
2019#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2020pub struct SoundfieldGroupLabelSubDescriptor {
2021 #[serde(
2022 rename = "MCATagSymbol",
2023 default,
2024 deserialize_with = "de_helpers::de_optional_mca_tag_symbol"
2025 )]
2026 pub mca_tag_symbol: Option<McaTagSymbol>,
2027
2028 #[serde(rename = "MCATagName", default)]
2029 pub mca_tag_name: Option<String>,
2030
2031 #[serde(rename = "MCAAudioContentKind", default)]
2032 pub mca_audio_content_kind: Option<String>,
2033
2034 #[serde(
2036 rename = "RFC5646SpokenLanguage",
2037 alias = "RFC5646AudioLanguageCode",
2038 default,
2039 deserialize_with = "de_helpers::de_optional_language_tag"
2040 )]
2041 pub rfc5646_spoken_language: Option<LanguageTag>,
2042}
2043
2044#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2046#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2047#[cfg_attr(feature = "typescript", derive(TS))]
2048#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2049#[cfg_attr(feature = "wasm", derive(Tsify))]
2050#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2051pub struct DCTimedTextDescriptor {
2052 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2053 pub instance_id: Option<String>,
2054
2055 #[serde(rename = "LinkedTrackID", default)]
2056 pub linked_track_id: Option<u32>,
2057
2058 #[serde(
2059 rename = "SampleRate",
2060 default,
2061 deserialize_with = "de_helpers::de_optional_edit_rate"
2062 )]
2063 pub sample_rate: Option<EditRate>,
2064
2065 #[serde(
2067 rename = "RFC5646LanguageTagList",
2068 default,
2069 deserialize_with = "de_helpers::de_language_tag_list"
2070 )]
2071 pub rfc5646_language_tag_list: Vec<LanguageTag>,
2072
2073 #[serde(rename = "NamespaceURI", default)]
2074 pub namespace_uri: Option<String>,
2075}
2076
2077#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2079#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2080#[cfg_attr(feature = "typescript", derive(TS))]
2081#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2082#[cfg_attr(feature = "wasm", derive(Tsify))]
2083#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2084pub struct IABEssenceDescriptor {
2085 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2086 pub instance_id: Option<String>,
2087
2088 #[serde(rename = "LinkedTrackID", default)]
2089 pub linked_track_id: Option<u32>,
2090
2091 #[serde(
2092 rename = "SampleRate",
2093 default,
2094 deserialize_with = "de_helpers::de_optional_edit_rate"
2095 )]
2096 pub sample_rate: Option<EditRate>,
2097
2098 #[serde(
2099 rename = "AudioSampleRate",
2100 default,
2101 deserialize_with = "de_helpers::de_optional_edit_rate"
2102 )]
2103 pub audio_sample_rate: Option<EditRate>,
2104
2105 #[serde(rename = "ChannelCount", default)]
2106 pub channel_count: Option<u32>,
2107
2108 #[serde(rename = "QuantizationBits", default)]
2110 pub quantization_bits: Option<u32>,
2111
2112 #[serde(rename = "ContainerFormat", default)]
2114 pub container_format: Option<String>,
2115
2116 #[serde(rename = "SoundCompression", default)]
2117 pub sound_compression: Option<String>,
2118
2119 #[serde(rename = "Codec", default)]
2121 pub codec: Option<String>,
2122
2123 #[serde(rename = "ElectrospatialFormulation", default)]
2125 pub electrospatial_formulation: Option<u32>,
2126
2127 #[serde(rename = "SubDescriptors", default)]
2128 pub sub_descriptors: Option<IABSubDescriptors>,
2129}
2130
2131#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2133#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2134#[cfg_attr(feature = "typescript", derive(TS))]
2135#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2136#[cfg_attr(feature = "wasm", derive(Tsify))]
2137#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2138pub struct IABSubDescriptors {
2139 #[serde(rename = "IABSoundfieldLabelSubDescriptor", default)]
2140 pub iab_soundfield_label_sub_descriptor: Option<IABSoundfieldLabelSubDescriptor>,
2141
2142 #[serde(rename = "IABChannelSubDescriptor", default)]
2149 pub iab_channel_sub_descriptors: Vec<IABChannelSubDescriptor>,
2150}
2151
2152#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2161#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
2162#[cfg_attr(feature = "typescript", derive(TS))]
2163#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2164#[cfg_attr(feature = "wasm", derive(Tsify))]
2165#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2166pub struct IABChannelSubDescriptor {
2167 #[serde(rename = "IABBedMetaID", default)]
2169 pub bed_meta_id: Option<u32>,
2170 #[serde(rename = "IABChannelID", default)]
2172 pub channel_id: Option<u32>,
2173}
2174
2175#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2177#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2178#[cfg_attr(feature = "typescript", derive(TS))]
2179#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2180#[cfg_attr(feature = "wasm", derive(Tsify))]
2181#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2182pub struct IABSoundfieldLabelSubDescriptor {
2183 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2184 pub instance_id: Option<String>,
2185
2186 #[serde(
2187 rename = "MCATagSymbol",
2188 default,
2189 deserialize_with = "de_helpers::de_optional_mca_tag_symbol"
2190 )]
2191 pub mca_tag_symbol: Option<McaTagSymbol>,
2192
2193 #[serde(rename = "MCATagName", default)]
2194 pub mca_tag_name: Option<String>,
2195
2196 #[serde(rename = "MCALabelDictionaryID", default)]
2198 pub mca_label_dictionary_id: Option<String>,
2199
2200 #[serde(
2201 rename = "RFC5646SpokenLanguage",
2202 alias = "RFC5646AudioLanguageCode",
2203 default,
2204 deserialize_with = "de_helpers::de_optional_language_tag"
2205 )]
2206 pub rfc5646_spoken_language: Option<LanguageTag>,
2207}
2208
2209#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2211#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
2212#[cfg_attr(feature = "typescript", derive(TS))]
2213#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2214#[cfg_attr(feature = "wasm", derive(Tsify))]
2215#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2216pub struct IsxdSubDescriptors {
2217 #[serde(rename = "ContainerConstraintsSubDescriptor", default)]
2219 pub container_constraints_sub_descriptor: Option<ContainerConstraintsSubDescriptor>,
2220}
2221
2222#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2224#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2225#[cfg_attr(feature = "typescript", derive(TS))]
2226#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2227#[cfg_attr(feature = "wasm", derive(Tsify))]
2228#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2229pub struct ContainerConstraintsSubDescriptor {
2230 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2231 pub instance_id: Option<String>,
2232}
2233
2234#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2236#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2237#[cfg_attr(feature = "typescript", derive(TS))]
2238#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2239#[cfg_attr(feature = "wasm", derive(Tsify))]
2240#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2241pub struct ISXDDataEssenceDescriptor {
2242 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2243 pub instance_id: Option<String>,
2244
2245 #[serde(rename = "LinkedTrackID", default)]
2246 pub linked_track_id: Option<u32>,
2247
2248 #[serde(
2249 rename = "SampleRate",
2250 default,
2251 deserialize_with = "de_helpers::de_optional_edit_rate"
2252 )]
2253 pub sample_rate: Option<EditRate>,
2254
2255 #[serde(rename = "DataEssenceCoding", default)]
2256 pub data_essence_coding: Option<String>,
2257
2258 #[serde(rename = "NamespaceURI", default)]
2259 pub namespace_uri: Option<String>,
2260
2261 #[serde(rename = "SubDescriptors", default)]
2262 pub sub_descriptors: Option<IsxdSubDescriptors>,
2263}
2264
2265#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2313#[derive(Debug, Serialize, Deserialize, PartialEq)]
2314#[cfg_attr(feature = "typescript", derive(TS))]
2315#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2316#[cfg_attr(feature = "wasm", derive(Tsify))]
2317#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2318pub struct CompositionPlaylist {
2319 #[serde(skip)]
2322 pub namespace: CplNamespace,
2323
2324 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2325 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2326 #[cfg_attr(feature = "typescript", ts(rename = "id"))]
2327 pub id: ImfUuid,
2328
2329 #[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
2330 #[cfg_attr(
2331 feature = "wasm",
2332 serde(rename = "annotation", alias = "Annotation", default)
2333 )]
2334 #[cfg_attr(feature = "typescript", ts(rename = "annotation"))]
2335 pub annotation: Option<LanguageString>,
2336
2337 #[cfg_attr(not(feature = "wasm"), serde(rename = "IssueDate"))]
2338 #[cfg_attr(feature = "wasm", serde(rename = "issueDate", alias = "IssueDate"))]
2339 #[cfg_attr(feature = "typescript", ts(rename = "issueDate"))]
2340 pub issue_date: String, #[cfg_attr(not(feature = "wasm"), serde(rename = "Issuer", default))]
2343 #[cfg_attr(feature = "wasm", serde(rename = "issuer", alias = "Issuer", default))]
2344 #[cfg_attr(feature = "typescript", ts(rename = "issuer"))]
2345 pub issuer: Option<LanguageString>,
2346
2347 #[cfg_attr(not(feature = "wasm"), serde(rename = "Creator", default))]
2348 #[cfg_attr(
2349 feature = "wasm",
2350 serde(rename = "creator", alias = "Creator", default)
2351 )]
2352 #[cfg_attr(feature = "typescript", ts(rename = "creator"))]
2353 pub creator: Option<LanguageString>,
2354
2355 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentOriginator", default))]
2356 #[cfg_attr(
2357 feature = "wasm",
2358 serde(rename = "contentOriginator", alias = "ContentOriginator", default)
2359 )]
2360 #[cfg_attr(feature = "typescript", ts(rename = "contentOriginator"))]
2361 pub content_originator: Option<LanguageString>,
2362
2363 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentTitle"))]
2364 #[cfg_attr(
2365 feature = "wasm",
2366 serde(rename = "contentTitle", alias = "ContentTitle")
2367 )]
2368 #[cfg_attr(feature = "typescript", ts(rename = "contentTitle"))]
2369 pub content_title: LanguageString,
2370
2371 #[cfg_attr(
2372 not(feature = "wasm"),
2373 serde(rename = "ContentKind", default = "default_content_kind")
2374 )]
2375 #[cfg_attr(
2376 feature = "wasm",
2377 serde(
2378 rename = "contentKind",
2379 alias = "ContentKind",
2380 default = "default_content_kind"
2381 )
2382 )]
2383 #[cfg_attr(feature = "typescript", ts(rename = "contentKind"))]
2384 pub content_kind: ContentKindElement,
2385
2386 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentVersionList", default))]
2387 #[cfg_attr(
2388 feature = "wasm",
2389 serde(rename = "contentVersionList", alias = "ContentVersionList", default)
2390 )]
2391 #[cfg_attr(feature = "typescript", ts(rename = "contentVersionList"))]
2392 pub content_version_list: Option<ContentVersionList>,
2393
2394 #[cfg_attr(
2395 not(feature = "wasm"),
2396 serde(rename = "EssenceDescriptorList", default)
2397 )]
2398 #[cfg_attr(
2399 feature = "wasm",
2400 serde(
2401 rename = "essenceDescriptorList",
2402 alias = "EssenceDescriptorList",
2403 default
2404 )
2405 )]
2406 #[cfg_attr(feature = "typescript", ts(rename = "essenceDescriptorList"))]
2407 pub essence_descriptor_list: Option<EssenceDescriptorList>,
2408
2409 #[cfg_attr(
2410 not(feature = "wasm"),
2411 serde(
2412 rename = "EditRate",
2413 default,
2414 deserialize_with = "de_helpers::de_optional_edit_rate"
2415 )
2416 )]
2417 #[cfg_attr(
2418 feature = "wasm",
2419 serde(
2420 rename = "editRate",
2421 alias = "EditRate",
2422 default,
2423 deserialize_with = "de_helpers::de_optional_edit_rate"
2424 )
2425 )]
2426 #[cfg_attr(feature = "typescript", ts(rename = "editRate"))]
2427 pub edit_rate: Option<EditRate>,
2428
2429 #[cfg_attr(not(feature = "wasm"), serde(rename = "TotalRunningTime", default))]
2430 #[cfg_attr(
2431 feature = "wasm",
2432 serde(rename = "totalRunningTime", alias = "TotalRunningTime", default)
2433 )]
2434 #[cfg_attr(feature = "typescript", ts(rename = "totalRunningTime"))]
2435 pub total_running_time: Option<String>,
2436
2437 #[cfg_attr(not(feature = "wasm"), serde(rename = "LocaleList", default))]
2438 #[cfg_attr(
2439 feature = "wasm",
2440 serde(rename = "localeList", alias = "LocaleList", default)
2441 )]
2442 #[cfg_attr(feature = "typescript", ts(rename = "localeList"))]
2443 pub locale_list: Option<LocaleList>,
2444
2445 #[cfg_attr(not(feature = "wasm"), serde(rename = "ExtensionProperties", default))]
2446 #[cfg_attr(
2447 feature = "wasm",
2448 serde(rename = "extensionProperties", alias = "ExtensionProperties", default)
2449 )]
2450 #[cfg_attr(feature = "typescript", ts(rename = "extensionProperties"))]
2451 pub extension_properties: Option<ExtensionProperties>,
2452
2453 #[cfg_attr(not(feature = "wasm"), serde(rename = "CompositionTimecode", default))]
2454 #[cfg_attr(
2455 feature = "wasm",
2456 serde(rename = "compositionTimecode", alias = "CompositionTimecode", default)
2457 )]
2458 #[cfg_attr(feature = "typescript", ts(rename = "compositionTimecode"))]
2459 pub composition_timecode: Option<CompositionTimecode>,
2460
2461 #[serde(skip)]
2464 pub has_signer: bool,
2465
2466 #[serde(skip)]
2469 pub has_signature: bool,
2470
2471 #[serde(skip)]
2477 pub source_xml: Option<String>,
2478
2479 #[cfg_attr(not(feature = "wasm"), serde(rename = "SegmentList"))]
2480 #[cfg_attr(feature = "wasm", serde(rename = "segmentList", alias = "SegmentList"))]
2481 #[cfg_attr(feature = "typescript", ts(rename = "segmentList"))]
2482 pub segment_list: SegmentList,
2483}
2484
2485#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2491#[cfg_attr(not(feature = "wasm"), serde(rename_all = "PascalCase"))]
2492#[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))]
2493#[cfg_attr(feature = "typescript", derive(TS))]
2494#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2495#[cfg_attr(feature = "wasm", derive(Tsify))]
2496#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2497pub struct CompositionTimecode {
2498 #[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeDropFrame"))]
2499 #[cfg_attr(
2500 feature = "wasm",
2501 serde(rename = "timecodeDropFrame", alias = "TimecodeDropFrame")
2502 )]
2503 pub timecode_drop_frame: Option<bool>,
2504
2505 #[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeRate"))]
2506 #[cfg_attr(
2507 feature = "wasm",
2508 serde(rename = "timecodeRate", alias = "TimecodeRate")
2509 )]
2510 pub timecode_rate: Option<u32>,
2511
2512 #[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeStartAddress"))]
2513 #[cfg_attr(
2514 feature = "wasm",
2515 serde(rename = "timecodeStartAddress", alias = "TimecodeStartAddress")
2516 )]
2517 pub timecode_start_address: Option<String>,
2518}
2519
2520#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2525#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2526#[cfg_attr(feature = "typescript", derive(TS))]
2527#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2528#[cfg_attr(feature = "wasm", derive(Tsify))]
2529#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2530pub struct ContentVersionList {
2531 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentVersion"))]
2532 #[cfg_attr(
2533 feature = "wasm",
2534 serde(rename = "contentVersions", alias = "ContentVersion")
2535 )]
2536 pub content_versions: Vec<ContentVersion>,
2537}
2538
2539#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2540#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2541#[cfg_attr(feature = "typescript", derive(TS))]
2542#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2543#[cfg_attr(feature = "wasm", derive(Tsify))]
2544#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2545pub struct ContentVersion {
2546 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2547 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2548 pub id: String,
2549
2550 #[cfg_attr(not(feature = "wasm"), serde(rename = "LabelText", default))]
2551 #[cfg_attr(
2552 feature = "wasm",
2553 serde(rename = "labelText", alias = "LabelText", default)
2554 )]
2555 pub label_text: Option<LanguageString>,
2556}
2557
2558#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2563#[derive(Debug, Serialize, Deserialize, PartialEq)]
2564#[cfg_attr(feature = "typescript", derive(TS))]
2565#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2566#[cfg_attr(feature = "wasm", derive(Tsify))]
2567#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2568pub struct SegmentList {
2569 #[cfg_attr(not(feature = "wasm"), serde(rename = "Segment", default))]
2570 #[cfg_attr(
2571 feature = "wasm",
2572 serde(rename = "segments", alias = "Segment", default)
2573 )]
2574 pub segments: Vec<Segment>,
2575}
2576
2577#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2578#[derive(Debug, Serialize, Deserialize, PartialEq)]
2579#[cfg_attr(feature = "typescript", derive(TS))]
2580#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2581#[cfg_attr(feature = "wasm", derive(Tsify))]
2582#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2583pub struct Segment {
2584 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2585 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2586 pub id: ImfUuid,
2587
2588 #[cfg_attr(not(feature = "wasm"), serde(rename = "SequenceList"))]
2589 #[cfg_attr(
2590 feature = "wasm",
2591 serde(rename = "sequenceList", alias = "SequenceList")
2592 )]
2593 pub sequence_list: SequenceList,
2594}
2595
2596#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2597#[derive(Debug, Serialize, Deserialize, PartialEq)]
2598#[cfg_attr(feature = "typescript", derive(TS))]
2599#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2600#[cfg_attr(feature = "wasm", derive(Tsify))]
2601#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2602pub struct SequenceList {
2603 #[cfg_attr(not(feature = "wasm"), serde(rename = "MarkerSequence", default))]
2604 #[cfg_attr(
2605 feature = "wasm",
2606 serde(rename = "markerSequences", alias = "MarkerSequence", default)
2607 )]
2608 pub marker_sequences: Vec<MarkerSequence>,
2609
2610 #[cfg_attr(not(feature = "wasm"), serde(rename = "MainImageSequence", default))]
2611 #[cfg_attr(
2612 feature = "wasm",
2613 serde(rename = "mainImageSequences", alias = "MainImageSequence", default)
2614 )]
2615 pub main_image_sequences: Vec<MainImageSequence>,
2616
2617 #[cfg_attr(not(feature = "wasm"), serde(rename = "MainAudioSequence", default))]
2618 #[cfg_attr(
2619 feature = "wasm",
2620 serde(rename = "mainAudioSequences", alias = "MainAudioSequence", default)
2621 )]
2622 pub main_audio_sequences: Vec<MainAudioSequence>,
2623
2624 #[cfg_attr(
2625 not(feature = "wasm"),
2626 serde(rename = "SubtitlesSequence", alias = "MainSubtitleSequence", default)
2627 )]
2628 #[cfg_attr(
2629 feature = "wasm",
2630 serde(
2631 rename = "subtitlesSequences",
2632 alias = "SubtitlesSequence",
2633 alias = "MainSubtitleSequence",
2634 default
2635 )
2636 )]
2637 pub subtitles_sequences: Vec<SubtitlesSequence>,
2638
2639 #[cfg_attr(
2640 not(feature = "wasm"),
2641 serde(rename = "HearingImpairedCaptionsSequence", default)
2642 )]
2643 #[cfg_attr(
2644 feature = "wasm",
2645 serde(
2646 rename = "hearingImpairedCaptionsSequences",
2647 alias = "HearingImpairedCaptionsSequence",
2648 default
2649 )
2650 )]
2651 pub hearing_impaired_captions_sequences: Vec<HearingImpairedCaptionsSequence>,
2652
2653 #[cfg_attr(
2654 not(feature = "wasm"),
2655 serde(rename = "ForcedNarrativeSequence", default)
2656 )]
2657 #[cfg_attr(
2658 feature = "wasm",
2659 serde(
2660 rename = "forcedNarrativeSequences",
2661 alias = "ForcedNarrativeSequence",
2662 default
2663 )
2664 )]
2665 pub forced_narrative_sequences: Vec<ForcedNarrativeSequence>,
2666
2667 #[cfg_attr(not(feature = "wasm"), serde(rename = "IABSequence", default))]
2668 #[cfg_attr(
2669 feature = "wasm",
2670 serde(rename = "iabSequences", alias = "IABSequence", default)
2671 )]
2672 pub iab_sequences: Vec<IABSequence>,
2673
2674 #[cfg_attr(not(feature = "wasm"), serde(rename = "ISXDSequence", default))]
2675 #[cfg_attr(
2676 feature = "wasm",
2677 serde(rename = "isxdSequences", alias = "ISXDSequence", default)
2678 )]
2679 pub isxd_sequences: Vec<ISXDSequence>,
2680}
2681
2682impl SequenceList {
2683 pub fn all_sequences(&self) -> Vec<&dyn SequenceAccess> {
2685 let mut v: Vec<&dyn SequenceAccess> = Vec::new();
2686 for s in &self.main_image_sequences {
2687 v.push(s);
2688 }
2689 for s in &self.main_audio_sequences {
2690 v.push(s);
2691 }
2692 for s in &self.subtitles_sequences {
2693 v.push(s);
2694 }
2695 for s in &self.hearing_impaired_captions_sequences {
2696 v.push(s);
2697 }
2698 for s in &self.forced_narrative_sequences {
2699 v.push(s);
2700 }
2701 for s in &self.iab_sequences {
2702 v.push(s);
2703 }
2704 for s in &self.isxd_sequences {
2705 v.push(s);
2706 }
2707 v
2708 }
2709
2710 pub fn all_sequences_typed(&self) -> Vec<(&dyn SequenceAccess, &'static str)> {
2712 let mut v: Vec<(&dyn SequenceAccess, &'static str)> = Vec::new();
2713 for s in &self.main_image_sequences {
2714 v.push((s, "MainImage"));
2715 }
2716 for s in &self.main_audio_sequences {
2717 v.push((s, "MainAudio"));
2718 }
2719 for s in &self.subtitles_sequences {
2720 v.push((s, "Subtitles"));
2721 }
2722 for s in &self.hearing_impaired_captions_sequences {
2723 v.push((s, "HearingImpairedCaptions"));
2724 }
2725 for s in &self.forced_narrative_sequences {
2726 v.push((s, "ForcedNarrative"));
2727 }
2728 for s in &self.iab_sequences {
2729 v.push((s, "IAB"));
2730 }
2731 for s in &self.isxd_sequences {
2732 v.push((s, "ISXD"));
2733 }
2734 v
2735 }
2736}
2737
2738pub trait SequenceAccess {
2741 fn id(&self) -> &ImfUuid;
2742 fn track_id(&self) -> &ImfUuid;
2743 fn resource_list(&self) -> &ResourceList;
2744}
2745
2746macro_rules! define_sequence_type {
2747 ($name:ident) => {
2748 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2749 #[derive(Debug, Serialize, Deserialize, PartialEq)]
2750 #[cfg_attr(feature = "typescript", derive(TS))]
2751 #[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2752 #[cfg_attr(feature = "wasm", derive(Tsify))]
2753 #[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2754 pub struct $name {
2755 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2756 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2757 pub id: ImfUuid,
2758
2759 #[cfg_attr(not(feature = "wasm"), serde(rename = "TrackId"))]
2760 #[cfg_attr(feature = "wasm", serde(rename = "trackId", alias = "TrackId"))]
2761 pub track_id: ImfUuid,
2762
2763 #[cfg_attr(not(feature = "wasm"), serde(rename = "ResourceList"))]
2764 #[cfg_attr(
2765 feature = "wasm",
2766 serde(rename = "resourceList", alias = "ResourceList")
2767 )]
2768 pub resource_list: ResourceList,
2769 }
2770
2771 impl SequenceAccess for $name {
2772 fn id(&self) -> &ImfUuid {
2773 &self.id
2774 }
2775 fn track_id(&self) -> &ImfUuid {
2776 &self.track_id
2777 }
2778 fn resource_list(&self) -> &ResourceList {
2779 &self.resource_list
2780 }
2781 }
2782 };
2783}
2784
2785define_sequence_type!(MarkerSequence);
2786define_sequence_type!(MainImageSequence);
2787define_sequence_type!(MainAudioSequence);
2788define_sequence_type!(SubtitlesSequence);
2789define_sequence_type!(HearingImpairedCaptionsSequence);
2790define_sequence_type!(ForcedNarrativeSequence);
2791define_sequence_type!(IABSequence);
2792define_sequence_type!(ISXDSequence);
2793
2794#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2799#[derive(Debug, Serialize, Deserialize, PartialEq)]
2800#[cfg_attr(feature = "typescript", derive(TS))]
2801#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2802#[cfg_attr(feature = "wasm", derive(Tsify))]
2803#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2804pub struct ResourceList {
2805 #[cfg_attr(not(feature = "wasm"), serde(rename = "Resource", default))]
2806 #[cfg_attr(
2807 feature = "wasm",
2808 serde(rename = "resources", alias = "Resource", default)
2809 )]
2810 pub resources: Vec<Resource>,
2811}
2812
2813#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2814#[derive(Debug, Serialize, Deserialize, PartialEq)]
2815#[cfg_attr(feature = "typescript", derive(TS))]
2816#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2817#[cfg_attr(feature = "wasm", derive(Tsify))]
2818#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2819pub struct Resource {
2820 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2821 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2822 pub id: ImfUuid,
2823
2824 #[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
2825 #[cfg_attr(
2826 feature = "wasm",
2827 serde(rename = "annotation", alias = "Annotation", default)
2828 )]
2829 pub annotation: Option<LanguageString>,
2830
2831 #[cfg_attr(
2832 not(feature = "wasm"),
2833 serde(
2834 rename = "EditRate",
2835 default,
2836 deserialize_with = "de_helpers::de_optional_edit_rate"
2837 )
2838 )]
2839 #[cfg_attr(
2840 feature = "wasm",
2841 serde(
2842 rename = "editRate",
2843 alias = "EditRate",
2844 default,
2845 deserialize_with = "de_helpers::de_optional_edit_rate"
2846 )
2847 )]
2848 pub edit_rate: Option<EditRate>,
2849
2850 #[cfg_attr(not(feature = "wasm"), serde(rename = "IntrinsicDuration"))]
2851 #[cfg_attr(
2852 feature = "wasm",
2853 serde(rename = "intrinsicDuration", alias = "IntrinsicDuration")
2854 )]
2855 pub intrinsic_duration: u64,
2856
2857 #[cfg_attr(not(feature = "wasm"), serde(rename = "EntryPoint", default))]
2858 #[cfg_attr(
2859 feature = "wasm",
2860 serde(rename = "entryPoint", alias = "EntryPoint", default)
2861 )]
2862 pub entry_point: Option<u64>,
2863
2864 #[cfg_attr(not(feature = "wasm"), serde(rename = "SourceDuration", default))]
2865 #[cfg_attr(
2866 feature = "wasm",
2867 serde(rename = "sourceDuration", alias = "SourceDuration", default)
2868 )]
2869 pub source_duration: Option<u64>,
2870
2871 #[cfg_attr(not(feature = "wasm"), serde(rename = "SourceEncoding", default))]
2872 #[cfg_attr(
2873 feature = "wasm",
2874 serde(rename = "sourceEncoding", alias = "SourceEncoding", default)
2875 )]
2876 pub source_encoding: Option<ImfUuid>, #[cfg_attr(not(feature = "wasm"), serde(rename = "TrackFileId", default))]
2879 #[cfg_attr(
2880 feature = "wasm",
2881 serde(rename = "trackFileId", alias = "TrackFileId", default)
2882 )]
2883 pub track_file_id: Option<ImfUuid>, #[cfg_attr(not(feature = "wasm"), serde(rename = "RepeatCount", default))]
2886 #[cfg_attr(
2887 feature = "wasm",
2888 serde(rename = "repeatCount", alias = "RepeatCount", default)
2889 )]
2890 pub repeat_count: Option<u64>,
2891
2892 #[cfg_attr(not(feature = "wasm"), serde(rename = "KeyId", default))]
2893 #[cfg_attr(feature = "wasm", serde(rename = "keyId", alias = "KeyId", default))]
2894 pub key_id: Option<ImfUuid>, #[cfg_attr(not(feature = "wasm"), serde(rename = "Hash", default))]
2897 #[cfg_attr(feature = "wasm", serde(rename = "hash", alias = "Hash", default))]
2898 pub hash: Option<String>,
2899
2900 #[cfg_attr(not(feature = "wasm"), serde(rename = "Marker", default))]
2901 #[cfg_attr(feature = "wasm", serde(rename = "markers", alias = "Marker", default))]
2902 pub markers: Vec<MarkerInfo>,
2903}
2904
2905#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2906#[derive(Debug, Serialize, Deserialize, PartialEq)]
2907#[cfg_attr(feature = "typescript", derive(TS))]
2908#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2909#[cfg_attr(feature = "wasm", derive(Tsify))]
2910#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2911pub struct MarkerInfo {
2912 #[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
2913 #[cfg_attr(
2914 feature = "wasm",
2915 serde(rename = "annotation", alias = "Annotation", default)
2916 )]
2917 pub annotation: Option<String>,
2918
2919 #[cfg_attr(not(feature = "wasm"), serde(rename = "Label"))]
2920 #[cfg_attr(feature = "wasm", serde(rename = "label", alias = "Label"))]
2921 pub label: MarkerLabelElement,
2922
2923 #[cfg_attr(not(feature = "wasm"), serde(rename = "Offset"))]
2924 #[cfg_attr(feature = "wasm", serde(rename = "offset", alias = "Offset"))]
2925 pub offset: u64,
2926}
2927
2928#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2934#[derive(Debug, Clone, Serialize, Deserialize)]
2935#[cfg_attr(feature = "typescript", derive(TS))]
2936#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2937#[cfg_attr(feature = "wasm", derive(Tsify))]
2938#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2939pub struct TrackInfo {
2940 pub track_id: String,
2941 pub track_type: String, pub codec: String,
2943 pub language: Option<String>,
2944 pub channels: Option<String>,
2945 pub format_details: Option<String>,
2946 pub resolution: Option<String>,
2947 pub framerate: Option<String>,
2948 pub bit_depth: Option<String>,
2949 pub subtitle_type: Option<String>,
2950}
2951
2952pub fn parse_cpl(xml_content: &str) -> Result<CompositionPlaylist, CplParseError> {
2962 parse_cpl_with_options(xml_content, &CplParseOptions::default())
2963}
2964
2965pub fn parse_cpl_with_options(
2967 xml_content: &str,
2968 options: &CplParseOptions<'_>,
2969) -> Result<CompositionPlaylist, CplParseError> {
2970 let namespace = crate::assetmap::detect_root_namespace(xml_content)
2976 .map(|uri| CplNamespace::from_uri(&uri))
2977 .unwrap_or_else(|| CplNamespace::Unknown(String::new()));
2978
2979 let has_signer = xml_content.contains("<Signer") || xml_content.contains(":Signer");
2981 let has_signature = xml_content.contains("<Signature") || xml_content.contains(":Signature");
2982
2983 match options.signature_validation_mode {
2984 SignatureValidationMode::Ignore => {}
2985 SignatureValidationMode::RequirePresence => {
2986 if !has_signature {
2987 return Err(CplParseError::StrictSchema(
2988 "Signature element is required by selected signature mode".to_string(),
2989 ));
2990 }
2991 }
2992 SignatureValidationMode::VerifyIfPresent => {
2993 if has_signature {
2994 let verifier = options
2995 .signature_verifier
2996 .ok_or(CplParseError::SignatureVerifierRequired)?;
2997 verifier
2998 .verify(xml_content)
2999 .map_err(CplParseError::SignatureVerificationFailed)?;
3000 }
3001 }
3002 SignatureValidationMode::RequireValid => {
3003 if !has_signature {
3004 return Err(CplParseError::StrictSchema(
3005 "Signature element is required by selected signature mode".to_string(),
3006 ));
3007 }
3008 let verifier = options
3009 .signature_verifier
3010 .ok_or(CplParseError::SignatureVerifierRequired)?;
3011 verifier
3012 .verify(xml_content)
3013 .map_err(CplParseError::SignatureVerificationFailed)?;
3014 }
3015 }
3016
3017 let stripped = strip_xml_namespaces(xml_content);
3018
3019 if options.unknown_field_mode == UnknownFieldMode::Error {
3020 let unknown = collect_unknown_xml_tokens(&stripped).map_err(|e| {
3021 CplParseError::StrictUnknownXml(format!("unknown token scan failed: {}", e))
3022 })?;
3023 if !unknown.is_empty() {
3024 let list = unknown.into_iter().collect::<Vec<_>>().join(", ");
3025 return Err(CplParseError::StrictUnknownXml(list));
3026 }
3027 }
3028
3029 let mut cpl: CompositionPlaylist = quick_xml::de::from_str(&stripped)?;
3030
3031 if options.schema_strict_mode == SchemaStrictMode::Basic {
3032 validate_basic_schema_constraints(&cpl)?;
3033 }
3034
3035 cpl.namespace = namespace;
3036 cpl.has_signer = has_signer;
3037 cpl.has_signature = has_signature;
3038 cpl.source_xml = Some(xml_content.to_string());
3039 Ok(cpl)
3040}
3041
3042fn validate_basic_schema_constraints(cpl: &CompositionPlaylist) -> Result<(), CplParseError> {
3043 if cpl.segment_list.segments.is_empty() {
3044 return Err(CplParseError::StrictSchema(
3045 "SegmentList must contain at least one Segment".to_string(),
3046 ));
3047 }
3048
3049 for (segment_index, segment) in cpl.segment_list.segments.iter().enumerate() {
3050 let sequence_count = segment.sequence_list.marker_sequences.len()
3051 + segment.sequence_list.main_image_sequences.len()
3052 + segment.sequence_list.main_audio_sequences.len()
3053 + segment.sequence_list.subtitles_sequences.len()
3054 + segment
3055 .sequence_list
3056 .hearing_impaired_captions_sequences
3057 .len()
3058 + segment.sequence_list.forced_narrative_sequences.len()
3059 + segment.sequence_list.iab_sequences.len()
3060 + segment.sequence_list.isxd_sequences.len();
3061
3062 if sequence_count == 0 {
3063 return Err(CplParseError::StrictSchema(format!(
3064 "Segment[{}] must contain at least one sequence",
3065 segment_index
3066 )));
3067 }
3068 }
3069
3070 Ok(())
3071}
3072
3073fn collect_unknown_xml_tokens(xml: &str) -> Result<BTreeSet<String>, String> {
3074 let mut reader = quick_xml::Reader::from_str(xml);
3075 reader.trim_text(true);
3076
3077 let allowed_elements: BTreeSet<&'static str> = [
3078 "CompositionPlaylist",
3079 "Id",
3080 "Annotation",
3081 "IssueDate",
3082 "Issuer",
3083 "Creator",
3084 "ContentOriginator",
3085 "ContentTitle",
3086 "ContentKind",
3087 "ContentVersionList",
3088 "ContentVersion",
3089 "LabelText",
3090 "EssenceDescriptorList",
3091 "EssenceDescriptor",
3092 "EditRate",
3093 "TotalRunningTime",
3094 "LocaleList",
3095 "Locale",
3096 "LanguageList",
3097 "Language",
3098 "RegionList",
3099 "Region",
3100 "ContentMaturityRatingList",
3101 "ContentMaturityRating",
3102 "Agency",
3103 "Rating",
3104 "Audience",
3105 "ExtensionProperties",
3106 "ApplicationIdentification",
3107 "MaxCLL",
3108 "MaxFALL",
3109 "CompositionTimecode",
3110 "TimecodeDropFrame",
3111 "TimecodeRate",
3112 "TimecodeStartAddress",
3113 "SegmentList",
3114 "Segment",
3115 "SequenceList",
3116 "MarkerSequence",
3117 "MainImageSequence",
3118 "MainAudioSequence",
3119 "SubtitlesSequence",
3120 "MainSubtitleSequence",
3121 "HearingImpairedCaptionsSequence",
3122 "ForcedNarrativeSequence",
3123 "IABSequence",
3124 "ISXDSequence",
3125 "TrackId",
3126 "ResourceList",
3127 "Resource",
3128 "IntrinsicDuration",
3129 "EntryPoint",
3130 "SourceDuration",
3131 "SourceEncoding",
3132 "TrackFileId",
3133 "RepeatCount",
3134 "KeyId",
3135 "Hash",
3136 "Marker",
3137 "Label",
3138 "Offset",
3139 "RGBADescriptor",
3140 "CDCIDescriptor",
3141 "WAVEPCMDescriptor",
3142 "DCTimedTextDescriptor",
3143 "IABEssenceDescriptor",
3144 "ISXDDataEssenceDescriptor",
3145 "InstanceID",
3146 "InstanceUID",
3147 "DisplayWidth",
3148 "DisplayHeight",
3149 "StoredWidth",
3150 "StoredHeight",
3151 "SampleRate",
3152 "ImageAspectRatio",
3153 "ColorPrimaries",
3154 "TransferCharacteristic",
3155 "CodingEquations",
3156 "PictureCompression",
3157 "FrameLayout",
3158 "DisplayF2Offset",
3159 "ComponentMaxRef",
3160 "ComponentMinRef",
3161 "ScanningDirection",
3162 "StoredF2Offset",
3163 "SampledWidth",
3164 "SampledHeight",
3165 "SampledXOffset",
3166 "SampledYOffset",
3167 "AlphaTransparency",
3168 "ImageAlignmentOffset",
3169 "ImageStartOffset",
3170 "ImageEndOffset",
3171 "FieldDominance",
3172 "AlphaMaxRef",
3173 "AlphaMinRef",
3174 "Palette",
3175 "PaletteLayout",
3176 "LinkedTrackID",
3177 "SubDescriptors",
3178 "ActiveWidth",
3179 "ActiveHeight",
3180 "ComponentDepth",
3181 "HorizontalSubsampling",
3182 "VerticalSubsampling",
3183 "ColorSiting",
3184 "BlackRefLevel",
3185 "WhiteRefLevel",
3186 "ColorRange",
3187 "ReversedByteOrder",
3188 "PaddingBits",
3189 "AlphaSampleDepth",
3190 "PHDRMetadataTrackSubDescriptor",
3191 "JPEG2000SubDescriptor",
3192 "Rsiz",
3193 "Xsiz",
3194 "Ysiz",
3195 "XOsiz",
3196 "YOsiz",
3197 "XTsiz",
3198 "YTsiz",
3199 "XTOsiz",
3200 "YTOsiz",
3201 "Csiz",
3202 "CodingStyleDefault",
3203 "QuantizationDefault",
3204 "J2CLayout",
3205 "J2KExtendedCapabilities",
3206 "PictureComponentSizing",
3207 "RGBAComponent",
3208 "Code",
3209 "ComponentSize",
3210 "Pcap",
3211 "J2KComponentSizing",
3212 "Ssiz",
3213 "XRSiz",
3214 "YRSiz",
3215 "PHDRMetadataTrackSubDescriptor_DataDefinition",
3216 "PHDRMetadataTrackSubDescriptor_SimplePayloadSID",
3217 "PHDRMetadataTrackSubDescriptor_SourceTrackID",
3218 "AudioSampleRate",
3219 "ChannelCount",
3220 "QuantizationBits",
3221 "SoundfieldGroupLabelSubDescriptor",
3222 "MCATagSymbol",
3223 "MCATagName",
3224 "MCAAudioContentKind",
3225 "RFC5646SpokenLanguage",
3226 "RFC5646AudioLanguageCode",
3227 "RFC5646LanguageTagList",
3228 "NamespaceURI",
3229 "SoundCompression",
3230 "IABSoundfieldLabelSubDescriptor",
3231 "ContainerFormat",
3232 "Codec",
3233 "ElectrospatialFormulation",
3234 "MCALabelDictionaryID",
3235 "EssenceLength",
3236 "Locked",
3237 "MCALinkID",
3238 "MCAChannelID",
3239 "AudioChannelLabelSubDescriptor",
3240 "MCATitle",
3241 "MCATitleVersion",
3242 "MCAAudioElementKind",
3243 "SoundfieldGroupLinkID",
3244 "DataEssenceCoding",
3245 "ContainerConstraintsSubDescriptor",
3246 "Signer",
3247 "Signature",
3248 ]
3249 .into_iter()
3250 .collect();
3251
3252 let allowed_attributes: BTreeSet<&'static str> =
3253 ["xmlns", "scope", "language"].into_iter().collect();
3254
3255 let mut unknown = BTreeSet::new();
3256
3257 loop {
3258 match reader.read_event() {
3259 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
3260 let name = std::str::from_utf8(e.name().as_ref())
3261 .map_err(|e| e.to_string())?
3262 .to_string();
3263 if !allowed_elements.contains(name.as_str()) {
3264 unknown.insert(format!("element:{}", name));
3265 }
3266 for attr in e.attributes() {
3267 let attr = attr.map_err(|e| e.to_string())?;
3268 let key = std::str::from_utf8(attr.key.as_ref())
3269 .map_err(|e| e.to_string())?
3270 .to_string();
3271 if !(allowed_attributes.contains(key.as_str()) || key.starts_with("xmlns:")) {
3272 unknown.insert(format!("attribute:{}@{}", key, name));
3273 }
3274 }
3275 }
3276 Ok(Event::Eof) => break,
3277 Ok(_) => {}
3278 Err(e) => return Err(e.to_string()),
3279 }
3280 }
3281
3282 Ok(unknown)
3283}
3284
3285pub fn extract_cpl_languages(cpl: &CompositionPlaylist) -> Vec<LanguageTag> {
3287 let mut languages: Vec<LanguageTag> = Vec::new();
3288
3289 let add_lang = |languages: &mut Vec<LanguageTag>, lang_opt: &Option<LanguageTag>| {
3290 if let Some(lang) = lang_opt {
3291 if !lang.as_str().is_empty() && !languages.contains(lang) {
3292 languages.push(lang.clone());
3293 }
3294 }
3295 };
3296
3297 let add_lang_string = |languages: &mut Vec<LanguageTag>,
3298 lang_string: &Option<LanguageString>| {
3299 if let Some(ls) = lang_string {
3300 add_lang(languages, &ls.language);
3301 }
3302 };
3303
3304 let add_required_lang_string =
3305 |languages: &mut Vec<LanguageTag>, lang_string: &LanguageString| {
3306 add_lang(languages, &lang_string.language);
3307 };
3308
3309 add_lang_string(&mut languages, &cpl.annotation);
3311 add_lang_string(&mut languages, &cpl.issuer);
3312 add_lang_string(&mut languages, &cpl.creator);
3313 add_lang_string(&mut languages, &cpl.content_originator);
3314 add_required_lang_string(&mut languages, &cpl.content_title);
3315
3316 if let Some(content_version_list) = &cpl.content_version_list {
3318 for version in &content_version_list.content_versions {
3319 add_lang_string(&mut languages, &version.label_text);
3320 }
3321 }
3322
3323 if let Some(locale_list) = &cpl.locale_list {
3325 for locale in &locale_list.locales {
3326 if let Some(language_list) = &locale.language_list {
3327 for lang in &language_list.languages {
3328 if !lang.as_str().is_empty() && !languages.contains(lang) {
3329 languages.push(lang.clone());
3330 }
3331 }
3332 }
3333 }
3334 }
3335
3336 if let Some(edl) = &cpl.essence_descriptor_list {
3338 for ed in &edl.essence_descriptors {
3339 if let Some(wave) = &ed.wave_pcm_descriptor {
3341 if let Some(subs) = &wave.sub_descriptors {
3342 if let Some(sf) = &subs.soundfield_group_label_sub_descriptor {
3343 add_lang(&mut languages, &sf.rfc5646_spoken_language);
3344 }
3345 }
3346 }
3347 if let Some(iab) = &ed.iab_essence_descriptor {
3349 if let Some(subs) = &iab.sub_descriptors {
3350 if let Some(sf) = &subs.iab_soundfield_label_sub_descriptor {
3351 add_lang(&mut languages, &sf.rfc5646_spoken_language);
3352 }
3353 }
3354 }
3355 if let Some(tt) = &ed.dc_timed_text_descriptor {
3357 for lang in &tt.rfc5646_language_tag_list {
3358 if !lang.as_str().is_empty() && !languages.contains(lang) {
3359 languages.push(lang.clone());
3360 }
3361 }
3362 }
3363 }
3364 }
3365
3366 languages.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3367 languages.dedup();
3368 languages
3369}
3370
3371pub fn try_extract_cpl_track_codecs_from_xml(
3373 xml_content: &str,
3374) -> Result<Vec<TrackInfo>, CplParseError> {
3375 let cpl = parse_cpl(xml_content)?;
3376 Ok(extract_tracks_from_cpl(&cpl, xml_content))
3377}
3378
3379pub fn extract_cpl_track_codecs_from_xml(xml_content: &str) -> Vec<TrackInfo> {
3384 try_extract_cpl_track_codecs_from_xml(xml_content).unwrap_or_default()
3385}
3386
3387fn extract_tracks_from_cpl(cpl: &CompositionPlaylist, _raw_xml: &str) -> Vec<TrackInfo> {
3389 let mut tracks = Vec::new();
3390
3391 let descriptors: std::collections::HashMap<ImfUuid, &EssenceDescriptor> =
3393 if let Some(edl) = &cpl.essence_descriptor_list {
3394 edl.essence_descriptors
3395 .iter()
3396 .map(|ed| (ed.id, ed))
3397 .collect()
3398 } else {
3399 std::collections::HashMap::new()
3400 };
3401
3402 for segment in &cpl.segment_list.segments {
3403 let seq_list = &segment.sequence_list;
3404
3405 for seq in &seq_list.main_image_sequences {
3407 for resource in &seq.resource_list.resources {
3408 if let Some(source_encoding) = &resource.source_encoding {
3409 if let Some(ed) = descriptors.get(source_encoding) {
3410 let (codec, resolution, bit_depth) = extract_video_info_from_descriptor(ed);
3411 let framerate = resource
3412 .edit_rate
3413 .as_ref()
3414 .or(cpl.edit_rate.as_ref())
3415 .map(format_framerate);
3416 tracks.push(TrackInfo {
3417 track_id: seq.track_id.to_string(),
3418 track_type: "video".to_string(),
3419 codec,
3420 language: None,
3421 channels: None,
3422 format_details: None,
3423 resolution,
3424 framerate,
3425 bit_depth,
3426 subtitle_type: None,
3427 });
3428 }
3429 }
3430 }
3431 }
3432
3433 for seq in &seq_list.main_audio_sequences {
3435 for resource in &seq.resource_list.resources {
3436 if let Some(source_encoding) = &resource.source_encoding {
3437 if let Some(ed) = descriptors.get(source_encoding) {
3438 let (codec, channels, format_details, language) =
3439 extract_audio_info_from_descriptor(ed);
3440 tracks.push(TrackInfo {
3441 track_id: seq.track_id.to_string(),
3442 track_type: "audio".to_string(),
3443 codec,
3444 language,
3445 channels,
3446 format_details,
3447 resolution: None,
3448 framerate: None,
3449 bit_depth: None,
3450 subtitle_type: None,
3451 });
3452 }
3453 }
3454 }
3455 }
3456
3457 for seq in &seq_list.iab_sequences {
3459 for resource in &seq.resource_list.resources {
3460 if let Some(source_encoding) = &resource.source_encoding {
3461 if let Some(ed) = descriptors.get(source_encoding) {
3462 let language = ed
3463 .iab_essence_descriptor
3464 .as_ref()
3465 .and_then(|iab| iab.sub_descriptors.as_ref())
3466 .and_then(|sd| sd.iab_soundfield_label_sub_descriptor.as_ref())
3467 .and_then(|sf| sf.rfc5646_spoken_language.as_ref())
3468 .map(|lt| lt.as_str().to_string());
3469 tracks.push(TrackInfo {
3470 track_id: seq.track_id.to_string(),
3471 track_type: "audio".to_string(),
3472 codec: "IAB (Dolby Atmos)".to_string(),
3473 language,
3474 channels: Some("Object-based".to_string()),
3475 format_details: Some("Immersive Audio".to_string()),
3476 resolution: None,
3477 framerate: None,
3478 bit_depth: None,
3479 subtitle_type: None,
3480 });
3481 }
3482 }
3483 }
3484 }
3485
3486 let subtitle_sequences: Vec<(&str, &[SubtitlesSequence])> = vec![
3488 ];
3490 let _ = subtitle_sequences; for seq in &seq_list.subtitles_sequences {
3493 if let Some(track) =
3494 extract_timed_text_track(seq.track_id, "standard", &seq.resource_list, &descriptors)
3495 {
3496 tracks.push(track);
3497 }
3498 }
3499 for seq in &seq_list.hearing_impaired_captions_sequences {
3500 if let Some(track) =
3501 extract_timed_text_track(seq.track_id, "hi", &seq.resource_list, &descriptors)
3502 {
3503 tracks.push(track);
3504 }
3505 }
3506 for seq in &seq_list.forced_narrative_sequences {
3507 if let Some(track) =
3508 extract_timed_text_track(seq.track_id, "forced", &seq.resource_list, &descriptors)
3509 {
3510 tracks.push(track);
3511 }
3512 }
3513 }
3514
3515 tracks
3516}
3517
3518fn extract_video_info_from_descriptor(
3519 ed: &EssenceDescriptor,
3520) -> (String, Option<String>, Option<String>) {
3521 if let Some(rgba) = &ed.rgba_descriptor {
3522 let width = rgba.display_width.or(rgba.stored_width);
3523 let height = rgba.display_height.or(rgba.stored_height);
3524 let resolution = match (width, height) {
3525 (Some(w), Some(h)) => Some(format!("{}x{}", w, h)),
3526 _ => None,
3527 };
3528 let codec = rgba
3529 .picture_compression
3530 .as_ref()
3531 .map(|c| c.to_string())
3532 .unwrap_or_else(|| "JPEG 2000".to_string());
3533 return (codec, resolution, None);
3534 }
3535 if let Some(cdci) = &ed.cdci_descriptor {
3536 let width = cdci
3537 .active_width
3538 .or(cdci.display_width)
3539 .or(cdci.stored_width);
3540 let height = cdci
3541 .active_height
3542 .or(cdci.display_height)
3543 .or(cdci.stored_height);
3544 let resolution = match (width, height) {
3545 (Some(w), Some(h)) => Some(format!("{}x{}", w, h)),
3546 _ => None,
3547 };
3548 let codec = cdci
3549 .picture_compression
3550 .as_ref()
3551 .map(|c| c.to_string())
3552 .unwrap_or_else(|| "JPEG 2000".to_string());
3553 let bit_depth = cdci.component_depth.map(|d| format!("{}-bit", d));
3554 return (codec, resolution, bit_depth);
3555 }
3556 ("Unknown".to_string(), None, None)
3557}
3558
3559fn extract_audio_info_from_descriptor(
3560 ed: &EssenceDescriptor,
3561) -> (String, Option<String>, Option<String>, Option<String>) {
3562 if let Some(wave) = &ed.wave_pcm_descriptor {
3563 let codec = wave
3564 .quantization_bits
3565 .map(|b| format!("PCM {}-bit", b))
3566 .unwrap_or_else(|| "PCM".to_string());
3567 let (channels, format_details) = match wave.channel_count {
3568 Some(1) => (Some("1.0".to_string()), Some("Mono".to_string())),
3569 Some(2) => (Some("2.0".to_string()), Some("Stereo".to_string())),
3570 Some(6) => (Some("5.1".to_string()), Some("Surround".to_string())),
3571 Some(8) => (Some("7.1".to_string()), Some("Surround".to_string())),
3572 Some(n) => (Some(format!("{}.0", n)), Some(format!("{} Channel", n))),
3573 None => (None, None),
3574 };
3575 let language = wave
3576 .sub_descriptors
3577 .as_ref()
3578 .and_then(|sd| sd.soundfield_group_label_sub_descriptor.as_ref())
3579 .and_then(|sf| sf.rfc5646_spoken_language.as_ref())
3580 .map(|lt| lt.as_str().to_string());
3581 return (codec, channels, format_details, language);
3582 }
3583 ("Unknown".to_string(), None, None, None)
3584}
3585
3586fn extract_timed_text_track(
3587 track_id: ImfUuid,
3588 subtitle_type: &str,
3589 resource_list: &ResourceList,
3590 descriptors: &std::collections::HashMap<ImfUuid, &EssenceDescriptor>,
3591) -> Option<TrackInfo> {
3592 for resource in &resource_list.resources {
3593 if let Some(source_encoding) = &resource.source_encoding {
3594 if let Some(ed) = descriptors.get(source_encoding) {
3595 let language = ed
3596 .dc_timed_text_descriptor
3597 .as_ref()
3598 .map(|tt| {
3599 tt.rfc5646_language_tag_list
3600 .iter()
3601 .map(|lt| lt.as_str())
3602 .filter(|s| !s.is_empty())
3603 .collect::<Vec<_>>()
3604 .join(",")
3605 })
3606 .filter(|s| !s.is_empty());
3607 return Some(TrackInfo {
3608 track_id: track_id.to_string(),
3609 track_type: "subtitle".to_string(),
3610 codec: "IMSC1 (Timed Text)".to_string(),
3611 language,
3612 channels: None,
3613 format_details: None,
3614 resolution: None,
3615 framerate: None,
3616 bit_depth: None,
3617 subtitle_type: Some(subtitle_type.to_string()),
3618 });
3619 }
3620 }
3621 }
3622 None
3623}
3624
3625pub fn format_framerate(edit_rate: &EditRate) -> String {
3626 let fps = edit_rate.as_f64();
3627 if (fps - 23.976).abs() < 0.01 {
3628 "23.976".to_string()
3629 } else if (fps - 29.97).abs() < 0.01 {
3630 "29.97".to_string()
3631 } else if (fps - 59.94).abs() < 0.01 {
3632 "59.94".to_string()
3633 } else if fps == fps.round() {
3634 format!("{}", fps as u32)
3635 } else {
3636 format!("{:.3}", fps)
3637 }
3638}
3639
3640#[cfg(test)]
3645mod tests {
3646 use super::*;
3647 use crate::assetmap::ImfUuid;
3648
3649 fn make_seq_list_with_all_types() -> SequenceList {
3650 let uuid = || ImfUuid::parse("urn:uuid:00000000-0000-0000-0000-000000000001").unwrap();
3651 let rl = || ResourceList { resources: vec![] };
3652 SequenceList {
3653 marker_sequences: vec![MarkerSequence {
3654 id: uuid(),
3655 track_id: uuid(),
3656 resource_list: rl(),
3657 }],
3658 main_image_sequences: vec![MainImageSequence {
3659 id: uuid(),
3660 track_id: uuid(),
3661 resource_list: rl(),
3662 }],
3663 main_audio_sequences: vec![MainAudioSequence {
3664 id: uuid(),
3665 track_id: uuid(),
3666 resource_list: rl(),
3667 }],
3668 subtitles_sequences: vec![SubtitlesSequence {
3669 id: uuid(),
3670 track_id: uuid(),
3671 resource_list: rl(),
3672 }],
3673 hearing_impaired_captions_sequences: vec![HearingImpairedCaptionsSequence {
3674 id: uuid(),
3675 track_id: uuid(),
3676 resource_list: rl(),
3677 }],
3678 forced_narrative_sequences: vec![ForcedNarrativeSequence {
3679 id: uuid(),
3680 track_id: uuid(),
3681 resource_list: rl(),
3682 }],
3683 iab_sequences: vec![IABSequence {
3684 id: uuid(),
3685 track_id: uuid(),
3686 resource_list: rl(),
3687 }],
3688 isxd_sequences: vec![ISXDSequence {
3689 id: uuid(),
3690 track_id: uuid(),
3691 resource_list: rl(),
3692 }],
3693 }
3694 }
3695
3696 #[test]
3697 fn all_sequences_excludes_markers() {
3698 let sl = make_seq_list_with_all_types();
3699 assert_eq!(sl.all_sequences().len(), 7);
3701 }
3702
3703 #[test]
3704 fn all_sequences_typed_returns_type_names() {
3705 let sl = make_seq_list_with_all_types();
3706 let typed = sl.all_sequences_typed();
3707 assert_eq!(typed.len(), 7);
3708 let names: Vec<&str> = typed.iter().map(|(_, n)| *n).collect();
3709 assert!(names.contains(&"MainImage"));
3710 assert!(names.contains(&"MainAudio"));
3711 assert!(names.contains(&"Subtitles"));
3712 assert!(names.contains(&"HearingImpairedCaptions"));
3713 assert!(names.contains(&"ForcedNarrative"));
3714 assert!(names.contains(&"IAB"));
3715 assert!(names.contains(&"ISXD"));
3716 }
3717
3718 #[test]
3719 fn all_sequences_empty_list() {
3720 let sl = SequenceList {
3721 marker_sequences: vec![],
3722 main_image_sequences: vec![],
3723 main_audio_sequences: vec![],
3724 subtitles_sequences: vec![],
3725 hearing_impaired_captions_sequences: vec![],
3726 forced_narrative_sequences: vec![],
3727 iab_sequences: vec![],
3728 isxd_sequences: vec![],
3729 };
3730 assert!(sl.all_sequences().is_empty());
3731 assert!(sl.all_sequences_typed().is_empty());
3732 }
3733
3734 #[test]
3735 fn try_extract_cpl_track_codecs_invalid_xml() {
3736 let result = try_extract_cpl_track_codecs_from_xml("<not-a-cpl/>");
3737 assert!(result.is_err());
3738 }
3739
3740 struct AcceptAllSignatureVerifier;
3741 impl XmlSignatureVerifier for AcceptAllSignatureVerifier {
3742 fn verify(&self, _xml_content: &str) -> Result<(), String> {
3743 Ok(())
3744 }
3745 }
3746
3747 struct RejectingSignatureVerifier;
3748 impl XmlSignatureVerifier for RejectingSignatureVerifier {
3749 fn verify(&self, _xml_content: &str) -> Result<(), String> {
3750 Err("bad signature".to_string())
3751 }
3752 }
3753
3754 #[test]
3755 fn strict_production_options_enable_all_strict_checks() {
3756 let verifier = ReferenceDigestXmlDsigVerifier;
3757 let options = strict_production_parse_options(&verifier);
3758 assert_eq!(options.unknown_field_mode, UnknownFieldMode::Error);
3759 assert_eq!(options.schema_strict_mode, SchemaStrictMode::Basic);
3760 assert_eq!(
3761 options.signature_validation_mode,
3762 SignatureValidationMode::RequireValid
3763 );
3764 assert!(options.signature_verifier.is_some());
3765 }
3766
3767 #[test]
3768 fn recommended_signature_verifier_rejects_unsigned_xml() {
3769 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
3770 let verifier = recommended_signature_verifier();
3771 assert!(verifier.verify(&xml).is_err());
3772 }
3773
3774 #[test]
3775 fn test_strip_xml_namespaces() {
3776 let input = r#"<r0:RGBADescriptor xmlns:r0="http://example.com"><r1:DisplayWidth>3840</r1:DisplayWidth></r0:RGBADescriptor>"#;
3777 let result = strip_xml_namespaces(input);
3778 assert!(result.contains("<RGBADescriptor"));
3779 assert!(result.contains("<DisplayWidth>3840</DisplayWidth>"));
3780 assert!(result.contains("</RGBADescriptor>"));
3781 assert!(!result.contains("xmlns:r0"));
3782 }
3783
3784 #[test]
3785 fn test_strip_preserves_content_with_colons() {
3786 let input = r#"<PictureCompression>urn:smpte:ul:060e2b34</PictureCompression>"#;
3787 let result = strip_xml_namespaces(input);
3788 assert_eq!(result, input); }
3790
3791 #[test]
3792 fn test_parse_simple_cpl() {
3793 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3794<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3795<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3796<Annotation>Test CPL</Annotation>
3797<IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
3798<ContentTitle>Test Content</ContentTitle>
3799<ContentKind>Test</ContentKind>
3800<SegmentList>
3801<Segment>
3802<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3803<SequenceList>
3804</SequenceList>
3805</Segment>
3806</SegmentList>
3807</CompositionPlaylist>"#;
3808
3809 let result = parse_cpl(xml);
3810 match result {
3811 Ok(cpl) => {
3812 assert_eq!(
3813 cpl.id,
3814 ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap()
3815 );
3816 assert_eq!(cpl.content_title.text, "Test Content");
3817 assert_eq!(cpl.content_kind, ContentKind::Test);
3818 assert!(!cpl.segment_list.segments.is_empty());
3819 }
3820 Err(e) => panic!("Failed to parse CPL: {:?}", e),
3821 }
3822 }
3823
3824 #[test]
3825 fn test_content_kind_scope_attribute() {
3826 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3828<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3829<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3830<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3831<ContentTitle>Test</ContentTitle>
3832<ContentKind scope="http://www.smpte-ra.org/schemas/2067-3/2013#content-kind">feature</ContentKind>
3833<SegmentList>
3834<Segment>
3835<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3836<SequenceList>
3837</SequenceList>
3838</Segment>
3839</SegmentList>
3840</CompositionPlaylist>"#;
3841
3842 let cpl = parse_cpl(xml).expect("Failed to parse CPL with ContentKind scope");
3843 assert_eq!(cpl.content_kind.kind, ContentKind::Feature);
3844 assert_eq!(
3845 cpl.content_kind.scope.as_deref(),
3846 Some("http://www.smpte-ra.org/schemas/2067-3/2013#content-kind")
3847 );
3848 assert_eq!(
3849 cpl.content_kind.effective_scope(),
3850 "http://www.smpte-ra.org/schemas/2067-3/2013#content-kind"
3851 );
3852 }
3853
3854 #[test]
3855 fn test_content_kind_custom_scope() {
3856 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3858<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3859<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3860<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3861<ContentTitle>Test</ContentTitle>
3862<ContentKind scope="http://example.com/custom-kinds">my-custom-kind</ContentKind>
3863<SegmentList>
3864<Segment>
3865<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3866<SequenceList>
3867</SequenceList>
3868</Segment>
3869</SegmentList>
3870</CompositionPlaylist>"#;
3871
3872 let cpl = parse_cpl(xml).expect("Failed to parse CPL with custom scope");
3873 assert_eq!(
3874 cpl.content_kind.kind,
3875 ContentKind::Other("my-custom-kind".to_string())
3876 );
3877 assert_eq!(
3878 cpl.content_kind.scope.as_deref(),
3879 Some("http://example.com/custom-kinds")
3880 );
3881 }
3882
3883 #[test]
3884 fn test_content_kind_no_scope_uses_default() {
3885 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3887<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3888<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3889<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3890<ContentTitle>Test</ContentTitle>
3891<ContentKind>Test</ContentKind>
3892<SegmentList>
3893<Segment>
3894<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3895<SequenceList>
3896</SequenceList>
3897</Segment>
3898</SegmentList>
3899</CompositionPlaylist>"#;
3900
3901 let cpl = parse_cpl(xml).expect("Failed to parse CPL without scope");
3902 assert_eq!(cpl.content_kind.kind, ContentKind::Test);
3903 assert!(cpl.content_kind.scope.is_none());
3904 assert_eq!(
3905 cpl.content_kind.effective_scope(),
3906 CONTENT_KIND_DEFAULT_SCOPE
3907 );
3908 }
3909
3910 #[test]
3911 fn test_malformed_xml_handling() {
3912 let malformed_xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3913<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3914<Id>urn:uuid:test</Id>
3915<ContentTitle>Broken XML"#;
3916
3917 let result: Result<CompositionPlaylist, CplParseError> = parse_cpl(malformed_xml);
3918 assert!(result.is_err(), "Should fail with malformed XML");
3919 }
3920
3921 fn minimal_cpl_with_ns(ns: &str) -> String {
3925 format!(
3926 r#"<?xml version="1.0" encoding="UTF-8" ?>
3927<CompositionPlaylist xmlns="{ns}">
3928<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3929<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3930<ContentTitle>NS Test</ContentTitle>
3931<ContentKind>Test</ContentKind>
3932<SegmentList>
3933<Segment>
3934<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3935<SequenceList></SequenceList>
3936</Segment>
3937</SegmentList>
3938</CompositionPlaylist>"#
3939 )
3940 }
3941
3942 #[test]
3944 fn cpl_parses_with_2067_3_2013_namespace() {
3945 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
3946 let cpl = parse_cpl(&xml).expect("2013 namespace should parse");
3947 assert_eq!(cpl.content_title.text, "NS Test");
3948 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2013);
3949 assert_eq!(cpl.namespace.spec_id(), "ST 2067-3:2013");
3950 assert_eq!(cpl.namespace.year(), Some(2013));
3951 }
3952
3953 #[test]
3955 fn cpl_parses_with_2067_3_2016_namespace() {
3956 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2016");
3957 let cpl = parse_cpl(&xml).expect("2016 namespace should parse");
3958 assert_eq!(cpl.content_title.text, "NS Test");
3959 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2016);
3960 assert_eq!(cpl.namespace.year(), Some(2016));
3961 }
3962
3963 #[test]
3967 fn cpl_parses_with_fake_2020_namespace_as_unknown() {
3968 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/ns/2067-3/2020");
3969 let cpl = parse_cpl(&xml).expect("CPL should still parse, namespace just unknown");
3970 assert_eq!(cpl.content_title.text, "NS Test");
3971 assert!(matches!(cpl.namespace, CplNamespace::Unknown(_)));
3972 assert_eq!(cpl.namespace.year(), None);
3973 }
3974
3975 #[test]
3979 fn cpl_without_root_xmlns_lands_in_unknown_not_2013() {
3980 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
3982<CompositionPlaylist>
3983 <Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3984 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
3985 <ContentTitle>NS Test</ContentTitle>
3986 <EditRate>24 1</EditRate>
3987 <SegmentList></SegmentList>
3988</CompositionPlaylist>"#;
3989 let cpl = parse_cpl(xml).expect("CPL should still parse without xmlns");
3990 assert!(
3991 matches!(cpl.namespace, CplNamespace::Unknown(ref s) if s.is_empty()),
3992 "expected Unknown(\"\") for missing xmlns, got {:?}",
3993 cpl.namespace
3994 );
3995 }
3996
3997 #[test]
3999 fn cpl_parses_with_dci_429_7_namespace() {
4000 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/429-7/2006/CPL");
4001 let cpl = parse_cpl(&xml).expect("DCI 429-7 namespace should parse");
4002 assert_eq!(cpl.content_title.text, "NS Test");
4003 assert_eq!(cpl.namespace, CplNamespace::Dci429_7);
4004 assert_eq!(cpl.namespace.year(), Some(2006));
4005 }
4006
4007 #[test]
4009 fn cpl_meridian_detects_2013_namespace() {
4010 let xml = include_str!("../../../../test-data/MERIDIAN_Netflix_Photon_161006/CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml");
4011 let cpl = parse_cpl(xml).expect("MERIDIAN CPL should parse");
4012 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2013);
4013 }
4014
4015 #[test]
4016 fn strict_unknown_mode_rejects_unknown_elements() {
4017 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4018<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4019<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4020<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4021<ContentTitle>Strict Unknown</ContentTitle>
4022<ContentKind>Test</ContentKind>
4023<UnknownElement>oops</UnknownElement>
4024<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
4025</CompositionPlaylist>"#;
4026
4027 let options = CplParseOptions {
4028 unknown_field_mode: UnknownFieldMode::Error,
4029 ..Default::default()
4030 };
4031 let result = parse_cpl_with_options(xml, &options);
4032 assert!(matches!(result, Err(CplParseError::StrictUnknownXml(_))));
4033 }
4034
4035 #[test]
4036 fn strict_schema_mode_rejects_empty_sequence_list_per_segment() {
4037 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4038<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4039<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4040<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4041<ContentTitle>Strict Schema</ContentTitle>
4042<ContentKind>Test</ContentKind>
4043<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
4044</CompositionPlaylist>"#;
4045
4046 let options = CplParseOptions {
4047 schema_strict_mode: SchemaStrictMode::Basic,
4048 ..Default::default()
4049 };
4050 let result = parse_cpl_with_options(xml, &options);
4051 assert!(matches!(result, Err(CplParseError::StrictSchema(_))));
4052 }
4053
4054 #[test]
4055 fn signature_mode_require_presence_rejects_unsigned_cpl() {
4056 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
4057 let options = CplParseOptions {
4058 signature_validation_mode: SignatureValidationMode::RequirePresence,
4059 ..Default::default()
4060 };
4061 let result = parse_cpl_with_options(&xml, &options);
4062 assert!(matches!(result, Err(CplParseError::StrictSchema(_))));
4063 }
4064
4065 #[test]
4066 fn signature_mode_require_valid_needs_verifier() {
4067 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4068<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4069<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4070<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4071<ContentTitle>Signed</ContentTitle>
4072<ContentKind>Test</ContentKind>
4073<Signature>dummy</Signature>
4074<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
4075</CompositionPlaylist>"#;
4076
4077 let options = CplParseOptions {
4078 signature_validation_mode: SignatureValidationMode::RequireValid,
4079 ..Default::default()
4080 };
4081 let result = parse_cpl_with_options(xml, &options);
4082 assert!(matches!(
4083 result,
4084 Err(CplParseError::SignatureVerifierRequired)
4085 ));
4086 }
4087
4088 #[test]
4089 fn signature_mode_require_valid_uses_verifier() {
4090 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4091<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4092<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4093<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4094<ContentTitle>Signed</ContentTitle>
4095<ContentKind>Test</ContentKind>
4096<Signature>dummy</Signature>
4097<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>
4098</CompositionPlaylist>"#;
4099
4100 let verifier = AcceptAllSignatureVerifier;
4101 let options = CplParseOptions {
4102 signature_validation_mode: SignatureValidationMode::RequireValid,
4103 signature_verifier: Some(&verifier),
4104 ..Default::default()
4105 };
4106 let result = parse_cpl_with_options(xml, &options);
4107 assert!(
4108 result.is_ok(),
4109 "signature verifier should allow parse: {result:?}"
4110 );
4111 }
4112
4113 #[test]
4114 fn signature_mode_require_valid_surfaces_verification_failure() {
4115 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4116<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4117<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4118<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4119<ContentTitle>Signed</ContentTitle>
4120<ContentKind>Test</ContentKind>
4121<Signature>dummy</Signature>
4122<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
4123</CompositionPlaylist>"#;
4124
4125 let verifier = RejectingSignatureVerifier;
4126 let options = CplParseOptions {
4127 signature_validation_mode: SignatureValidationMode::RequireValid,
4128 signature_verifier: Some(&verifier),
4129 ..Default::default()
4130 };
4131 let result = parse_cpl_with_options(xml, &options);
4132 assert!(matches!(
4133 result,
4134 Err(CplParseError::SignatureVerificationFailed(_))
4135 ));
4136 }
4137
4138 fn build_signed_cpl_with_reference_digest(tamper_digest: bool) -> String {
4139 let unsigned_xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4140<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4141<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4142<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4143<ContentTitle>Signed Digest CPL</ContentTitle>
4144<ContentKind>Test</ContentKind>
4145<SegmentList>
4146<Segment>
4147<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
4148<SequenceList></SequenceList>
4149</Segment>
4150</SegmentList>
4151</CompositionPlaylist>"#;
4152
4153 let normalized = normalize_xml_for_digest(unsigned_xml);
4154 let digest = compute_hash(HashAlgorithm::Sha256, normalized.as_bytes());
4155 let mut digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
4156 if tamper_digest {
4157 digest_b64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string();
4158 }
4159
4160 format!(
4161 r#"<?xml version="1.0" encoding="UTF-8" ?>
4162<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4163<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4164<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4165<ContentTitle>Signed Digest CPL</ContentTitle>
4166<ContentKind>Test</ContentKind>
4167<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
4168 <SignedInfo>
4169 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4170 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4171 <Reference URI="">
4172 <Transforms>
4173 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
4174 </Transforms>
4175 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4176 <DigestValue>{}</DigestValue>
4177 </Reference>
4178 </SignedInfo>
4179 <SignatureValue>AQ==</SignatureValue>
4180</Signature>
4181<SegmentList>
4182<Segment>
4183<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
4184<SequenceList></SequenceList>
4185</Segment>
4186</SegmentList>
4187</CompositionPlaylist>"#,
4188 digest_b64
4189 )
4190 }
4191
4192 #[test]
4193 fn reference_digest_verifier_accepts_valid_uri_empty_digest() {
4194 let xml = build_signed_cpl_with_reference_digest(false);
4195 let verifier = ReferenceDigestXmlDsigVerifier;
4196 assert!(verifier.verify(&xml).is_ok());
4197 }
4198
4199 #[test]
4200 fn reference_digest_verifier_rejects_mismatched_digest() {
4201 let xml = build_signed_cpl_with_reference_digest(true);
4202 let verifier = ReferenceDigestXmlDsigVerifier;
4203 let result = verifier.verify(&xml);
4204 assert!(result.is_err());
4205 let error = result.unwrap_err();
4206 assert!(
4207 error.contains("DigestValue mismatch"),
4208 "unexpected error: {error}"
4209 );
4210 }
4211
4212 #[test]
4213 fn signature_mode_require_valid_with_reference_digest_verifier() {
4214 let xml = build_signed_cpl_with_reference_digest(false);
4215 let verifier = ReferenceDigestXmlDsigVerifier;
4216 let options = CplParseOptions {
4217 signature_validation_mode: SignatureValidationMode::RequireValid,
4218 signature_verifier: Some(&verifier),
4219 ..Default::default()
4220 };
4221 let result = parse_cpl_with_options(&xml, &options);
4222 assert!(
4223 result.is_ok(),
4224 "expected valid signature digest path to parse: {result:?}"
4225 );
4226 }
4227
4228 #[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
4229 #[test]
4230 fn xmlsec_verifier_surfaces_missing_binary() {
4231 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
4232 let verifier = XmlSec1Verifier::new().with_binary_path("xmlsec1-definitely-not-installed");
4233 let error = verifier
4234 .verify(&xml)
4235 .expect_err("expected missing binary error");
4236 assert!(
4237 error.contains("failed to execute"),
4238 "unexpected error message: {error}"
4239 );
4240 }
4241
4242 #[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
4243 #[test]
4244 fn xmlsec_crate_verifier_rejects_invalid_key_material() {
4245 let xml = build_signed_cpl_with_reference_digest(false);
4246 let verifier = XmlSecCrateVerifier::from_pem("not-a-valid-key");
4247 let error = verifier
4248 .verify(&xml)
4249 .expect_err("expected xmlsec key load error");
4250 assert!(
4251 error.contains("xmlsec key load failed") || error.contains("xmlsec verify failed"),
4252 "unexpected error message: {error}"
4253 );
4254 }
4255}