1pub mod codes;
11pub mod volindex;
12pub mod volindex_codes;
13
14pub use volindex::{parse_volindex, VolindexError, VolumeIndex};
16
17use base64::Engine;
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20use uuid::Uuid;
21
22#[derive(Debug, Error, PartialEq)]
25pub enum ImfTypeError {
26 #[error("Invalid UUID '{0}': expected urn:uuid:<uuid> or bare UUID")]
27 InvalidUuid(String),
28 #[error("Invalid edit rate '{0}': expected 'numerator denominator'")]
29 InvalidEditRate(String),
30 #[error("Invalid hash: {0}")]
31 InvalidHash(String),
32 #[error("Invalid language tag '{0}': must be non-empty")]
33 InvalidLanguageTag(String),
34 #[error("Invalid SMPTE UL '{0}': expected 16 hex bytes in dotted groups")]
35 InvalidUl(String),
36}
37
38#[derive(Debug, Clone, Copy)]
55pub struct SmpteUl(pub [u8; 16]);
56
57impl SmpteUl {
58 pub fn parse(s: &str) -> Result<Self, ImfTypeError> {
65 let hex_part = s.strip_prefix("urn:smpte:ul:").unwrap_or(s).trim();
66
67 let hex_str: String = hex_part.chars().filter(|c| *c != '.').collect();
69
70 if hex_str.len() != 32 {
71 return Err(ImfTypeError::InvalidUl(s.to_string()));
72 }
73
74 let mut bytes = [0u8; 16];
75 for i in 0..16 {
76 bytes[i] = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16)
77 .map_err(|_| ImfTypeError::InvalidUl(s.to_string()))?;
78 }
79
80 Ok(SmpteUl(bytes))
81 }
82
83 pub fn matches_ignoring_version(&self, other: &SmpteUl) -> bool {
87 for i in 0..16 {
88 if i == 7 {
89 continue; }
91 if self.0[i] != other.0[i] {
92 return false;
93 }
94 }
95 true
96 }
97
98 pub fn normalized(&self) -> Self {
100 let mut bytes = self.0;
101 bytes[7] = 0;
102 SmpteUl(bytes)
103 }
104
105 pub fn item_bytes(&self) -> &[u8] {
107 &self.0[8..]
108 }
109}
110
111impl PartialEq for SmpteUl {
112 fn eq(&self, other: &Self) -> bool {
114 self.matches_ignoring_version(other)
115 }
116}
117
118impl Eq for SmpteUl {}
119
120impl std::hash::Hash for SmpteUl {
121 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
122 let norm = self.normalized();
124 norm.0.hash(state);
125 }
126}
127
128impl std::fmt::Display for SmpteUl {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 write!(
131 f,
132 "urn:smpte:ul:{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}",
133 self.0[0], self.0[1], self.0[2], self.0[3],
134 self.0[4], self.0[5], self.0[6], self.0[7],
135 self.0[8], self.0[9], self.0[10], self.0[11],
136 self.0[12], self.0[13], self.0[14], self.0[15],
137 )
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
150#[cfg_attr(feature = "typescript", ts(type = "string"))]
151pub struct ImfUuid(pub Uuid);
152
153impl ImfUuid {
154 pub fn parse(s: &str) -> Result<Self, ImfTypeError> {
156 let bare = s.strip_prefix("urn:uuid:").unwrap_or(s);
157 Uuid::parse_str(bare)
158 .map(ImfUuid)
159 .map_err(|_| ImfTypeError::InvalidUuid(s.to_string()))
160 }
161
162 pub fn to_urn(&self) -> String {
164 format!("urn:uuid:{}", self.0)
165 }
166}
167
168impl std::fmt::Display for ImfUuid {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 self.0.fmt(f)
171 }
172}
173
174impl Serialize for ImfUuid {
175 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
176 s.serialize_str(&self.0.to_string())
177 }
178}
179
180impl<'de> Deserialize<'de> for ImfUuid {
181 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
182 let s = String::deserialize(d)?;
183 ImfUuid::parse(&s).map_err(serde::de::Error::custom)
184 }
185}
186
187#[cfg(feature = "jsonschema")]
188impl schemars::JsonSchema for ImfUuid {
189 fn schema_name() -> String {
190 "ImfUuid".to_owned()
191 }
192
193 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
194 let mut schema = gen.subschema_for::<String>().into_object();
195 schema.metadata().description = Some(
196 "A SMPTE IMF UUID, serialised as a bare UUID string (e.g. \"0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85\")".to_owned()
197 );
198 schema.format = Some("uuid".to_owned());
199 schema.into()
200 }
201}
202
203#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct AssetHash {
211 algorithm: HashAlgorithm,
212 bytes: Vec<u8>,
213}
214
215#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222pub enum HashAlgorithm {
223 Sha1,
226 Sha256,
229}
230
231impl HashAlgorithm {
232 pub fn from_uri(uri: &str) -> Option<Self> {
237 match uri.trim() {
238 "http://www.w3.org/2000/09/xmldsig#sha1" => Some(Self::Sha1),
239 "http://www.w3.org/2001/04/xmlenc#sha256" => Some(Self::Sha256),
240 _ => None,
241 }
242 }
243
244 pub fn digest_len(&self) -> usize {
246 match self {
247 Self::Sha1 => 20,
248 Self::Sha256 => 32,
249 }
250 }
251}
252
253impl std::fmt::Display for HashAlgorithm {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 match self {
256 Self::Sha1 => write!(f, "SHA-1"),
257 Self::Sha256 => write!(f, "SHA-256"),
258 }
259 }
260}
261
262impl AssetHash {
263 pub fn algorithm(&self) -> HashAlgorithm {
265 self.algorithm
266 }
267
268 pub fn bytes(&self) -> &[u8] {
270 &self.bytes
271 }
272
273 pub fn from_base64_sha1(b64: &str) -> Result<Self, ImfTypeError> {
277 let bytes = base64::engine::general_purpose::STANDARD
278 .decode(b64)
279 .map_err(|e| ImfTypeError::InvalidHash(e.to_string()))?;
280 if bytes.len() != 20 {
281 return Err(ImfTypeError::InvalidHash(format!(
282 "SHA-1 digest must be 20 bytes, got {}",
283 bytes.len()
284 )));
285 }
286 Ok(Self {
287 algorithm: HashAlgorithm::Sha1,
288 bytes,
289 })
290 }
291
292 pub fn from_base64_sha256(b64: &str) -> Result<Self, ImfTypeError> {
296 let bytes = base64::engine::general_purpose::STANDARD
297 .decode(b64)
298 .map_err(|e| ImfTypeError::InvalidHash(e.to_string()))?;
299 if bytes.len() != 32 {
300 return Err(ImfTypeError::InvalidHash(format!(
301 "SHA-256 digest must be 32 bytes, got {}",
302 bytes.len()
303 )));
304 }
305 Ok(Self {
306 algorithm: HashAlgorithm::Sha256,
307 bytes,
308 })
309 }
310
311 pub fn from_base64(b64: &str, algorithm: HashAlgorithm) -> Result<Self, ImfTypeError> {
313 match algorithm {
314 HashAlgorithm::Sha1 => Self::from_base64_sha1(b64),
315 HashAlgorithm::Sha256 => Self::from_base64_sha256(b64),
316 }
317 }
318
319 pub fn to_base64(&self) -> String {
321 base64::engine::general_purpose::STANDARD.encode(&self.bytes)
322 }
323}
324
325#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330pub enum MimeType {
331 TextXml,
333 ApplicationXml,
335 ApplicationMxf,
337 Other(String),
339}
340
341impl MimeType {
342 pub fn parse(s: &str) -> Self {
343 match s.trim() {
344 "text/xml" => Self::TextXml,
345 "application/xml" => Self::ApplicationXml,
346 "application/mxf" => Self::ApplicationMxf,
347 other => Self::Other(other.to_string()),
348 }
349 }
350
351 pub fn is_xml(&self) -> bool {
352 matches!(self, Self::TextXml | Self::ApplicationXml)
353 }
354
355 pub fn is_mxf(&self) -> bool {
356 matches!(self, Self::ApplicationMxf)
357 }
358}
359
360impl std::fmt::Display for MimeType {
361 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362 match self {
363 Self::TextXml => write!(f, "text/xml"),
364 Self::ApplicationXml => write!(f, "application/xml"),
365 Self::ApplicationMxf => write!(f, "application/mxf"),
366 Self::Other(s) => write!(f, "{}", s),
367 }
368 }
369}
370
371#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
375#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
376pub enum AssetMapNamespace {
377 #[default]
379 Dci429_9,
380 Smpte2067_9_2016,
382 Smpte2067_9_2020,
384 Unknown(String),
386}
387
388impl AssetMapNamespace {
389 pub fn from_uri(uri: &str) -> Self {
391 match uri.trim() {
392 "http://www.smpte-ra.org/schemas/429-9/2007/AM" => Self::Dci429_9,
393 "http://www.smpte-ra.org/schemas/2067-9/2016" => Self::Smpte2067_9_2016,
394 "http://www.smpte-ra.org/ns/2067-9/2020" => Self::Smpte2067_9_2020,
395 other => Self::Unknown(other.to_string()),
396 }
397 }
398
399 pub fn spec_id(&self) -> &str {
401 match self {
402 Self::Dci429_9 => "ST 429-9:2007",
403 Self::Smpte2067_9_2016 => "ST 2067-9:2016",
404 Self::Smpte2067_9_2020 => "ST 2067-9:2020",
405 Self::Unknown(_) => "Unknown",
406 }
407 }
408}
409
410impl std::fmt::Display for AssetMapNamespace {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 match self {
413 Self::Dci429_9 => write!(f, "http://www.smpte-ra.org/schemas/429-9/2007/AM"),
414 Self::Smpte2067_9_2016 => write!(f, "http://www.smpte-ra.org/schemas/2067-9/2016"),
415 Self::Smpte2067_9_2020 => write!(f, "http://www.smpte-ra.org/ns/2067-9/2020"),
416 Self::Unknown(s) => write!(f, "{}", s),
417 }
418 }
419}
420
421#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
427#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
428pub enum PklNamespace {
429 #[default]
431 Dci429_8,
432 Smpte2067_2_2013,
434 Smpte2067_2_2016,
436 Smpte2067_2_2016Pkl,
438 Smpte2067_2_2020,
440 Unknown(String),
442}
443
444impl PklNamespace {
445 pub fn from_uri(uri: &str) -> Self {
447 match uri.trim() {
448 "http://www.smpte-ra.org/schemas/429-8/2007/PKL" => Self::Dci429_8,
449 "http://www.smpte-ra.org/schemas/2067-2/2013" => Self::Smpte2067_2_2013,
450 "http://www.smpte-ra.org/schemas/2067-2/2016" => Self::Smpte2067_2_2016,
451 "http://www.smpte-ra.org/schemas/2067-2/2016/PKL" => Self::Smpte2067_2_2016Pkl,
452 "http://www.smpte-ra.org/ns/2067-2/2020" => Self::Smpte2067_2_2020,
453 other => Self::Unknown(other.to_string()),
454 }
455 }
456
457 pub fn spec_id(&self) -> &str {
459 match self {
460 Self::Dci429_8 => "ST 429-8:2007",
461 Self::Smpte2067_2_2013 => "ST 2067-2:2013",
462 Self::Smpte2067_2_2016 | Self::Smpte2067_2_2016Pkl => "ST 2067-2:2016",
463 Self::Smpte2067_2_2020 => "ST 2067-2:2020",
464 Self::Unknown(_) => "Unknown",
465 }
466 }
467}
468
469impl std::fmt::Display for PklNamespace {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 match self {
472 Self::Dci429_8 => write!(f, "http://www.smpte-ra.org/schemas/429-8/2007/PKL"),
473 Self::Smpte2067_2_2013 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2013"),
474 Self::Smpte2067_2_2016 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2016"),
475 Self::Smpte2067_2_2016Pkl => {
476 write!(f, "http://www.smpte-ra.org/schemas/2067-2/2016/PKL")
477 }
478 Self::Smpte2067_2_2020 => write!(f, "http://www.smpte-ra.org/ns/2067-2/2020"),
479 Self::Unknown(s) => write!(f, "{}", s),
480 }
481 }
482}
483
484#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
492#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
493pub enum CoreConstraintsNamespace {
494 Smpte2067_2_2013,
496 #[default]
498 Smpte2067_2_2016,
499 Smpte2067_2_2020,
501 Unknown(String),
503}
504
505impl CoreConstraintsNamespace {
506 pub fn from_uri(uri: &str) -> Self {
508 match uri.trim() {
509 "http://www.smpte-ra.org/schemas/2067-2/2013" => Self::Smpte2067_2_2013,
510 "http://www.smpte-ra.org/schemas/2067-2/2016" => Self::Smpte2067_2_2016,
511 "http://www.smpte-ra.org/ns/2067-2/2020" => Self::Smpte2067_2_2020,
512 other => Self::Unknown(other.to_string()),
513 }
514 }
515
516 pub fn spec_id(&self) -> &str {
518 match self {
519 Self::Smpte2067_2_2013 => "ST 2067-2:2013",
520 Self::Smpte2067_2_2016 => "ST 2067-2:2016",
521 Self::Smpte2067_2_2020 => "ST 2067-2:2020",
522 Self::Unknown(_) => "Unknown",
523 }
524 }
525}
526
527impl std::fmt::Display for CoreConstraintsNamespace {
528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 match self {
530 Self::Smpte2067_2_2013 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2013"),
531 Self::Smpte2067_2_2016 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2016"),
532 Self::Smpte2067_2_2020 => write!(f, "http://www.smpte-ra.org/ns/2067-2/2020"),
533 Self::Unknown(s) => write!(f, "{}", s),
534 }
535 }
536}
537
538pub fn detect_root_namespace(xml: &str) -> Option<String> {
545 use std::sync::LazyLock;
546 static RE_XMLNS: LazyLock<regex::Regex> =
549 LazyLock::new(|| regex::Regex::new(r#"(?:^|[\s<])xmlns="([^"]*)""#).unwrap());
550 RE_XMLNS.captures(xml).map(|cap| cap[1].to_string())
551}
552
553#[derive(Debug, Error)]
557pub enum AssetMapParseError {
558 #[error("XML parse error: {0}")]
560 Xml(#[from] quick_xml::DeError),
561 #[error("Invalid field '{field}': {source}")]
563 Field {
564 field: &'static str,
565 #[source]
566 source: ImfTypeError,
567 },
568}
569
570mod raw {
573 use serde::Deserialize;
574
575 fn default_volume_index() -> u32 {
576 1
577 }
578
579 #[derive(Deserialize)]
580 pub struct AssetMap {
581 #[serde(rename = "Id")]
582 pub id: String,
583 #[serde(rename = "AnnotationText", default)]
584 pub annotation_text: Option<String>,
585 #[serde(rename = "Creator", default)]
586 pub creator: Option<String>,
587 #[serde(rename = "VolumeCount")]
588 pub volume_count: u32,
589 #[serde(rename = "IssueDate")]
590 pub issue_date: String,
591 #[serde(rename = "Issuer", default)]
592 pub issuer: Option<String>,
593 #[serde(rename = "AssetList")]
594 pub asset_list: AssetList,
595 }
596
597 #[derive(Deserialize)]
598 pub struct AssetList {
599 #[serde(rename = "Asset")]
600 pub assets: Vec<Asset>,
601 }
602
603 #[derive(Deserialize)]
604 pub struct Asset {
605 #[serde(rename = "Id")]
606 pub id: String,
607 #[serde(rename = "PackingList", default)]
608 pub packing_list: Option<bool>,
609 #[serde(rename = "ChunkList")]
610 pub chunk_list: ChunkList,
611 }
612
613 #[derive(Deserialize)]
614 pub struct ChunkList {
615 #[serde(rename = "Chunk")]
616 pub chunks: Vec<Chunk>,
617 }
618
619 #[derive(Deserialize)]
620 pub struct Chunk {
621 #[serde(rename = "Path")]
622 pub path: String,
623 #[serde(rename = "VolumeIndex", default = "default_volume_index")]
624 pub volume_index: u32,
625 }
626
627 #[derive(Deserialize)]
630 pub struct OutputProfileList {
631 #[serde(rename = "Id")]
632 pub id: String,
633 #[serde(rename = "Annotation", default)]
634 pub annotation: Option<String>,
635 #[serde(rename = "IssueDate")]
636 pub issue_date: String,
637 #[serde(rename = "Issuer", default)]
638 pub issuer: Option<String>,
639 #[serde(rename = "Creator", default)]
640 pub creator: Option<String>,
641 #[serde(rename = "CompositionPlaylistId")]
642 pub composition_playlist_id: String,
643 }
644
645 #[derive(Deserialize)]
648 pub struct PackingList {
649 #[serde(rename = "Id")]
650 pub id: String,
651 #[serde(rename = "AnnotationText", default)]
652 pub annotation_text: Option<String>,
653 #[serde(rename = "IssueDate")]
654 pub issue_date: String,
655 #[serde(rename = "Issuer", default)]
656 pub issuer: Option<String>,
657 #[serde(rename = "Creator", default)]
658 pub creator: Option<String>,
659 #[serde(rename = "GroupId", default)]
661 pub group_id: Option<String>,
662 #[serde(rename = "AssetList")]
663 pub asset_list: PklAssetList,
664 }
665
666 #[derive(Deserialize)]
667 pub struct PklAssetList {
668 #[serde(rename = "Asset")]
669 pub assets: Vec<PklAsset>,
670 }
671
672 #[derive(Deserialize)]
675 pub struct DigestMethod {
676 #[serde(rename = "@Algorithm")]
677 pub algorithm: String,
678 }
679
680 #[derive(Deserialize)]
681 pub struct PklAsset {
682 #[serde(rename = "Id")]
683 pub id: String,
684 #[serde(rename = "AnnotationText", default)]
685 pub annotation_text: Option<String>,
686 #[serde(rename = "Hash")]
687 pub hash: String,
688 #[serde(rename = "Size")]
689 pub size: u64,
690 #[serde(rename = "Type")]
691 pub mime_type: String,
692 #[serde(rename = "OriginalFileName", default)]
693 pub original_file_name: Option<String>,
694 #[serde(rename = "HashAlgorithm", default)]
697 pub hash_algorithm: Option<DigestMethod>,
698 }
699}
700
701#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
707#[derive(Debug, Serialize, Deserialize, PartialEq)]
708#[serde(rename_all = "camelCase")]
709#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
710#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
711#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
712#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
713pub struct AssetMap {
714 #[serde(skip)]
716 pub namespace: AssetMapNamespace,
717 pub id: ImfUuid,
719 pub annotation_text: Option<String>,
720 pub creator: Option<String>,
721 pub volume_count: u32,
722 pub issue_date: String,
724 pub issuer: Option<String>,
725 pub asset_list: AssetList,
726}
727
728impl AssetMap {
729 fn from_raw(
730 raw: raw::AssetMap,
731 namespace: AssetMapNamespace,
732 ) -> Result<Self, AssetMapParseError> {
733 Ok(Self {
734 namespace,
735 id: ImfUuid::parse(&raw.id).map_err(|source| AssetMapParseError::Field {
736 field: "Id",
737 source,
738 })?,
739 annotation_text: raw.annotation_text,
740 creator: raw.creator,
741 volume_count: raw.volume_count,
742 issue_date: raw.issue_date,
743 issuer: raw.issuer,
744 asset_list: AssetList::from_raw(raw.asset_list)?,
745 })
746 }
747}
748
749#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
751#[derive(Debug, Serialize, Deserialize, PartialEq)]
752#[serde(rename_all = "camelCase")]
753#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
754#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
755#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
756#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
757pub struct AssetList {
758 pub assets: Vec<Asset>,
759}
760
761impl AssetList {
762 fn from_raw(raw: raw::AssetList) -> Result<Self, AssetMapParseError> {
763 let assets = raw
764 .assets
765 .into_iter()
766 .map(Asset::from_raw)
767 .collect::<Result<Vec<_>, _>>()?;
768 Ok(Self { assets })
769 }
770}
771
772#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
774#[derive(Debug, Serialize, Deserialize, PartialEq)]
775#[serde(rename_all = "camelCase")]
776#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
777#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
778#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
779#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
780pub struct Asset {
781 pub id: ImfUuid,
783 pub packing_list: Option<bool>,
785 pub chunk_list: ChunkList,
786}
787
788impl Asset {
789 fn from_raw(raw: raw::Asset) -> Result<Self, AssetMapParseError> {
790 Ok(Self {
791 id: ImfUuid::parse(&raw.id).map_err(|source| AssetMapParseError::Field {
792 field: "Id",
793 source,
794 })?,
795 packing_list: raw.packing_list,
796 chunk_list: ChunkList::from_raw(raw.chunk_list),
797 })
798 }
799}
800
801#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
803#[derive(Debug, Serialize, Deserialize, PartialEq)]
804#[serde(rename_all = "camelCase")]
805#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
806#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
807#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
808#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
809pub struct ChunkList {
810 pub chunks: Vec<Chunk>,
811}
812
813impl ChunkList {
814 fn from_raw(raw: raw::ChunkList) -> Self {
815 Self {
816 chunks: raw
817 .chunks
818 .into_iter()
819 .map(|c| Chunk {
820 path: c.path,
821 volume_index: c.volume_index,
822 })
823 .collect(),
824 }
825 }
826}
827
828#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
830#[derive(Debug, Serialize, Deserialize, PartialEq)]
831#[serde(rename_all = "camelCase")]
832#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
833#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
834#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
835#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
836pub struct Chunk {
837 pub path: String,
839 pub volume_index: u32,
840}
841
842#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
849#[derive(Debug, Serialize, Deserialize, PartialEq)]
850#[serde(rename_all = "camelCase")]
851pub struct OutputProfileList {
852 pub id: ImfUuid,
853 pub annotation: Option<String>,
854 pub issue_date: String,
856 pub issuer: Option<String>,
857 pub creator: Option<String>,
858 pub composition_playlist_id: ImfUuid,
860}
861
862impl OutputProfileList {
863 fn from_raw(raw: raw::OutputProfileList) -> Result<Self, AssetMapParseError> {
864 Ok(Self {
865 id: ImfUuid::parse(&raw.id).map_err(|source| AssetMapParseError::Field {
866 field: "Id",
867 source,
868 })?,
869 annotation: raw.annotation,
870 issue_date: raw.issue_date,
871 issuer: raw.issuer,
872 creator: raw.creator,
873 composition_playlist_id: ImfUuid::parse(&raw.composition_playlist_id).map_err(
874 |source| AssetMapParseError::Field {
875 field: "CompositionPlaylistId",
876 source,
877 },
878 )?,
879 })
880 }
881}
882
883#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
888#[derive(Debug, Serialize, Deserialize, PartialEq)]
889#[serde(rename_all = "camelCase")]
890pub struct PackingList {
891 #[serde(skip)]
893 pub namespace: PklNamespace,
894 pub id: ImfUuid,
895 pub annotation_text: Option<String>,
896 pub issue_date: String,
898 pub issuer: Option<String>,
899 pub creator: Option<String>,
900 pub group_id: Option<ImfUuid>,
902 pub asset_list: PklAssetList,
903}
904
905impl PackingList {
906 fn from_raw(
907 raw: raw::PackingList,
908 namespace: PklNamespace,
909 ) -> Result<Self, AssetMapParseError> {
910 let group_id = raw
911 .group_id
912 .map(|s| ImfUuid::parse(&s))
913 .transpose()
914 .map_err(|source| AssetMapParseError::Field {
915 field: "GroupId",
916 source,
917 })?;
918
919 Ok(Self {
920 namespace,
921 id: ImfUuid::parse(&raw.id).map_err(|source| AssetMapParseError::Field {
922 field: "Id",
923 source,
924 })?,
925 annotation_text: raw.annotation_text,
926 issue_date: raw.issue_date,
927 issuer: raw.issuer,
928 creator: raw.creator,
929 group_id,
930 asset_list: PklAssetList::from_raw(raw.asset_list)?,
931 })
932 }
933}
934
935#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
937#[derive(Debug, Serialize, Deserialize, PartialEq)]
938#[serde(rename_all = "camelCase")]
939pub struct PklAssetList {
940 pub assets: Vec<PklAsset>,
941}
942
943impl PklAssetList {
944 fn from_raw(raw: raw::PklAssetList) -> Result<Self, AssetMapParseError> {
945 let assets = raw
946 .assets
947 .into_iter()
948 .map(PklAsset::from_raw)
949 .collect::<Result<Vec<_>, _>>()?;
950 Ok(Self { assets })
951 }
952}
953
954#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
956#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
957#[serde(rename_all = "camelCase")]
958pub struct PklAsset {
959 pub id: ImfUuid,
960 pub annotation_text: Option<String>,
961 pub hash: AssetHash,
963 pub size: u64,
965 pub mime_type: MimeType,
967 pub original_file_name: Option<String>,
968}
969
970impl PklAsset {
971 fn from_raw(raw: raw::PklAsset) -> Result<Self, AssetMapParseError> {
972 let algorithm = match &raw.hash_algorithm {
975 Some(dm) => {
976 HashAlgorithm::from_uri(&dm.algorithm).ok_or_else(|| AssetMapParseError::Field {
977 field: "HashAlgorithm",
978 source: ImfTypeError::InvalidHash(format!(
979 "unsupported hash algorithm URI: {}",
980 dm.algorithm
981 )),
982 })?
983 }
984 None => HashAlgorithm::Sha1,
985 };
986
987 Ok(Self {
988 id: ImfUuid::parse(&raw.id).map_err(|source| AssetMapParseError::Field {
989 field: "Id",
990 source,
991 })?,
992 annotation_text: raw.annotation_text,
993 hash: AssetHash::from_base64(&raw.hash, algorithm).map_err(|source| {
994 AssetMapParseError::Field {
995 field: "Hash",
996 source,
997 }
998 })?,
999 size: raw.size,
1000 mime_type: MimeType::parse(&raw.mime_type),
1001 original_file_name: raw.original_file_name,
1002 })
1003 }
1004}
1005
1006pub fn parse_assetmap(xml_content: &str) -> Result<AssetMap, AssetMapParseError> {
1015 let namespace = detect_root_namespace(xml_content)
1016 .map(|uri| AssetMapNamespace::from_uri(&uri))
1017 .unwrap_or_default();
1018 let raw: raw::AssetMap = quick_xml::de::from_str(xml_content)?;
1019 AssetMap::from_raw(raw, namespace)
1020}
1021
1022pub fn parse_pkl(xml_content: &str) -> Result<PackingList, AssetMapParseError> {
1027 let namespace = detect_root_namespace(xml_content)
1028 .map(|uri| PklNamespace::from_uri(&uri))
1029 .unwrap_or_default();
1030 let raw: raw::PackingList = quick_xml::de::from_str(xml_content)?;
1031 PackingList::from_raw(raw, namespace)
1032}
1033
1034pub fn parse_opl(xml_content: &str) -> Result<OutputProfileList, AssetMapParseError> {
1040 let raw: raw::OutputProfileList = quick_xml::de::from_str(xml_content)?;
1041 OutputProfileList::from_raw(raw)
1042}
1043
1044#[cfg(test)]
1047mod tests {
1048 use super::*;
1049 use pretty_assertions::assert_eq;
1050 use std::path::PathBuf;
1051
1052 fn test_data(name: &str) -> PathBuf {
1053 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1054 .join("../../test-data")
1055 .join(name)
1056 }
1057
1058 #[test]
1062 fn uuid_parse_urn_form() {
1063 let id = ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap();
1064 assert_eq!(id.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
1065 assert_eq!(id.to_urn(), "urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
1066 }
1067
1068 #[test]
1069 fn uuid_parse_bare_form() {
1070 let id = ImfUuid::parse("0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap();
1071 assert_eq!(id.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
1072 }
1073
1074 #[test]
1075 fn uuid_parse_invalid() {
1076 assert!(ImfUuid::parse("not-a-uuid").is_err());
1077 assert!(ImfUuid::parse("").is_err());
1078 assert!(ImfUuid::parse("urn:uuid:not-valid").is_err());
1079 }
1080
1081 #[test]
1082 fn uuid_roundtrip_serde() {
1083 let id = ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap();
1084 let json = serde_json::to_string(&id).unwrap();
1085 assert_eq!(json, r#""0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85""#);
1087 let back: ImfUuid = serde_json::from_str(&json).unwrap();
1088 assert_eq!(id, back);
1089 }
1090
1091 #[test]
1092 fn uuid_deserialize_urn_from_json() {
1093 let back: ImfUuid =
1095 serde_json::from_str(r#""urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85""#).unwrap();
1096 assert_eq!(back.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
1097 }
1098
1099 #[test]
1103 fn smpte_ul_parse_4group() {
1104 let ul = SmpteUl::parse("060e2b34.04010106.04010101.03030000").unwrap();
1105 assert_eq!(ul.0[0], 0x06);
1106 assert_eq!(ul.0[7], 0x06); assert_eq!(ul.0[12], 0x03);
1108 }
1109
1110 #[test]
1111 fn smpte_ul_parse_urn_form() {
1112 let ul = SmpteUl::parse("urn:smpte:ul:060e2b34.04010106.04010101.03030000").unwrap();
1113 assert_eq!(ul.0[0], 0x06);
1114 }
1115
1116 #[test]
1117 fn smpte_ul_parse_5group_variant() {
1118 let ul = SmpteUl::parse("urn:smpte:ul:060e2b34.0401.0101.04010101.01020000").unwrap();
1120 assert_eq!(ul.0[4], 0x04);
1121 assert_eq!(ul.0[5], 0x01);
1122 }
1123
1124 #[test]
1125 fn smpte_ul_version_agnostic_equality() {
1126 let v1 = SmpteUl::parse("060e2b34.04010101.04010101.03030000").unwrap();
1128 let v6 = SmpteUl::parse("060e2b34.04010106.04010101.03030000").unwrap();
1129 let vd = SmpteUl::parse("060e2b34.0401010d.04010101.03030000").unwrap();
1130 assert_eq!(v1, v6, "version 01 == version 06");
1131 assert_eq!(v6, vd, "version 06 == version 0d");
1132 }
1133
1134 #[test]
1135 fn smpte_ul_different_items_not_equal() {
1136 let a = SmpteUl::parse("060e2b34.04010106.04010101.03030000").unwrap();
1137 let b = SmpteUl::parse("060e2b34.04010106.04010101.03040000").unwrap();
1138 assert_ne!(a, b);
1139 }
1140
1141 #[test]
1142 fn smpte_ul_display_roundtrip() {
1143 let ul = SmpteUl::parse("urn:smpte:ul:060e2b34.04010106.04010101.03030000").unwrap();
1144 let s = ul.to_string();
1145 assert!(s.starts_with("urn:smpte:ul:"));
1146 let ul2 = SmpteUl::parse(&s).unwrap();
1147 assert_eq!(ul, ul2);
1148 }
1149
1150 #[test]
1151 fn smpte_ul_parse_invalid() {
1152 assert!(SmpteUl::parse("not-a-ul").is_err());
1153 assert!(SmpteUl::parse("060e2b34.04010106").is_err()); }
1155
1156 #[test]
1160 fn asset_hash_sha1_roundtrip() {
1161 let b64 = "2jmj7l5rSw0yVb/vlWAYkK/YBwk=";
1163 let h = AssetHash::from_base64_sha1(b64).unwrap();
1164 assert_eq!(h.algorithm, HashAlgorithm::Sha1);
1165 assert_eq!(h.bytes.len(), 20);
1166 assert_eq!(h.to_base64(), b64);
1167 }
1168
1169 #[test]
1171 fn asset_hash_sha1_wrong_length_rejected() {
1172 let err = AssetHash::from_base64_sha1("AAAA").unwrap_err();
1173 assert!(err.to_string().contains("20 bytes"));
1174 }
1175
1176 #[test]
1177 fn asset_hash_sha256_roundtrip() {
1178 let b64 = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
1179 let h = AssetHash::from_base64_sha256(b64).unwrap();
1180 assert_eq!(h.algorithm, HashAlgorithm::Sha256);
1181 assert_eq!(h.bytes.len(), 32);
1182 assert_eq!(h.to_base64(), b64);
1183 }
1184
1185 #[test]
1186 fn asset_hash_sha256_wrong_length_rejected() {
1187 let err = AssetHash::from_base64_sha256("2jmj7l5rSw0yVb/vlWAYkK/YBwk=").unwrap_err();
1188 assert!(err.to_string().contains("32 bytes"));
1189 }
1190
1191 #[test]
1192 fn asset_hash_from_base64_routes_correctly() {
1193 let sha1_b64 = "2jmj7l5rSw0yVb/vlWAYkK/YBwk=";
1194 let sha256_b64 = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
1195
1196 let h1 = AssetHash::from_base64(sha1_b64, HashAlgorithm::Sha1).unwrap();
1197 assert_eq!(h1.algorithm, HashAlgorithm::Sha1);
1198
1199 let h2 = AssetHash::from_base64(sha256_b64, HashAlgorithm::Sha256).unwrap();
1200 assert_eq!(h2.algorithm, HashAlgorithm::Sha256);
1201 }
1202
1203 #[test]
1204 fn asset_hash_invalid_base64() {
1205 assert!(AssetHash::from_base64_sha1("not-valid-base64!!!").is_err());
1206 }
1207
1208 #[test]
1212 fn hash_algorithm_from_uri() {
1213 assert_eq!(
1214 HashAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#sha1"),
1215 Some(HashAlgorithm::Sha1)
1216 );
1217 assert_eq!(
1218 HashAlgorithm::from_uri("http://www.w3.org/2001/04/xmlenc#sha256"),
1219 Some(HashAlgorithm::Sha256)
1220 );
1221 assert_eq!(HashAlgorithm::from_uri("http://example.com/unknown"), None);
1222 }
1223
1224 #[test]
1225 fn hash_algorithm_digest_len() {
1226 assert_eq!(HashAlgorithm::Sha1.digest_len(), 20);
1227 assert_eq!(HashAlgorithm::Sha256.digest_len(), 32);
1228 }
1229
1230 #[test]
1231 fn hash_algorithm_display() {
1232 assert_eq!(HashAlgorithm::Sha1.to_string(), "SHA-1");
1233 assert_eq!(HashAlgorithm::Sha256.to_string(), "SHA-256");
1234 }
1235
1236 #[test]
1240 fn volindex_parses_index_element() {
1241 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
1242<VolumeIndex xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
1243 <Index>1</Index>
1244</VolumeIndex>"#;
1245 let result = parse_volindex(xml).unwrap();
1246 assert_eq!(result.index, 1);
1247 }
1248
1249 #[test]
1253 fn assetmap_id_is_imf_uuid() {
1254 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
1255<AssetMap xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
1256 <Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
1257 <AnnotationText>MERIDIAN</AnnotationText>
1258 <Creator>Clipster 6.1.0.0 Beta (build 111500)</Creator>
1259 <VolumeCount>1</VolumeCount>
1260 <IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
1261 <Issuer>R&S</Issuer>
1262 <AssetList>
1263 <Asset>
1264 <Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
1265 <ChunkList>
1266 <Chunk>
1267 <Path>CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml</Path>
1268 <VolumeIndex>1</VolumeIndex>
1269 </Chunk>
1270 </ChunkList>
1271 </Asset>
1272 <Asset>
1273 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1274 <PackingList>true</PackingList>
1275 <ChunkList>
1276 <Chunk>
1277 <Path>PKL_f5e93462-aed2-44ad-a4ba-2adb65823e7c.xml</Path>
1278 <VolumeIndex>1</VolumeIndex>
1279 </Chunk>
1280 </ChunkList>
1281 </Asset>
1282 </AssetList>
1283</AssetMap>"#;
1284
1285 let result = parse_assetmap(xml).unwrap();
1286 assert_eq!(
1287 result.id,
1288 ImfUuid::parse("urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7").unwrap()
1289 );
1290 assert_eq!(result.annotation_text, Some("MERIDIAN".to_string()));
1291 assert_eq!(result.volume_count, 1);
1292 assert_eq!(result.asset_list.assets.len(), 2);
1293
1294 let cpl_asset = &result.asset_list.assets[0];
1296 assert_eq!(
1297 cpl_asset.id,
1298 ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap()
1299 );
1300 assert_eq!(cpl_asset.packing_list, None);
1301 assert_eq!(
1302 cpl_asset.chunk_list.chunks[0].path,
1303 "CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml"
1304 );
1305
1306 let pkl_asset = &result.asset_list.assets[1];
1308 assert_eq!(pkl_asset.packing_list, Some(true));
1309 assert_eq!(
1310 pkl_asset.chunk_list.chunks[0].path,
1311 "PKL_f5e93462-aed2-44ad-a4ba-2adb65823e7c.xml"
1312 );
1313 }
1314
1315 #[test]
1317 fn assetmap_invalid_uuid_returns_field_error() {
1318 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1319<AssetMap xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
1320 <Id>not-a-valid-uuid</Id>
1321 <VolumeCount>1</VolumeCount>
1322 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1323 <AssetList><Asset>
1324 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1325 <ChunkList><Chunk><Path>foo.xml</Path></Chunk></ChunkList>
1326 </Asset></AssetList>
1327</AssetMap>"#;
1328 let err = parse_assetmap(xml).unwrap_err();
1329 assert!(
1330 matches!(err, AssetMapParseError::Field { field: "Id", .. }),
1331 "expected Field error for Id, got: {err}"
1332 );
1333 }
1334
1335 #[test]
1339 fn pkl_parses_assets_with_strong_types() {
1340 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
1341<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1342 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1343 <AnnotationText>MERIDIAN</AnnotationText>
1344 <IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
1345 <Issuer>R&S</Issuer>
1346 <Creator>Clipster 6.1.0.0 Beta (build 111500)</Creator>
1347 <AssetList>
1348 <Asset>
1349 <Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
1350 <AnnotationText>Meridian UHD 5994P</AnnotationText>
1351 <Hash>IW0J5IZBsAxLMCCmWtHvfHhjVUw=</Hash>
1352 <Size>15214</Size>
1353 <Type>text/xml</Type>
1354 <OriginalFileName>CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml</OriginalFileName>
1355 </Asset>
1356 <Asset>
1357 <Id>urn:uuid:61d91654-2650-4abf-abbc-ad2c7f640bf8</Id>
1358 <Hash>fL7SnTeNskm71I4otXqr/T0D5LQ=</Hash>
1359 <Size>79486353</Size>
1360 <Type>application/mxf</Type>
1361 <OriginalFileName>MERIDIAN_Netflix_Photon_161006_00.mxf</OriginalFileName>
1362 </Asset>
1363 </AssetList>
1364</PackingList>"#;
1365
1366 let result = parse_pkl(xml).unwrap();
1367 assert_eq!(
1368 result.id,
1369 ImfUuid::parse("urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c").unwrap()
1370 );
1371 assert_eq!(result.annotation_text, Some("MERIDIAN".to_string()));
1372 assert_eq!(result.issuer, Some("R&S".to_string()));
1373 assert_eq!(result.asset_list.assets.len(), 2);
1374
1375 let cpl_asset = &result.asset_list.assets[0];
1376 assert_eq!(cpl_asset.hash.algorithm, HashAlgorithm::Sha1);
1377 assert_eq!(cpl_asset.hash.bytes.len(), 20);
1378 assert_eq!(cpl_asset.hash.to_base64(), "IW0J5IZBsAxLMCCmWtHvfHhjVUw=");
1379 assert_eq!(cpl_asset.size, 15214);
1380 assert_eq!(cpl_asset.mime_type, MimeType::TextXml);
1381 assert!(cpl_asset.mime_type.is_xml());
1382
1383 let mxf_asset = &result.asset_list.assets[1];
1384 assert_eq!(mxf_asset.mime_type, MimeType::ApplicationMxf);
1385 assert!(mxf_asset.mime_type.is_mxf());
1386 }
1387
1388 #[test]
1390 fn pkl_explicit_sha1_hash_algorithm() {
1391 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1392<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1393 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1394 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1395 <AssetList><Asset>
1396 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1397 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1398 <Size>1024</Size>
1399 <Type>application/mxf</Type>
1400 <HashAlgorithm Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1401 </Asset></AssetList>
1402</PackingList>"#;
1403 let result = parse_pkl(xml).unwrap();
1404 assert_eq!(
1405 result.asset_list.assets[0].hash.algorithm,
1406 HashAlgorithm::Sha1
1407 );
1408 }
1409
1410 #[test]
1412 fn pkl_sha256_hash_algorithm() {
1413 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1414<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1415 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1416 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1417 <AssetList><Asset>
1418 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1419 <Hash>47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=</Hash>
1420 <Size>1024</Size>
1421 <Type>application/mxf</Type>
1422 <HashAlgorithm Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1423 </Asset></AssetList>
1424</PackingList>"#;
1425 let result = parse_pkl(xml).unwrap();
1426 assert_eq!(
1427 result.asset_list.assets[0].hash.algorithm,
1428 HashAlgorithm::Sha256
1429 );
1430 assert_eq!(result.asset_list.assets[0].hash.bytes.len(), 32);
1431 }
1432
1433 #[test]
1435 fn pkl_missing_hash_algorithm_defaults_to_sha1() {
1436 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1437<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1438 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1439 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1440 <AssetList><Asset>
1441 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1442 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1443 <Size>1024</Size>
1444 <Type>application/mxf</Type>
1445 </Asset></AssetList>
1446</PackingList>"#;
1447 let result = parse_pkl(xml).unwrap();
1448 assert_eq!(
1449 result.asset_list.assets[0].hash.algorithm,
1450 HashAlgorithm::Sha1
1451 );
1452 }
1453
1454 #[test]
1456 fn pkl_with_group_id() {
1457 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1458<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1459 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1460 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1461 <GroupId>urn:uuid:aabbccdd-1122-3344-5566-778899aabbcc</GroupId>
1462 <AssetList><Asset>
1463 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1464 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1465 <Size>1024</Size>
1466 <Type>application/mxf</Type>
1467 </Asset></AssetList>
1468</PackingList>"#;
1469 let result = parse_pkl(xml).unwrap();
1470 assert_eq!(
1471 result.group_id,
1472 Some(ImfUuid::parse("urn:uuid:aabbccdd-1122-3344-5566-778899aabbcc").unwrap())
1473 );
1474 }
1475
1476 #[test]
1478 fn pkl_unknown_mime_type_preserved() {
1479 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1480<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1481 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1482 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1483 <AssetList><Asset>
1484 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1485 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1486 <Size>512</Size>
1487 <Type>application/octet-stream</Type>
1488 </Asset></AssetList>
1489</PackingList>"#;
1490 let result = parse_pkl(xml).unwrap();
1491 assert_eq!(
1492 result.asset_list.assets[0].mime_type,
1493 MimeType::Other("application/octet-stream".to_string())
1494 );
1495 }
1496
1497 #[test]
1501 fn pkl_parses_with_2067_2_2016_namespace() {
1502 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1503<PackingList xmlns="http://www.smpte-ra.org/schemas/2067-2/2016">
1504 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1505 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1506 <AssetList><Asset>
1507 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1508 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1509 <Size>1024</Size>
1510 <Type>application/mxf</Type>
1511 </Asset></AssetList>
1512</PackingList>"#;
1513 let result = parse_pkl(xml).unwrap();
1514 assert_eq!(result.asset_list.assets.len(), 1);
1515 assert_eq!(result.namespace, PklNamespace::Smpte2067_2_2016);
1516 assert_eq!(result.namespace.spec_id(), "ST 2067-2:2016");
1517 }
1518
1519 #[test]
1520 fn pkl_parses_with_2067_2_2020_namespace() {
1521 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1522<PackingList xmlns="http://www.smpte-ra.org/ns/2067-2/2020">
1523 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1524 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1525 <AssetList><Asset>
1526 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1527 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1528 <Size>1024</Size>
1529 <Type>application/mxf</Type>
1530 </Asset></AssetList>
1531</PackingList>"#;
1532 let result = parse_pkl(xml).unwrap();
1533 assert_eq!(result.namespace, PklNamespace::Smpte2067_2_2020);
1534 }
1535
1536 #[test]
1537 fn pkl_detects_dci_429_8_namespace() {
1538 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1539<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
1540 <Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
1541 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1542 <AssetList><Asset>
1543 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
1544 <Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
1545 <Size>1024</Size>
1546 <Type>application/mxf</Type>
1547 </Asset></AssetList>
1548</PackingList>"#;
1549 let result = parse_pkl(xml).unwrap();
1550 assert_eq!(result.namespace, PklNamespace::Dci429_8);
1551 }
1552
1553 #[test]
1554 fn assetmap_parses_with_2067_9_namespace() {
1555 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1556<AssetMap xmlns="http://www.smpte-ra.org/schemas/2067-9/2016">
1557 <Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
1558 <VolumeCount>1</VolumeCount>
1559 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1560 <AssetList>
1561 <Asset>
1562 <Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
1563 <ChunkList><Chunk><Path>test.xml</Path></Chunk></ChunkList>
1564 </Asset>
1565 </AssetList>
1566</AssetMap>"#;
1567 let result = parse_assetmap(xml).unwrap();
1568 assert_eq!(result.asset_list.assets.len(), 1);
1569 assert_eq!(result.namespace, AssetMapNamespace::Smpte2067_9_2016);
1570 }
1571
1572 #[test]
1573 fn assetmap_parses_with_2020_namespace() {
1574 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1575<AssetMap xmlns="http://www.smpte-ra.org/ns/2067-9/2020">
1576 <Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
1577 <VolumeCount>1</VolumeCount>
1578 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1579 <AssetList>
1580 <Asset>
1581 <Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
1582 <ChunkList><Chunk><Path>test.xml</Path></Chunk></ChunkList>
1583 </Asset>
1584 </AssetList>
1585</AssetMap>"#;
1586 let result = parse_assetmap(xml).unwrap();
1587 assert_eq!(result.namespace, AssetMapNamespace::Smpte2067_9_2020);
1588 }
1589
1590 #[test]
1591 fn assetmap_detects_dci_429_9_namespace() {
1592 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1593<AssetMap xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
1594 <Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
1595 <VolumeCount>1</VolumeCount>
1596 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
1597 <AssetList>
1598 <Asset>
1599 <Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
1600 <ChunkList><Chunk><Path>test.xml</Path></Chunk></ChunkList>
1601 </Asset>
1602 </AssetList>
1603</AssetMap>"#;
1604 let result = parse_assetmap(xml).unwrap();
1605 assert_eq!(result.namespace, AssetMapNamespace::Dci429_9);
1606 }
1607
1608 #[test]
1612 fn opl_parses_core_metadata() {
1613 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1614<OutputProfileList xmlns="http://www.smpte-ra.org/schemas/2067-100/2014">
1615 <Id>urn:uuid:8cf83c32-4949-4f00-b081-01e12b18932f</Id>
1616 <Annotation>OPL Example</Annotation>
1617 <IssueDate>2016-06-14T19:22:37-00:00</IssueDate>
1618 <Issuer>Clipster</Issuer>
1619 <Creator>Clipster 5.9.3.7</Creator>
1620 <CompositionPlaylistId>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</CompositionPlaylistId>
1621 <AliasList/>
1622 <MacroList/>
1623</OutputProfileList>"#;
1624 let result = parse_opl(xml).unwrap();
1625 assert_eq!(
1626 result.id.to_string(),
1627 "8cf83c32-4949-4f00-b081-01e12b18932f"
1628 );
1629 assert_eq!(result.annotation.as_deref(), Some("OPL Example"));
1630 assert_eq!(result.issuer.as_deref(), Some("Clipster"));
1631 assert_eq!(result.creator.as_deref(), Some("Clipster 5.9.3.7"));
1632 assert_eq!(
1633 result.composition_playlist_id.to_string(),
1634 "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85"
1635 );
1636 }
1637
1638 #[test]
1640 fn opl_parses_real_test_file() {
1641 let xml = std::fs::read_to_string(test_data(
1642 "OPL/OPL_8cf83c32-4949-4f00-b081-01e12b18932f.xml",
1643 ))
1644 .unwrap();
1645 let result = parse_opl(&xml).unwrap();
1646 assert_eq!(
1647 result.id.to_string(),
1648 "8cf83c32-4949-4f00-b081-01e12b18932f"
1649 );
1650 assert_eq!(
1651 result.composition_playlist_id.to_string(),
1652 "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85"
1653 );
1654 }
1655
1656 #[test]
1658 fn opl_parses_isxd_test_file() {
1659 let xml = std::fs::read_to_string(test_data(
1660 "ISXD/CompleteIMP/OPL_af6b288d-27e8-441f-9a36-2c4ab9025d19.xml",
1661 ))
1662 .unwrap();
1663 let result = parse_opl(&xml).unwrap();
1664 assert_eq!(
1665 result.id.to_string(),
1666 "af6b288d-27e8-441f-9a36-2c4ab9025d19"
1667 );
1668 assert_eq!(
1669 result.composition_playlist_id.to_string(),
1670 "b2d74f92-1990-41e0-869f-2179a50f7090"
1671 );
1672 }
1673}