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"))]
1217 #[cfg_attr(feature = "wasm", serde(rename = "locales", alias = "Locale"))]
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
2143#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2145#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2146#[cfg_attr(feature = "typescript", derive(TS))]
2147#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2148#[cfg_attr(feature = "wasm", derive(Tsify))]
2149#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2150pub struct IABSoundfieldLabelSubDescriptor {
2151 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2152 pub instance_id: Option<String>,
2153
2154 #[serde(
2155 rename = "MCATagSymbol",
2156 default,
2157 deserialize_with = "de_helpers::de_optional_mca_tag_symbol"
2158 )]
2159 pub mca_tag_symbol: Option<McaTagSymbol>,
2160
2161 #[serde(rename = "MCATagName", default)]
2162 pub mca_tag_name: Option<String>,
2163
2164 #[serde(rename = "MCALabelDictionaryID", default)]
2166 pub mca_label_dictionary_id: Option<String>,
2167
2168 #[serde(
2169 rename = "RFC5646SpokenLanguage",
2170 alias = "RFC5646AudioLanguageCode",
2171 default,
2172 deserialize_with = "de_helpers::de_optional_language_tag"
2173 )]
2174 pub rfc5646_spoken_language: Option<LanguageTag>,
2175}
2176
2177#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2179#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
2180#[cfg_attr(feature = "typescript", derive(TS))]
2181#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2182#[cfg_attr(feature = "wasm", derive(Tsify))]
2183#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2184pub struct IsxdSubDescriptors {
2185 #[serde(rename = "ContainerConstraintsSubDescriptor", default)]
2187 pub container_constraints_sub_descriptor: Option<ContainerConstraintsSubDescriptor>,
2188}
2189
2190#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2192#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2193#[cfg_attr(feature = "typescript", derive(TS))]
2194#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2195#[cfg_attr(feature = "wasm", derive(Tsify))]
2196#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2197pub struct ContainerConstraintsSubDescriptor {
2198 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2199 pub instance_id: Option<String>,
2200}
2201
2202#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2204#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2205#[cfg_attr(feature = "typescript", derive(TS))]
2206#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2207#[cfg_attr(feature = "wasm", derive(Tsify))]
2208#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2209pub struct ISXDDataEssenceDescriptor {
2210 #[serde(rename = "InstanceID", alias = "InstanceUID", default)]
2211 pub instance_id: Option<String>,
2212
2213 #[serde(rename = "LinkedTrackID", default)]
2214 pub linked_track_id: Option<u32>,
2215
2216 #[serde(
2217 rename = "SampleRate",
2218 default,
2219 deserialize_with = "de_helpers::de_optional_edit_rate"
2220 )]
2221 pub sample_rate: Option<EditRate>,
2222
2223 #[serde(rename = "DataEssenceCoding", default)]
2224 pub data_essence_coding: Option<String>,
2225
2226 #[serde(rename = "NamespaceURI", default)]
2227 pub namespace_uri: Option<String>,
2228
2229 #[serde(rename = "SubDescriptors", default)]
2230 pub sub_descriptors: Option<IsxdSubDescriptors>,
2231}
2232
2233#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2239#[derive(Debug, Serialize, Deserialize, PartialEq)]
2240#[cfg_attr(feature = "typescript", derive(TS))]
2241#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2242#[cfg_attr(feature = "wasm", derive(Tsify))]
2243#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2244pub struct CompositionPlaylist {
2245 #[serde(skip)]
2248 pub namespace: CplNamespace,
2249
2250 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2251 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2252 #[cfg_attr(feature = "typescript", ts(rename = "id"))]
2253 pub id: ImfUuid,
2254
2255 #[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
2256 #[cfg_attr(
2257 feature = "wasm",
2258 serde(rename = "annotation", alias = "Annotation", default)
2259 )]
2260 #[cfg_attr(feature = "typescript", ts(rename = "annotation"))]
2261 pub annotation: Option<LanguageString>,
2262
2263 #[cfg_attr(not(feature = "wasm"), serde(rename = "IssueDate"))]
2264 #[cfg_attr(feature = "wasm", serde(rename = "issueDate", alias = "IssueDate"))]
2265 #[cfg_attr(feature = "typescript", ts(rename = "issueDate"))]
2266 pub issue_date: String, #[cfg_attr(not(feature = "wasm"), serde(rename = "Issuer", default))]
2269 #[cfg_attr(feature = "wasm", serde(rename = "issuer", alias = "Issuer", default))]
2270 #[cfg_attr(feature = "typescript", ts(rename = "issuer"))]
2271 pub issuer: Option<LanguageString>,
2272
2273 #[cfg_attr(not(feature = "wasm"), serde(rename = "Creator", default))]
2274 #[cfg_attr(
2275 feature = "wasm",
2276 serde(rename = "creator", alias = "Creator", default)
2277 )]
2278 #[cfg_attr(feature = "typescript", ts(rename = "creator"))]
2279 pub creator: Option<LanguageString>,
2280
2281 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentOriginator", default))]
2282 #[cfg_attr(
2283 feature = "wasm",
2284 serde(rename = "contentOriginator", alias = "ContentOriginator", default)
2285 )]
2286 #[cfg_attr(feature = "typescript", ts(rename = "contentOriginator"))]
2287 pub content_originator: Option<LanguageString>,
2288
2289 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentTitle"))]
2290 #[cfg_attr(
2291 feature = "wasm",
2292 serde(rename = "contentTitle", alias = "ContentTitle")
2293 )]
2294 #[cfg_attr(feature = "typescript", ts(rename = "contentTitle"))]
2295 pub content_title: LanguageString,
2296
2297 #[cfg_attr(
2298 not(feature = "wasm"),
2299 serde(rename = "ContentKind", default = "default_content_kind")
2300 )]
2301 #[cfg_attr(
2302 feature = "wasm",
2303 serde(
2304 rename = "contentKind",
2305 alias = "ContentKind",
2306 default = "default_content_kind"
2307 )
2308 )]
2309 #[cfg_attr(feature = "typescript", ts(rename = "contentKind"))]
2310 pub content_kind: ContentKindElement,
2311
2312 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentVersionList", default))]
2313 #[cfg_attr(
2314 feature = "wasm",
2315 serde(rename = "contentVersionList", alias = "ContentVersionList", default)
2316 )]
2317 #[cfg_attr(feature = "typescript", ts(rename = "contentVersionList"))]
2318 pub content_version_list: Option<ContentVersionList>,
2319
2320 #[cfg_attr(
2321 not(feature = "wasm"),
2322 serde(rename = "EssenceDescriptorList", default)
2323 )]
2324 #[cfg_attr(
2325 feature = "wasm",
2326 serde(
2327 rename = "essenceDescriptorList",
2328 alias = "EssenceDescriptorList",
2329 default
2330 )
2331 )]
2332 #[cfg_attr(feature = "typescript", ts(rename = "essenceDescriptorList"))]
2333 pub essence_descriptor_list: Option<EssenceDescriptorList>,
2334
2335 #[cfg_attr(
2336 not(feature = "wasm"),
2337 serde(
2338 rename = "EditRate",
2339 default,
2340 deserialize_with = "de_helpers::de_optional_edit_rate"
2341 )
2342 )]
2343 #[cfg_attr(
2344 feature = "wasm",
2345 serde(
2346 rename = "editRate",
2347 alias = "EditRate",
2348 default,
2349 deserialize_with = "de_helpers::de_optional_edit_rate"
2350 )
2351 )]
2352 #[cfg_attr(feature = "typescript", ts(rename = "editRate"))]
2353 pub edit_rate: Option<EditRate>,
2354
2355 #[cfg_attr(not(feature = "wasm"), serde(rename = "TotalRunningTime", default))]
2356 #[cfg_attr(
2357 feature = "wasm",
2358 serde(rename = "totalRunningTime", alias = "TotalRunningTime", default)
2359 )]
2360 #[cfg_attr(feature = "typescript", ts(rename = "totalRunningTime"))]
2361 pub total_running_time: Option<String>,
2362
2363 #[cfg_attr(not(feature = "wasm"), serde(rename = "LocaleList", default))]
2364 #[cfg_attr(
2365 feature = "wasm",
2366 serde(rename = "localeList", alias = "LocaleList", default)
2367 )]
2368 #[cfg_attr(feature = "typescript", ts(rename = "localeList"))]
2369 pub locale_list: Option<LocaleList>,
2370
2371 #[cfg_attr(not(feature = "wasm"), serde(rename = "ExtensionProperties", default))]
2372 #[cfg_attr(
2373 feature = "wasm",
2374 serde(rename = "extensionProperties", alias = "ExtensionProperties", default)
2375 )]
2376 #[cfg_attr(feature = "typescript", ts(rename = "extensionProperties"))]
2377 pub extension_properties: Option<ExtensionProperties>,
2378
2379 #[cfg_attr(not(feature = "wasm"), serde(rename = "CompositionTimecode", default))]
2380 #[cfg_attr(
2381 feature = "wasm",
2382 serde(rename = "compositionTimecode", alias = "CompositionTimecode", default)
2383 )]
2384 #[cfg_attr(feature = "typescript", ts(rename = "compositionTimecode"))]
2385 pub composition_timecode: Option<CompositionTimecode>,
2386
2387 #[serde(skip)]
2390 pub has_signer: bool,
2391
2392 #[serde(skip)]
2395 pub has_signature: bool,
2396
2397 #[cfg_attr(not(feature = "wasm"), serde(rename = "SegmentList"))]
2398 #[cfg_attr(feature = "wasm", serde(rename = "segmentList", alias = "SegmentList"))]
2399 #[cfg_attr(feature = "typescript", ts(rename = "segmentList"))]
2400 pub segment_list: SegmentList,
2401}
2402
2403#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2409#[cfg_attr(not(feature = "wasm"), serde(rename_all = "PascalCase"))]
2410#[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))]
2411#[cfg_attr(feature = "typescript", derive(TS))]
2412#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2413#[cfg_attr(feature = "wasm", derive(Tsify))]
2414#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2415pub struct CompositionTimecode {
2416 #[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeDropFrame"))]
2417 #[cfg_attr(
2418 feature = "wasm",
2419 serde(rename = "timecodeDropFrame", alias = "TimecodeDropFrame")
2420 )]
2421 pub timecode_drop_frame: Option<bool>,
2422
2423 #[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeRate"))]
2424 #[cfg_attr(
2425 feature = "wasm",
2426 serde(rename = "timecodeRate", alias = "TimecodeRate")
2427 )]
2428 pub timecode_rate: Option<u32>,
2429
2430 #[cfg_attr(not(feature = "wasm"), serde(rename = "TimecodeStartAddress"))]
2431 #[cfg_attr(
2432 feature = "wasm",
2433 serde(rename = "timecodeStartAddress", alias = "TimecodeStartAddress")
2434 )]
2435 pub timecode_start_address: Option<String>,
2436}
2437
2438#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2443#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2444#[cfg_attr(feature = "typescript", derive(TS))]
2445#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2446#[cfg_attr(feature = "wasm", derive(Tsify))]
2447#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2448pub struct ContentVersionList {
2449 #[cfg_attr(not(feature = "wasm"), serde(rename = "ContentVersion"))]
2450 #[cfg_attr(
2451 feature = "wasm",
2452 serde(rename = "contentVersions", alias = "ContentVersion")
2453 )]
2454 pub content_versions: Vec<ContentVersion>,
2455}
2456
2457#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2458#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2459#[cfg_attr(feature = "typescript", derive(TS))]
2460#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2461#[cfg_attr(feature = "wasm", derive(Tsify))]
2462#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2463pub struct ContentVersion {
2464 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2465 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2466 pub id: String,
2467
2468 #[cfg_attr(not(feature = "wasm"), serde(rename = "LabelText", default))]
2469 #[cfg_attr(
2470 feature = "wasm",
2471 serde(rename = "labelText", alias = "LabelText", default)
2472 )]
2473 pub label_text: Option<LanguageString>,
2474}
2475
2476#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2481#[derive(Debug, Serialize, Deserialize, PartialEq)]
2482#[cfg_attr(feature = "typescript", derive(TS))]
2483#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2484#[cfg_attr(feature = "wasm", derive(Tsify))]
2485#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2486pub struct SegmentList {
2487 #[cfg_attr(not(feature = "wasm"), serde(rename = "Segment"))]
2488 #[cfg_attr(feature = "wasm", serde(rename = "segments", alias = "Segment"))]
2489 pub segments: Vec<Segment>,
2490}
2491
2492#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2493#[derive(Debug, Serialize, Deserialize, PartialEq)]
2494#[cfg_attr(feature = "typescript", derive(TS))]
2495#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2496#[cfg_attr(feature = "wasm", derive(Tsify))]
2497#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2498pub struct Segment {
2499 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2500 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2501 pub id: ImfUuid,
2502
2503 #[cfg_attr(not(feature = "wasm"), serde(rename = "SequenceList"))]
2504 #[cfg_attr(
2505 feature = "wasm",
2506 serde(rename = "sequenceList", alias = "SequenceList")
2507 )]
2508 pub sequence_list: SequenceList,
2509}
2510
2511#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2512#[derive(Debug, Serialize, Deserialize, PartialEq)]
2513#[cfg_attr(feature = "typescript", derive(TS))]
2514#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2515#[cfg_attr(feature = "wasm", derive(Tsify))]
2516#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2517pub struct SequenceList {
2518 #[cfg_attr(not(feature = "wasm"), serde(rename = "MarkerSequence", default))]
2519 #[cfg_attr(
2520 feature = "wasm",
2521 serde(rename = "markerSequences", alias = "MarkerSequence", default)
2522 )]
2523 pub marker_sequences: Vec<MarkerSequence>,
2524
2525 #[cfg_attr(not(feature = "wasm"), serde(rename = "MainImageSequence", default))]
2526 #[cfg_attr(
2527 feature = "wasm",
2528 serde(rename = "mainImageSequences", alias = "MainImageSequence", default)
2529 )]
2530 pub main_image_sequences: Vec<MainImageSequence>,
2531
2532 #[cfg_attr(not(feature = "wasm"), serde(rename = "MainAudioSequence", default))]
2533 #[cfg_attr(
2534 feature = "wasm",
2535 serde(rename = "mainAudioSequences", alias = "MainAudioSequence", default)
2536 )]
2537 pub main_audio_sequences: Vec<MainAudioSequence>,
2538
2539 #[cfg_attr(
2540 not(feature = "wasm"),
2541 serde(rename = "SubtitlesSequence", alias = "MainSubtitleSequence", default)
2542 )]
2543 #[cfg_attr(
2544 feature = "wasm",
2545 serde(
2546 rename = "subtitlesSequences",
2547 alias = "SubtitlesSequence",
2548 alias = "MainSubtitleSequence",
2549 default
2550 )
2551 )]
2552 pub subtitles_sequences: Vec<SubtitlesSequence>,
2553
2554 #[cfg_attr(
2555 not(feature = "wasm"),
2556 serde(rename = "HearingImpairedCaptionsSequence", default)
2557 )]
2558 #[cfg_attr(
2559 feature = "wasm",
2560 serde(
2561 rename = "hearingImpairedCaptionsSequences",
2562 alias = "HearingImpairedCaptionsSequence",
2563 default
2564 )
2565 )]
2566 pub hearing_impaired_captions_sequences: Vec<HearingImpairedCaptionsSequence>,
2567
2568 #[cfg_attr(
2569 not(feature = "wasm"),
2570 serde(rename = "ForcedNarrativeSequence", default)
2571 )]
2572 #[cfg_attr(
2573 feature = "wasm",
2574 serde(
2575 rename = "forcedNarrativeSequences",
2576 alias = "ForcedNarrativeSequence",
2577 default
2578 )
2579 )]
2580 pub forced_narrative_sequences: Vec<ForcedNarrativeSequence>,
2581
2582 #[cfg_attr(not(feature = "wasm"), serde(rename = "IABSequence", default))]
2583 #[cfg_attr(
2584 feature = "wasm",
2585 serde(rename = "iabSequences", alias = "IABSequence", default)
2586 )]
2587 pub iab_sequences: Vec<IABSequence>,
2588
2589 #[cfg_attr(not(feature = "wasm"), serde(rename = "ISXDSequence", default))]
2590 #[cfg_attr(
2591 feature = "wasm",
2592 serde(rename = "isxdSequences", alias = "ISXDSequence", default)
2593 )]
2594 pub isxd_sequences: Vec<ISXDSequence>,
2595}
2596
2597impl SequenceList {
2598 pub fn all_sequences(&self) -> Vec<&dyn SequenceAccess> {
2600 let mut v: Vec<&dyn SequenceAccess> = Vec::new();
2601 for s in &self.main_image_sequences {
2602 v.push(s);
2603 }
2604 for s in &self.main_audio_sequences {
2605 v.push(s);
2606 }
2607 for s in &self.subtitles_sequences {
2608 v.push(s);
2609 }
2610 for s in &self.hearing_impaired_captions_sequences {
2611 v.push(s);
2612 }
2613 for s in &self.forced_narrative_sequences {
2614 v.push(s);
2615 }
2616 for s in &self.iab_sequences {
2617 v.push(s);
2618 }
2619 for s in &self.isxd_sequences {
2620 v.push(s);
2621 }
2622 v
2623 }
2624
2625 pub fn all_sequences_typed(&self) -> Vec<(&dyn SequenceAccess, &'static str)> {
2627 let mut v: Vec<(&dyn SequenceAccess, &'static str)> = Vec::new();
2628 for s in &self.main_image_sequences {
2629 v.push((s, "MainImage"));
2630 }
2631 for s in &self.main_audio_sequences {
2632 v.push((s, "MainAudio"));
2633 }
2634 for s in &self.subtitles_sequences {
2635 v.push((s, "Subtitles"));
2636 }
2637 for s in &self.hearing_impaired_captions_sequences {
2638 v.push((s, "HearingImpairedCaptions"));
2639 }
2640 for s in &self.forced_narrative_sequences {
2641 v.push((s, "ForcedNarrative"));
2642 }
2643 for s in &self.iab_sequences {
2644 v.push((s, "IAB"));
2645 }
2646 for s in &self.isxd_sequences {
2647 v.push((s, "ISXD"));
2648 }
2649 v
2650 }
2651}
2652
2653pub trait SequenceAccess {
2656 fn id(&self) -> &ImfUuid;
2657 fn track_id(&self) -> &ImfUuid;
2658 fn resource_list(&self) -> &ResourceList;
2659}
2660
2661macro_rules! define_sequence_type {
2662 ($name:ident) => {
2663 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2664 #[derive(Debug, Serialize, Deserialize, PartialEq)]
2665 #[cfg_attr(feature = "typescript", derive(TS))]
2666 #[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2667 #[cfg_attr(feature = "wasm", derive(Tsify))]
2668 #[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2669 pub struct $name {
2670 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2671 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2672 pub id: ImfUuid,
2673
2674 #[cfg_attr(not(feature = "wasm"), serde(rename = "TrackId"))]
2675 #[cfg_attr(feature = "wasm", serde(rename = "trackId", alias = "TrackId"))]
2676 pub track_id: ImfUuid,
2677
2678 #[cfg_attr(not(feature = "wasm"), serde(rename = "ResourceList"))]
2679 #[cfg_attr(
2680 feature = "wasm",
2681 serde(rename = "resourceList", alias = "ResourceList")
2682 )]
2683 pub resource_list: ResourceList,
2684 }
2685
2686 impl SequenceAccess for $name {
2687 fn id(&self) -> &ImfUuid {
2688 &self.id
2689 }
2690 fn track_id(&self) -> &ImfUuid {
2691 &self.track_id
2692 }
2693 fn resource_list(&self) -> &ResourceList {
2694 &self.resource_list
2695 }
2696 }
2697 };
2698}
2699
2700define_sequence_type!(MarkerSequence);
2701define_sequence_type!(MainImageSequence);
2702define_sequence_type!(MainAudioSequence);
2703define_sequence_type!(SubtitlesSequence);
2704define_sequence_type!(HearingImpairedCaptionsSequence);
2705define_sequence_type!(ForcedNarrativeSequence);
2706define_sequence_type!(IABSequence);
2707define_sequence_type!(ISXDSequence);
2708
2709#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2714#[derive(Debug, Serialize, Deserialize, PartialEq)]
2715#[cfg_attr(feature = "typescript", derive(TS))]
2716#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2717#[cfg_attr(feature = "wasm", derive(Tsify))]
2718#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2719pub struct ResourceList {
2720 #[cfg_attr(not(feature = "wasm"), serde(rename = "Resource", default))]
2721 #[cfg_attr(
2722 feature = "wasm",
2723 serde(rename = "resources", alias = "Resource", default)
2724 )]
2725 pub resources: Vec<Resource>,
2726}
2727
2728#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2729#[derive(Debug, Serialize, Deserialize, PartialEq)]
2730#[cfg_attr(feature = "typescript", derive(TS))]
2731#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2732#[cfg_attr(feature = "wasm", derive(Tsify))]
2733#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2734pub struct Resource {
2735 #[cfg_attr(not(feature = "wasm"), serde(rename = "Id"))]
2736 #[cfg_attr(feature = "wasm", serde(rename = "id", alias = "Id"))]
2737 pub id: ImfUuid,
2738
2739 #[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
2740 #[cfg_attr(
2741 feature = "wasm",
2742 serde(rename = "annotation", alias = "Annotation", default)
2743 )]
2744 pub annotation: Option<LanguageString>,
2745
2746 #[cfg_attr(
2747 not(feature = "wasm"),
2748 serde(
2749 rename = "EditRate",
2750 default,
2751 deserialize_with = "de_helpers::de_optional_edit_rate"
2752 )
2753 )]
2754 #[cfg_attr(
2755 feature = "wasm",
2756 serde(
2757 rename = "editRate",
2758 alias = "EditRate",
2759 default,
2760 deserialize_with = "de_helpers::de_optional_edit_rate"
2761 )
2762 )]
2763 pub edit_rate: Option<EditRate>,
2764
2765 #[cfg_attr(not(feature = "wasm"), serde(rename = "IntrinsicDuration"))]
2766 #[cfg_attr(
2767 feature = "wasm",
2768 serde(rename = "intrinsicDuration", alias = "IntrinsicDuration")
2769 )]
2770 pub intrinsic_duration: u64,
2771
2772 #[cfg_attr(not(feature = "wasm"), serde(rename = "EntryPoint", default))]
2773 #[cfg_attr(
2774 feature = "wasm",
2775 serde(rename = "entryPoint", alias = "EntryPoint", default)
2776 )]
2777 pub entry_point: Option<u64>,
2778
2779 #[cfg_attr(not(feature = "wasm"), serde(rename = "SourceDuration", default))]
2780 #[cfg_attr(
2781 feature = "wasm",
2782 serde(rename = "sourceDuration", alias = "SourceDuration", default)
2783 )]
2784 pub source_duration: Option<u64>,
2785
2786 #[cfg_attr(not(feature = "wasm"), serde(rename = "SourceEncoding", default))]
2787 #[cfg_attr(
2788 feature = "wasm",
2789 serde(rename = "sourceEncoding", alias = "SourceEncoding", default)
2790 )]
2791 pub source_encoding: Option<ImfUuid>, #[cfg_attr(not(feature = "wasm"), serde(rename = "TrackFileId", default))]
2794 #[cfg_attr(
2795 feature = "wasm",
2796 serde(rename = "trackFileId", alias = "TrackFileId", default)
2797 )]
2798 pub track_file_id: Option<ImfUuid>, #[cfg_attr(not(feature = "wasm"), serde(rename = "RepeatCount", default))]
2801 #[cfg_attr(
2802 feature = "wasm",
2803 serde(rename = "repeatCount", alias = "RepeatCount", default)
2804 )]
2805 pub repeat_count: Option<u64>,
2806
2807 #[cfg_attr(not(feature = "wasm"), serde(rename = "KeyId", default))]
2808 #[cfg_attr(feature = "wasm", serde(rename = "keyId", alias = "KeyId", default))]
2809 pub key_id: Option<ImfUuid>, #[cfg_attr(not(feature = "wasm"), serde(rename = "Hash", default))]
2812 #[cfg_attr(feature = "wasm", serde(rename = "hash", alias = "Hash", default))]
2813 pub hash: Option<String>,
2814
2815 #[cfg_attr(not(feature = "wasm"), serde(rename = "Marker", default))]
2816 #[cfg_attr(feature = "wasm", serde(rename = "markers", alias = "Marker", default))]
2817 pub markers: Vec<MarkerInfo>,
2818}
2819
2820#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2821#[derive(Debug, Serialize, Deserialize, PartialEq)]
2822#[cfg_attr(feature = "typescript", derive(TS))]
2823#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2824#[cfg_attr(feature = "wasm", derive(Tsify))]
2825#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2826pub struct MarkerInfo {
2827 #[cfg_attr(not(feature = "wasm"), serde(rename = "Annotation", default))]
2828 #[cfg_attr(
2829 feature = "wasm",
2830 serde(rename = "annotation", alias = "Annotation", default)
2831 )]
2832 pub annotation: Option<String>,
2833
2834 #[cfg_attr(not(feature = "wasm"), serde(rename = "Label"))]
2835 #[cfg_attr(feature = "wasm", serde(rename = "label", alias = "Label"))]
2836 pub label: MarkerLabelElement,
2837
2838 #[cfg_attr(not(feature = "wasm"), serde(rename = "Offset"))]
2839 #[cfg_attr(feature = "wasm", serde(rename = "offset", alias = "Offset"))]
2840 pub offset: u64,
2841}
2842
2843#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
2849#[derive(Debug, Clone, Serialize, Deserialize)]
2850#[cfg_attr(feature = "typescript", derive(TS))]
2851#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
2852#[cfg_attr(feature = "wasm", derive(Tsify))]
2853#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
2854pub struct TrackInfo {
2855 pub track_id: String,
2856 pub track_type: String, pub codec: String,
2858 pub language: Option<String>,
2859 pub channels: Option<String>,
2860 pub format_details: Option<String>,
2861 pub resolution: Option<String>,
2862 pub framerate: Option<String>,
2863 pub bit_depth: Option<String>,
2864 pub subtitle_type: Option<String>,
2865}
2866
2867pub fn parse_cpl(xml_content: &str) -> Result<CompositionPlaylist, CplParseError> {
2877 parse_cpl_with_options(xml_content, &CplParseOptions::default())
2878}
2879
2880pub fn parse_cpl_with_options(
2882 xml_content: &str,
2883 options: &CplParseOptions<'_>,
2884) -> Result<CompositionPlaylist, CplParseError> {
2885 let namespace = crate::assetmap::detect_root_namespace(xml_content)
2888 .map(|uri| CplNamespace::from_uri(&uri))
2889 .unwrap_or_default();
2890
2891 let has_signer = xml_content.contains("<Signer") || xml_content.contains(":Signer");
2893 let has_signature = xml_content.contains("<Signature") || xml_content.contains(":Signature");
2894
2895 match options.signature_validation_mode {
2896 SignatureValidationMode::Ignore => {}
2897 SignatureValidationMode::RequirePresence => {
2898 if !has_signature {
2899 return Err(CplParseError::StrictSchema(
2900 "Signature element is required by selected signature mode".to_string(),
2901 ));
2902 }
2903 }
2904 SignatureValidationMode::VerifyIfPresent => {
2905 if has_signature {
2906 let verifier = options
2907 .signature_verifier
2908 .ok_or(CplParseError::SignatureVerifierRequired)?;
2909 verifier
2910 .verify(xml_content)
2911 .map_err(CplParseError::SignatureVerificationFailed)?;
2912 }
2913 }
2914 SignatureValidationMode::RequireValid => {
2915 if !has_signature {
2916 return Err(CplParseError::StrictSchema(
2917 "Signature element is required by selected signature mode".to_string(),
2918 ));
2919 }
2920 let verifier = options
2921 .signature_verifier
2922 .ok_or(CplParseError::SignatureVerifierRequired)?;
2923 verifier
2924 .verify(xml_content)
2925 .map_err(CplParseError::SignatureVerificationFailed)?;
2926 }
2927 }
2928
2929 let stripped = strip_xml_namespaces(xml_content);
2930
2931 if options.unknown_field_mode == UnknownFieldMode::Error {
2932 let unknown = collect_unknown_xml_tokens(&stripped).map_err(|e| {
2933 CplParseError::StrictUnknownXml(format!("unknown token scan failed: {}", e))
2934 })?;
2935 if !unknown.is_empty() {
2936 let list = unknown.into_iter().collect::<Vec<_>>().join(", ");
2937 return Err(CplParseError::StrictUnknownXml(list));
2938 }
2939 }
2940
2941 let mut cpl: CompositionPlaylist = quick_xml::de::from_str(&stripped)?;
2942
2943 if options.schema_strict_mode == SchemaStrictMode::Basic {
2944 validate_basic_schema_constraints(&cpl)?;
2945 }
2946
2947 cpl.namespace = namespace;
2948 cpl.has_signer = has_signer;
2949 cpl.has_signature = has_signature;
2950 Ok(cpl)
2951}
2952
2953fn validate_basic_schema_constraints(cpl: &CompositionPlaylist) -> Result<(), CplParseError> {
2954 if cpl.segment_list.segments.is_empty() {
2955 return Err(CplParseError::StrictSchema(
2956 "SegmentList must contain at least one Segment".to_string(),
2957 ));
2958 }
2959
2960 for (segment_index, segment) in cpl.segment_list.segments.iter().enumerate() {
2961 let sequence_count = segment.sequence_list.marker_sequences.len()
2962 + segment.sequence_list.main_image_sequences.len()
2963 + segment.sequence_list.main_audio_sequences.len()
2964 + segment.sequence_list.subtitles_sequences.len()
2965 + segment
2966 .sequence_list
2967 .hearing_impaired_captions_sequences
2968 .len()
2969 + segment.sequence_list.forced_narrative_sequences.len()
2970 + segment.sequence_list.iab_sequences.len()
2971 + segment.sequence_list.isxd_sequences.len();
2972
2973 if sequence_count == 0 {
2974 return Err(CplParseError::StrictSchema(format!(
2975 "Segment[{}] must contain at least one sequence",
2976 segment_index
2977 )));
2978 }
2979 }
2980
2981 Ok(())
2982}
2983
2984fn collect_unknown_xml_tokens(xml: &str) -> Result<BTreeSet<String>, String> {
2985 let mut reader = quick_xml::Reader::from_str(xml);
2986 reader.trim_text(true);
2987
2988 let allowed_elements: BTreeSet<&'static str> = [
2989 "CompositionPlaylist",
2990 "Id",
2991 "Annotation",
2992 "IssueDate",
2993 "Issuer",
2994 "Creator",
2995 "ContentOriginator",
2996 "ContentTitle",
2997 "ContentKind",
2998 "ContentVersionList",
2999 "ContentVersion",
3000 "LabelText",
3001 "EssenceDescriptorList",
3002 "EssenceDescriptor",
3003 "EditRate",
3004 "TotalRunningTime",
3005 "LocaleList",
3006 "Locale",
3007 "LanguageList",
3008 "Language",
3009 "RegionList",
3010 "Region",
3011 "ContentMaturityRatingList",
3012 "ContentMaturityRating",
3013 "Agency",
3014 "Rating",
3015 "Audience",
3016 "ExtensionProperties",
3017 "ApplicationIdentification",
3018 "MaxCLL",
3019 "MaxFALL",
3020 "CompositionTimecode",
3021 "TimecodeDropFrame",
3022 "TimecodeRate",
3023 "TimecodeStartAddress",
3024 "SegmentList",
3025 "Segment",
3026 "SequenceList",
3027 "MarkerSequence",
3028 "MainImageSequence",
3029 "MainAudioSequence",
3030 "SubtitlesSequence",
3031 "MainSubtitleSequence",
3032 "HearingImpairedCaptionsSequence",
3033 "ForcedNarrativeSequence",
3034 "IABSequence",
3035 "ISXDSequence",
3036 "TrackId",
3037 "ResourceList",
3038 "Resource",
3039 "IntrinsicDuration",
3040 "EntryPoint",
3041 "SourceDuration",
3042 "SourceEncoding",
3043 "TrackFileId",
3044 "RepeatCount",
3045 "KeyId",
3046 "Hash",
3047 "Marker",
3048 "Label",
3049 "Offset",
3050 "RGBADescriptor",
3051 "CDCIDescriptor",
3052 "WAVEPCMDescriptor",
3053 "DCTimedTextDescriptor",
3054 "IABEssenceDescriptor",
3055 "ISXDDataEssenceDescriptor",
3056 "InstanceID",
3057 "InstanceUID",
3058 "DisplayWidth",
3059 "DisplayHeight",
3060 "StoredWidth",
3061 "StoredHeight",
3062 "SampleRate",
3063 "ImageAspectRatio",
3064 "ColorPrimaries",
3065 "TransferCharacteristic",
3066 "CodingEquations",
3067 "PictureCompression",
3068 "FrameLayout",
3069 "DisplayF2Offset",
3070 "ComponentMaxRef",
3071 "ComponentMinRef",
3072 "ScanningDirection",
3073 "StoredF2Offset",
3074 "SampledWidth",
3075 "SampledHeight",
3076 "SampledXOffset",
3077 "SampledYOffset",
3078 "AlphaTransparency",
3079 "ImageAlignmentOffset",
3080 "ImageStartOffset",
3081 "ImageEndOffset",
3082 "FieldDominance",
3083 "AlphaMaxRef",
3084 "AlphaMinRef",
3085 "Palette",
3086 "PaletteLayout",
3087 "LinkedTrackID",
3088 "SubDescriptors",
3089 "ActiveWidth",
3090 "ActiveHeight",
3091 "ComponentDepth",
3092 "HorizontalSubsampling",
3093 "VerticalSubsampling",
3094 "ColorSiting",
3095 "BlackRefLevel",
3096 "WhiteRefLevel",
3097 "ColorRange",
3098 "ReversedByteOrder",
3099 "PaddingBits",
3100 "AlphaSampleDepth",
3101 "PHDRMetadataTrackSubDescriptor",
3102 "JPEG2000SubDescriptor",
3103 "Rsiz",
3104 "Xsiz",
3105 "Ysiz",
3106 "XOsiz",
3107 "YOsiz",
3108 "XTsiz",
3109 "YTsiz",
3110 "XTOsiz",
3111 "YTOsiz",
3112 "Csiz",
3113 "CodingStyleDefault",
3114 "QuantizationDefault",
3115 "J2CLayout",
3116 "J2KExtendedCapabilities",
3117 "PictureComponentSizing",
3118 "RGBAComponent",
3119 "Code",
3120 "ComponentSize",
3121 "Pcap",
3122 "J2KComponentSizing",
3123 "Ssiz",
3124 "XRSiz",
3125 "YRSiz",
3126 "PHDRMetadataTrackSubDescriptor_DataDefinition",
3127 "PHDRMetadataTrackSubDescriptor_SimplePayloadSID",
3128 "PHDRMetadataTrackSubDescriptor_SourceTrackID",
3129 "AudioSampleRate",
3130 "ChannelCount",
3131 "QuantizationBits",
3132 "SoundfieldGroupLabelSubDescriptor",
3133 "MCATagSymbol",
3134 "MCATagName",
3135 "MCAAudioContentKind",
3136 "RFC5646SpokenLanguage",
3137 "RFC5646AudioLanguageCode",
3138 "RFC5646LanguageTagList",
3139 "NamespaceURI",
3140 "SoundCompression",
3141 "IABSoundfieldLabelSubDescriptor",
3142 "ContainerFormat",
3143 "Codec",
3144 "ElectrospatialFormulation",
3145 "MCALabelDictionaryID",
3146 "EssenceLength",
3147 "Locked",
3148 "MCALinkID",
3149 "MCAChannelID",
3150 "AudioChannelLabelSubDescriptor",
3151 "MCATitle",
3152 "MCATitleVersion",
3153 "MCAAudioElementKind",
3154 "SoundfieldGroupLinkID",
3155 "DataEssenceCoding",
3156 "ContainerConstraintsSubDescriptor",
3157 "Signer",
3158 "Signature",
3159 ]
3160 .into_iter()
3161 .collect();
3162
3163 let allowed_attributes: BTreeSet<&'static str> =
3164 ["xmlns", "scope", "language"].into_iter().collect();
3165
3166 let mut unknown = BTreeSet::new();
3167
3168 loop {
3169 match reader.read_event() {
3170 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
3171 let name = std::str::from_utf8(e.name().as_ref())
3172 .map_err(|e| e.to_string())?
3173 .to_string();
3174 if !allowed_elements.contains(name.as_str()) {
3175 unknown.insert(format!("element:{}", name));
3176 }
3177 for attr in e.attributes() {
3178 let attr = attr.map_err(|e| e.to_string())?;
3179 let key = std::str::from_utf8(attr.key.as_ref())
3180 .map_err(|e| e.to_string())?
3181 .to_string();
3182 if !(allowed_attributes.contains(key.as_str()) || key.starts_with("xmlns:")) {
3183 unknown.insert(format!("attribute:{}@{}", key, name));
3184 }
3185 }
3186 }
3187 Ok(Event::Eof) => break,
3188 Ok(_) => {}
3189 Err(e) => return Err(e.to_string()),
3190 }
3191 }
3192
3193 Ok(unknown)
3194}
3195
3196pub fn extract_cpl_languages(cpl: &CompositionPlaylist) -> Vec<LanguageTag> {
3198 let mut languages: Vec<LanguageTag> = Vec::new();
3199
3200 let add_lang = |languages: &mut Vec<LanguageTag>, lang_opt: &Option<LanguageTag>| {
3201 if let Some(lang) = lang_opt {
3202 if !lang.as_str().is_empty() && !languages.contains(lang) {
3203 languages.push(lang.clone());
3204 }
3205 }
3206 };
3207
3208 let add_lang_string = |languages: &mut Vec<LanguageTag>,
3209 lang_string: &Option<LanguageString>| {
3210 if let Some(ls) = lang_string {
3211 add_lang(languages, &ls.language);
3212 }
3213 };
3214
3215 let add_required_lang_string =
3216 |languages: &mut Vec<LanguageTag>, lang_string: &LanguageString| {
3217 add_lang(languages, &lang_string.language);
3218 };
3219
3220 add_lang_string(&mut languages, &cpl.annotation);
3222 add_lang_string(&mut languages, &cpl.issuer);
3223 add_lang_string(&mut languages, &cpl.creator);
3224 add_lang_string(&mut languages, &cpl.content_originator);
3225 add_required_lang_string(&mut languages, &cpl.content_title);
3226
3227 if let Some(content_version_list) = &cpl.content_version_list {
3229 for version in &content_version_list.content_versions {
3230 add_lang_string(&mut languages, &version.label_text);
3231 }
3232 }
3233
3234 if let Some(locale_list) = &cpl.locale_list {
3236 for locale in &locale_list.locales {
3237 if let Some(language_list) = &locale.language_list {
3238 for lang in &language_list.languages {
3239 if !lang.as_str().is_empty() && !languages.contains(lang) {
3240 languages.push(lang.clone());
3241 }
3242 }
3243 }
3244 }
3245 }
3246
3247 if let Some(edl) = &cpl.essence_descriptor_list {
3249 for ed in &edl.essence_descriptors {
3250 if let Some(wave) = &ed.wave_pcm_descriptor {
3252 if let Some(subs) = &wave.sub_descriptors {
3253 if let Some(sf) = &subs.soundfield_group_label_sub_descriptor {
3254 add_lang(&mut languages, &sf.rfc5646_spoken_language);
3255 }
3256 }
3257 }
3258 if let Some(iab) = &ed.iab_essence_descriptor {
3260 if let Some(subs) = &iab.sub_descriptors {
3261 if let Some(sf) = &subs.iab_soundfield_label_sub_descriptor {
3262 add_lang(&mut languages, &sf.rfc5646_spoken_language);
3263 }
3264 }
3265 }
3266 if let Some(tt) = &ed.dc_timed_text_descriptor {
3268 for lang in &tt.rfc5646_language_tag_list {
3269 if !lang.as_str().is_empty() && !languages.contains(lang) {
3270 languages.push(lang.clone());
3271 }
3272 }
3273 }
3274 }
3275 }
3276
3277 languages.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3278 languages.dedup();
3279 languages
3280}
3281
3282pub fn try_extract_cpl_track_codecs_from_xml(
3284 xml_content: &str,
3285) -> Result<Vec<TrackInfo>, CplParseError> {
3286 let cpl = parse_cpl(xml_content)?;
3287 Ok(extract_tracks_from_cpl(&cpl, xml_content))
3288}
3289
3290pub fn extract_cpl_track_codecs_from_xml(xml_content: &str) -> Vec<TrackInfo> {
3295 try_extract_cpl_track_codecs_from_xml(xml_content).unwrap_or_default()
3296}
3297
3298fn extract_tracks_from_cpl(cpl: &CompositionPlaylist, _raw_xml: &str) -> Vec<TrackInfo> {
3300 let mut tracks = Vec::new();
3301
3302 let descriptors: std::collections::HashMap<ImfUuid, &EssenceDescriptor> =
3304 if let Some(edl) = &cpl.essence_descriptor_list {
3305 edl.essence_descriptors
3306 .iter()
3307 .map(|ed| (ed.id, ed))
3308 .collect()
3309 } else {
3310 std::collections::HashMap::new()
3311 };
3312
3313 for segment in &cpl.segment_list.segments {
3314 let seq_list = &segment.sequence_list;
3315
3316 for seq in &seq_list.main_image_sequences {
3318 for resource in &seq.resource_list.resources {
3319 if let Some(source_encoding) = &resource.source_encoding {
3320 if let Some(ed) = descriptors.get(source_encoding) {
3321 let (codec, resolution, bit_depth) = extract_video_info_from_descriptor(ed);
3322 let framerate = resource
3323 .edit_rate
3324 .as_ref()
3325 .or(cpl.edit_rate.as_ref())
3326 .map(format_framerate);
3327 tracks.push(TrackInfo {
3328 track_id: seq.track_id.to_string(),
3329 track_type: "video".to_string(),
3330 codec,
3331 language: None,
3332 channels: None,
3333 format_details: None,
3334 resolution,
3335 framerate,
3336 bit_depth,
3337 subtitle_type: None,
3338 });
3339 }
3340 }
3341 }
3342 }
3343
3344 for seq in &seq_list.main_audio_sequences {
3346 for resource in &seq.resource_list.resources {
3347 if let Some(source_encoding) = &resource.source_encoding {
3348 if let Some(ed) = descriptors.get(source_encoding) {
3349 let (codec, channels, format_details, language) =
3350 extract_audio_info_from_descriptor(ed);
3351 tracks.push(TrackInfo {
3352 track_id: seq.track_id.to_string(),
3353 track_type: "audio".to_string(),
3354 codec,
3355 language,
3356 channels,
3357 format_details,
3358 resolution: None,
3359 framerate: None,
3360 bit_depth: None,
3361 subtitle_type: None,
3362 });
3363 }
3364 }
3365 }
3366 }
3367
3368 for seq in &seq_list.iab_sequences {
3370 for resource in &seq.resource_list.resources {
3371 if let Some(source_encoding) = &resource.source_encoding {
3372 if let Some(ed) = descriptors.get(source_encoding) {
3373 let language = ed
3374 .iab_essence_descriptor
3375 .as_ref()
3376 .and_then(|iab| iab.sub_descriptors.as_ref())
3377 .and_then(|sd| sd.iab_soundfield_label_sub_descriptor.as_ref())
3378 .and_then(|sf| sf.rfc5646_spoken_language.as_ref())
3379 .map(|lt| lt.as_str().to_string());
3380 tracks.push(TrackInfo {
3381 track_id: seq.track_id.to_string(),
3382 track_type: "audio".to_string(),
3383 codec: "IAB (Dolby Atmos)".to_string(),
3384 language,
3385 channels: Some("Object-based".to_string()),
3386 format_details: Some("Immersive Audio".to_string()),
3387 resolution: None,
3388 framerate: None,
3389 bit_depth: None,
3390 subtitle_type: None,
3391 });
3392 }
3393 }
3394 }
3395 }
3396
3397 let subtitle_sequences: Vec<(&str, &[SubtitlesSequence])> = vec![
3399 ];
3401 let _ = subtitle_sequences; for seq in &seq_list.subtitles_sequences {
3404 if let Some(track) =
3405 extract_timed_text_track(seq.track_id, "standard", &seq.resource_list, &descriptors)
3406 {
3407 tracks.push(track);
3408 }
3409 }
3410 for seq in &seq_list.hearing_impaired_captions_sequences {
3411 if let Some(track) =
3412 extract_timed_text_track(seq.track_id, "hi", &seq.resource_list, &descriptors)
3413 {
3414 tracks.push(track);
3415 }
3416 }
3417 for seq in &seq_list.forced_narrative_sequences {
3418 if let Some(track) =
3419 extract_timed_text_track(seq.track_id, "forced", &seq.resource_list, &descriptors)
3420 {
3421 tracks.push(track);
3422 }
3423 }
3424 }
3425
3426 tracks
3427}
3428
3429fn extract_video_info_from_descriptor(
3430 ed: &EssenceDescriptor,
3431) -> (String, Option<String>, Option<String>) {
3432 if let Some(rgba) = &ed.rgba_descriptor {
3433 let width = rgba.display_width.or(rgba.stored_width);
3434 let height = rgba.display_height.or(rgba.stored_height);
3435 let resolution = match (width, height) {
3436 (Some(w), Some(h)) => Some(format!("{}x{}", w, h)),
3437 _ => None,
3438 };
3439 let codec = rgba
3440 .picture_compression
3441 .as_ref()
3442 .map(|c| c.to_string())
3443 .unwrap_or_else(|| "JPEG 2000".to_string());
3444 return (codec, resolution, None);
3445 }
3446 if let Some(cdci) = &ed.cdci_descriptor {
3447 let width = cdci
3448 .active_width
3449 .or(cdci.display_width)
3450 .or(cdci.stored_width);
3451 let height = cdci
3452 .active_height
3453 .or(cdci.display_height)
3454 .or(cdci.stored_height);
3455 let resolution = match (width, height) {
3456 (Some(w), Some(h)) => Some(format!("{}x{}", w, h)),
3457 _ => None,
3458 };
3459 let codec = cdci
3460 .picture_compression
3461 .as_ref()
3462 .map(|c| c.to_string())
3463 .unwrap_or_else(|| "JPEG 2000".to_string());
3464 let bit_depth = cdci.component_depth.map(|d| format!("{}-bit", d));
3465 return (codec, resolution, bit_depth);
3466 }
3467 ("Unknown".to_string(), None, None)
3468}
3469
3470fn extract_audio_info_from_descriptor(
3471 ed: &EssenceDescriptor,
3472) -> (String, Option<String>, Option<String>, Option<String>) {
3473 if let Some(wave) = &ed.wave_pcm_descriptor {
3474 let codec = wave
3475 .quantization_bits
3476 .map(|b| format!("PCM {}-bit", b))
3477 .unwrap_or_else(|| "PCM".to_string());
3478 let (channels, format_details) = match wave.channel_count {
3479 Some(1) => (Some("1.0".to_string()), Some("Mono".to_string())),
3480 Some(2) => (Some("2.0".to_string()), Some("Stereo".to_string())),
3481 Some(6) => (Some("5.1".to_string()), Some("Surround".to_string())),
3482 Some(8) => (Some("7.1".to_string()), Some("Surround".to_string())),
3483 Some(n) => (Some(format!("{}.0", n)), Some(format!("{} Channel", n))),
3484 None => (None, None),
3485 };
3486 let language = wave
3487 .sub_descriptors
3488 .as_ref()
3489 .and_then(|sd| sd.soundfield_group_label_sub_descriptor.as_ref())
3490 .and_then(|sf| sf.rfc5646_spoken_language.as_ref())
3491 .map(|lt| lt.as_str().to_string());
3492 return (codec, channels, format_details, language);
3493 }
3494 ("Unknown".to_string(), None, None, None)
3495}
3496
3497fn extract_timed_text_track(
3498 track_id: ImfUuid,
3499 subtitle_type: &str,
3500 resource_list: &ResourceList,
3501 descriptors: &std::collections::HashMap<ImfUuid, &EssenceDescriptor>,
3502) -> Option<TrackInfo> {
3503 for resource in &resource_list.resources {
3504 if let Some(source_encoding) = &resource.source_encoding {
3505 if let Some(ed) = descriptors.get(source_encoding) {
3506 let language = ed
3507 .dc_timed_text_descriptor
3508 .as_ref()
3509 .map(|tt| {
3510 tt.rfc5646_language_tag_list
3511 .iter()
3512 .map(|lt| lt.as_str())
3513 .filter(|s| !s.is_empty())
3514 .collect::<Vec<_>>()
3515 .join(",")
3516 })
3517 .filter(|s| !s.is_empty());
3518 return Some(TrackInfo {
3519 track_id: track_id.to_string(),
3520 track_type: "subtitle".to_string(),
3521 codec: "IMSC1 (Timed Text)".to_string(),
3522 language,
3523 channels: None,
3524 format_details: None,
3525 resolution: None,
3526 framerate: None,
3527 bit_depth: None,
3528 subtitle_type: Some(subtitle_type.to_string()),
3529 });
3530 }
3531 }
3532 }
3533 None
3534}
3535
3536pub fn format_framerate(edit_rate: &EditRate) -> String {
3537 let fps = edit_rate.as_f64();
3538 if (fps - 23.976).abs() < 0.01 {
3539 "23.976".to_string()
3540 } else if (fps - 29.97).abs() < 0.01 {
3541 "29.97".to_string()
3542 } else if (fps - 59.94).abs() < 0.01 {
3543 "59.94".to_string()
3544 } else if fps == fps.round() {
3545 format!("{}", fps as u32)
3546 } else {
3547 format!("{:.3}", fps)
3548 }
3549}
3550
3551#[cfg(test)]
3556mod tests {
3557 use super::*;
3558 use crate::assetmap::ImfUuid;
3559
3560 fn make_seq_list_with_all_types() -> SequenceList {
3561 let uuid = || ImfUuid::parse("urn:uuid:00000000-0000-0000-0000-000000000001").unwrap();
3562 let rl = || ResourceList { resources: vec![] };
3563 SequenceList {
3564 marker_sequences: vec![MarkerSequence {
3565 id: uuid(),
3566 track_id: uuid(),
3567 resource_list: rl(),
3568 }],
3569 main_image_sequences: vec![MainImageSequence {
3570 id: uuid(),
3571 track_id: uuid(),
3572 resource_list: rl(),
3573 }],
3574 main_audio_sequences: vec![MainAudioSequence {
3575 id: uuid(),
3576 track_id: uuid(),
3577 resource_list: rl(),
3578 }],
3579 subtitles_sequences: vec![SubtitlesSequence {
3580 id: uuid(),
3581 track_id: uuid(),
3582 resource_list: rl(),
3583 }],
3584 hearing_impaired_captions_sequences: vec![HearingImpairedCaptionsSequence {
3585 id: uuid(),
3586 track_id: uuid(),
3587 resource_list: rl(),
3588 }],
3589 forced_narrative_sequences: vec![ForcedNarrativeSequence {
3590 id: uuid(),
3591 track_id: uuid(),
3592 resource_list: rl(),
3593 }],
3594 iab_sequences: vec![IABSequence {
3595 id: uuid(),
3596 track_id: uuid(),
3597 resource_list: rl(),
3598 }],
3599 isxd_sequences: vec![ISXDSequence {
3600 id: uuid(),
3601 track_id: uuid(),
3602 resource_list: rl(),
3603 }],
3604 }
3605 }
3606
3607 #[test]
3608 fn all_sequences_excludes_markers() {
3609 let sl = make_seq_list_with_all_types();
3610 assert_eq!(sl.all_sequences().len(), 7);
3612 }
3613
3614 #[test]
3615 fn all_sequences_typed_returns_type_names() {
3616 let sl = make_seq_list_with_all_types();
3617 let typed = sl.all_sequences_typed();
3618 assert_eq!(typed.len(), 7);
3619 let names: Vec<&str> = typed.iter().map(|(_, n)| *n).collect();
3620 assert!(names.contains(&"MainImage"));
3621 assert!(names.contains(&"MainAudio"));
3622 assert!(names.contains(&"Subtitles"));
3623 assert!(names.contains(&"HearingImpairedCaptions"));
3624 assert!(names.contains(&"ForcedNarrative"));
3625 assert!(names.contains(&"IAB"));
3626 assert!(names.contains(&"ISXD"));
3627 }
3628
3629 #[test]
3630 fn all_sequences_empty_list() {
3631 let sl = SequenceList {
3632 marker_sequences: vec![],
3633 main_image_sequences: vec![],
3634 main_audio_sequences: vec![],
3635 subtitles_sequences: vec![],
3636 hearing_impaired_captions_sequences: vec![],
3637 forced_narrative_sequences: vec![],
3638 iab_sequences: vec![],
3639 isxd_sequences: vec![],
3640 };
3641 assert!(sl.all_sequences().is_empty());
3642 assert!(sl.all_sequences_typed().is_empty());
3643 }
3644
3645 #[test]
3646 fn try_extract_cpl_track_codecs_invalid_xml() {
3647 let result = try_extract_cpl_track_codecs_from_xml("<not-a-cpl/>");
3648 assert!(result.is_err());
3649 }
3650
3651 struct AcceptAllSignatureVerifier;
3652 impl XmlSignatureVerifier for AcceptAllSignatureVerifier {
3653 fn verify(&self, _xml_content: &str) -> Result<(), String> {
3654 Ok(())
3655 }
3656 }
3657
3658 struct RejectingSignatureVerifier;
3659 impl XmlSignatureVerifier for RejectingSignatureVerifier {
3660 fn verify(&self, _xml_content: &str) -> Result<(), String> {
3661 Err("bad signature".to_string())
3662 }
3663 }
3664
3665 #[test]
3666 fn strict_production_options_enable_all_strict_checks() {
3667 let verifier = ReferenceDigestXmlDsigVerifier;
3668 let options = strict_production_parse_options(&verifier);
3669 assert_eq!(options.unknown_field_mode, UnknownFieldMode::Error);
3670 assert_eq!(options.schema_strict_mode, SchemaStrictMode::Basic);
3671 assert_eq!(
3672 options.signature_validation_mode,
3673 SignatureValidationMode::RequireValid
3674 );
3675 assert!(options.signature_verifier.is_some());
3676 }
3677
3678 #[test]
3679 fn recommended_signature_verifier_rejects_unsigned_xml() {
3680 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
3681 let verifier = recommended_signature_verifier();
3682 assert!(verifier.verify(&xml).is_err());
3683 }
3684
3685 #[test]
3686 fn test_strip_xml_namespaces() {
3687 let input = r#"<r0:RGBADescriptor xmlns:r0="http://example.com"><r1:DisplayWidth>3840</r1:DisplayWidth></r0:RGBADescriptor>"#;
3688 let result = strip_xml_namespaces(input);
3689 assert!(result.contains("<RGBADescriptor"));
3690 assert!(result.contains("<DisplayWidth>3840</DisplayWidth>"));
3691 assert!(result.contains("</RGBADescriptor>"));
3692 assert!(!result.contains("xmlns:r0"));
3693 }
3694
3695 #[test]
3696 fn test_strip_preserves_content_with_colons() {
3697 let input = r#"<PictureCompression>urn:smpte:ul:060e2b34</PictureCompression>"#;
3698 let result = strip_xml_namespaces(input);
3699 assert_eq!(result, input); }
3701
3702 #[test]
3703 fn test_parse_simple_cpl() {
3704 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3705<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3706<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3707<Annotation>Test CPL</Annotation>
3708<IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
3709<ContentTitle>Test Content</ContentTitle>
3710<ContentKind>Test</ContentKind>
3711<SegmentList>
3712<Segment>
3713<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3714<SequenceList>
3715</SequenceList>
3716</Segment>
3717</SegmentList>
3718</CompositionPlaylist>"#;
3719
3720 let result = parse_cpl(xml);
3721 match result {
3722 Ok(cpl) => {
3723 assert_eq!(
3724 cpl.id,
3725 ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap()
3726 );
3727 assert_eq!(cpl.content_title.text, "Test Content");
3728 assert_eq!(cpl.content_kind, ContentKind::Test);
3729 assert!(!cpl.segment_list.segments.is_empty());
3730 }
3731 Err(e) => panic!("Failed to parse CPL: {:?}", e),
3732 }
3733 }
3734
3735 #[test]
3736 fn test_content_kind_scope_attribute() {
3737 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3739<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3740<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3741<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3742<ContentTitle>Test</ContentTitle>
3743<ContentKind scope="http://www.smpte-ra.org/schemas/2067-3/2013#content-kind">feature</ContentKind>
3744<SegmentList>
3745<Segment>
3746<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3747<SequenceList>
3748</SequenceList>
3749</Segment>
3750</SegmentList>
3751</CompositionPlaylist>"#;
3752
3753 let cpl = parse_cpl(xml).expect("Failed to parse CPL with ContentKind scope");
3754 assert_eq!(cpl.content_kind.kind, ContentKind::Feature);
3755 assert_eq!(
3756 cpl.content_kind.scope.as_deref(),
3757 Some("http://www.smpte-ra.org/schemas/2067-3/2013#content-kind")
3758 );
3759 assert_eq!(
3760 cpl.content_kind.effective_scope(),
3761 "http://www.smpte-ra.org/schemas/2067-3/2013#content-kind"
3762 );
3763 }
3764
3765 #[test]
3766 fn test_content_kind_custom_scope() {
3767 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3769<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3770<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3771<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3772<ContentTitle>Test</ContentTitle>
3773<ContentKind scope="http://example.com/custom-kinds">my-custom-kind</ContentKind>
3774<SegmentList>
3775<Segment>
3776<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3777<SequenceList>
3778</SequenceList>
3779</Segment>
3780</SegmentList>
3781</CompositionPlaylist>"#;
3782
3783 let cpl = parse_cpl(xml).expect("Failed to parse CPL with custom scope");
3784 assert_eq!(
3785 cpl.content_kind.kind,
3786 ContentKind::Other("my-custom-kind".to_string())
3787 );
3788 assert_eq!(
3789 cpl.content_kind.scope.as_deref(),
3790 Some("http://example.com/custom-kinds")
3791 );
3792 }
3793
3794 #[test]
3795 fn test_content_kind_no_scope_uses_default() {
3796 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3798<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3799<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3800<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3801<ContentTitle>Test</ContentTitle>
3802<ContentKind>Test</ContentKind>
3803<SegmentList>
3804<Segment>
3805<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3806<SequenceList>
3807</SequenceList>
3808</Segment>
3809</SegmentList>
3810</CompositionPlaylist>"#;
3811
3812 let cpl = parse_cpl(xml).expect("Failed to parse CPL without scope");
3813 assert_eq!(cpl.content_kind.kind, ContentKind::Test);
3814 assert!(cpl.content_kind.scope.is_none());
3815 assert_eq!(
3816 cpl.content_kind.effective_scope(),
3817 CONTENT_KIND_DEFAULT_SCOPE
3818 );
3819 }
3820
3821 #[test]
3822 fn test_malformed_xml_handling() {
3823 let malformed_xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3824<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3825<Id>urn:uuid:test</Id>
3826<ContentTitle>Broken XML"#;
3827
3828 let result: Result<CompositionPlaylist, CplParseError> = parse_cpl(malformed_xml);
3829 assert!(result.is_err(), "Should fail with malformed XML");
3830 }
3831
3832 fn minimal_cpl_with_ns(ns: &str) -> String {
3836 format!(
3837 r#"<?xml version="1.0" encoding="UTF-8" ?>
3838<CompositionPlaylist xmlns="{ns}">
3839<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3840<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3841<ContentTitle>NS Test</ContentTitle>
3842<ContentKind>Test</ContentKind>
3843<SegmentList>
3844<Segment>
3845<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
3846<SequenceList></SequenceList>
3847</Segment>
3848</SegmentList>
3849</CompositionPlaylist>"#
3850 )
3851 }
3852
3853 #[test]
3855 fn cpl_parses_with_2067_3_2013_namespace() {
3856 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
3857 let cpl = parse_cpl(&xml).expect("2013 namespace should parse");
3858 assert_eq!(cpl.content_title.text, "NS Test");
3859 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2013);
3860 assert_eq!(cpl.namespace.spec_id(), "ST 2067-3:2013");
3861 assert_eq!(cpl.namespace.year(), Some(2013));
3862 }
3863
3864 #[test]
3866 fn cpl_parses_with_2067_3_2016_namespace() {
3867 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2016");
3868 let cpl = parse_cpl(&xml).expect("2016 namespace should parse");
3869 assert_eq!(cpl.content_title.text, "NS Test");
3870 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2016);
3871 assert_eq!(cpl.namespace.year(), Some(2016));
3872 }
3873
3874 #[test]
3876 fn cpl_parses_with_2067_3_2020_namespace() {
3877 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/ns/2067-3/2020");
3878 let cpl = parse_cpl(&xml).expect("2020 namespace should parse");
3879 assert_eq!(cpl.content_title.text, "NS Test");
3880 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2020);
3881 assert_eq!(cpl.namespace.year(), Some(2020));
3882 }
3883
3884 #[test]
3886 fn cpl_parses_with_dci_429_7_namespace() {
3887 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/429-7/2006/CPL");
3888 let cpl = parse_cpl(&xml).expect("DCI 429-7 namespace should parse");
3889 assert_eq!(cpl.content_title.text, "NS Test");
3890 assert_eq!(cpl.namespace, CplNamespace::Dci429_7);
3891 assert_eq!(cpl.namespace.year(), Some(2006));
3892 }
3893
3894 #[test]
3896 fn cpl_meridian_detects_2013_namespace() {
3897 let xml = include_str!("../../../../test-data/MERIDIAN_Netflix_Photon_161006/CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml");
3898 let cpl = parse_cpl(xml).expect("MERIDIAN CPL should parse");
3899 assert_eq!(cpl.namespace, CplNamespace::Smpte2067_3_2013);
3900 }
3901
3902 #[test]
3903 fn strict_unknown_mode_rejects_unknown_elements() {
3904 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3905<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3906<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3907<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3908<ContentTitle>Strict Unknown</ContentTitle>
3909<ContentKind>Test</ContentKind>
3910<UnknownElement>oops</UnknownElement>
3911<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
3912</CompositionPlaylist>"#;
3913
3914 let options = CplParseOptions {
3915 unknown_field_mode: UnknownFieldMode::Error,
3916 ..Default::default()
3917 };
3918 let result = parse_cpl_with_options(xml, &options);
3919 assert!(matches!(result, Err(CplParseError::StrictUnknownXml(_))));
3920 }
3921
3922 #[test]
3923 fn strict_schema_mode_rejects_empty_sequence_list_per_segment() {
3924 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3925<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3926<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3927<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3928<ContentTitle>Strict Schema</ContentTitle>
3929<ContentKind>Test</ContentKind>
3930<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
3931</CompositionPlaylist>"#;
3932
3933 let options = CplParseOptions {
3934 schema_strict_mode: SchemaStrictMode::Basic,
3935 ..Default::default()
3936 };
3937 let result = parse_cpl_with_options(xml, &options);
3938 assert!(matches!(result, Err(CplParseError::StrictSchema(_))));
3939 }
3940
3941 #[test]
3942 fn signature_mode_require_presence_rejects_unsigned_cpl() {
3943 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
3944 let options = CplParseOptions {
3945 signature_validation_mode: SignatureValidationMode::RequirePresence,
3946 ..Default::default()
3947 };
3948 let result = parse_cpl_with_options(&xml, &options);
3949 assert!(matches!(result, Err(CplParseError::StrictSchema(_))));
3950 }
3951
3952 #[test]
3953 fn signature_mode_require_valid_needs_verifier() {
3954 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3955<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3956<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3957<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3958<ContentTitle>Signed</ContentTitle>
3959<ContentKind>Test</ContentKind>
3960<Signature>dummy</Signature>
3961<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
3962</CompositionPlaylist>"#;
3963
3964 let options = CplParseOptions {
3965 signature_validation_mode: SignatureValidationMode::RequireValid,
3966 ..Default::default()
3967 };
3968 let result = parse_cpl_with_options(xml, &options);
3969 assert!(matches!(
3970 result,
3971 Err(CplParseError::SignatureVerifierRequired)
3972 ));
3973 }
3974
3975 #[test]
3976 fn signature_mode_require_valid_uses_verifier() {
3977 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
3978<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
3979<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
3980<IssueDate>2024-01-01T00:00:00Z</IssueDate>
3981<ContentTitle>Signed</ContentTitle>
3982<ContentKind>Test</ContentKind>
3983<Signature>dummy</Signature>
3984<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>
3985</CompositionPlaylist>"#;
3986
3987 let verifier = AcceptAllSignatureVerifier;
3988 let options = CplParseOptions {
3989 signature_validation_mode: SignatureValidationMode::RequireValid,
3990 signature_verifier: Some(&verifier),
3991 ..Default::default()
3992 };
3993 let result = parse_cpl_with_options(xml, &options);
3994 assert!(
3995 result.is_ok(),
3996 "signature verifier should allow parse: {result:?}"
3997 );
3998 }
3999
4000 #[test]
4001 fn signature_mode_require_valid_surfaces_verification_failure() {
4002 let xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4003<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4004<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4005<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4006<ContentTitle>Signed</ContentTitle>
4007<ContentKind>Test</ContentKind>
4008<Signature>dummy</Signature>
4009<SegmentList><Segment><Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id><SequenceList></SequenceList></Segment></SegmentList>
4010</CompositionPlaylist>"#;
4011
4012 let verifier = RejectingSignatureVerifier;
4013 let options = CplParseOptions {
4014 signature_validation_mode: SignatureValidationMode::RequireValid,
4015 signature_verifier: Some(&verifier),
4016 ..Default::default()
4017 };
4018 let result = parse_cpl_with_options(xml, &options);
4019 assert!(matches!(
4020 result,
4021 Err(CplParseError::SignatureVerificationFailed(_))
4022 ));
4023 }
4024
4025 fn build_signed_cpl_with_reference_digest(tamper_digest: bool) -> String {
4026 let unsigned_xml = r#"<?xml version="1.0" encoding="UTF-8" ?>
4027<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4028<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4029<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4030<ContentTitle>Signed Digest CPL</ContentTitle>
4031<ContentKind>Test</ContentKind>
4032<SegmentList>
4033<Segment>
4034<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
4035<SequenceList></SequenceList>
4036</Segment>
4037</SegmentList>
4038</CompositionPlaylist>"#;
4039
4040 let normalized = normalize_xml_for_digest(unsigned_xml);
4041 let digest = compute_hash(HashAlgorithm::Sha256, normalized.as_bytes());
4042 let mut digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
4043 if tamper_digest {
4044 digest_b64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string();
4045 }
4046
4047 format!(
4048 r#"<?xml version="1.0" encoding="UTF-8" ?>
4049<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4050<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
4051<IssueDate>2024-01-01T00:00:00Z</IssueDate>
4052<ContentTitle>Signed Digest CPL</ContentTitle>
4053<ContentKind>Test</ContentKind>
4054<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
4055 <SignedInfo>
4056 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4057 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4058 <Reference URI="">
4059 <Transforms>
4060 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
4061 </Transforms>
4062 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4063 <DigestValue>{}</DigestValue>
4064 </Reference>
4065 </SignedInfo>
4066 <SignatureValue>AQ==</SignatureValue>
4067</Signature>
4068<SegmentList>
4069<Segment>
4070<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
4071<SequenceList></SequenceList>
4072</Segment>
4073</SegmentList>
4074</CompositionPlaylist>"#,
4075 digest_b64
4076 )
4077 }
4078
4079 #[test]
4080 fn reference_digest_verifier_accepts_valid_uri_empty_digest() {
4081 let xml = build_signed_cpl_with_reference_digest(false);
4082 let verifier = ReferenceDigestXmlDsigVerifier;
4083 assert!(verifier.verify(&xml).is_ok());
4084 }
4085
4086 #[test]
4087 fn reference_digest_verifier_rejects_mismatched_digest() {
4088 let xml = build_signed_cpl_with_reference_digest(true);
4089 let verifier = ReferenceDigestXmlDsigVerifier;
4090 let result = verifier.verify(&xml);
4091 assert!(result.is_err());
4092 let error = result.unwrap_err();
4093 assert!(
4094 error.contains("DigestValue mismatch"),
4095 "unexpected error: {error}"
4096 );
4097 }
4098
4099 #[test]
4100 fn signature_mode_require_valid_with_reference_digest_verifier() {
4101 let xml = build_signed_cpl_with_reference_digest(false);
4102 let verifier = ReferenceDigestXmlDsigVerifier;
4103 let options = CplParseOptions {
4104 signature_validation_mode: SignatureValidationMode::RequireValid,
4105 signature_verifier: Some(&verifier),
4106 ..Default::default()
4107 };
4108 let result = parse_cpl_with_options(&xml, &options);
4109 assert!(
4110 result.is_ok(),
4111 "expected valid signature digest path to parse: {result:?}"
4112 );
4113 }
4114
4115 #[cfg(all(feature = "xmlsec1", not(target_arch = "wasm32")))]
4116 #[test]
4117 fn xmlsec_verifier_surfaces_missing_binary() {
4118 let xml = minimal_cpl_with_ns("http://www.smpte-ra.org/schemas/2067-3/2013");
4119 let verifier = XmlSec1Verifier::new().with_binary_path("xmlsec1-definitely-not-installed");
4120 let error = verifier
4121 .verify(&xml)
4122 .expect_err("expected missing binary error");
4123 assert!(
4124 error.contains("failed to execute"),
4125 "unexpected error message: {error}"
4126 );
4127 }
4128
4129 #[cfg(all(feature = "xmlsec", not(target_arch = "wasm32")))]
4130 #[test]
4131 fn xmlsec_crate_verifier_rejects_invalid_key_material() {
4132 let xml = build_signed_cpl_with_reference_digest(false);
4133 let verifier = XmlSecCrateVerifier::from_pem("not-a-valid-key");
4134 let error = verifier
4135 .verify(&xml)
4136 .expect_err("expected xmlsec key load error");
4137 assert!(
4138 error.contains("xmlsec key load failed") || error.contains("xmlsec verify failed"),
4139 "unexpected error message: {error}"
4140 );
4141 }
4142}