pub mod codes;
pub mod volindex;
pub mod volindex_codes;
pub use volindex::{VolumeIndex, VolindexError, parse_volindex};
use base64::Engine;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error, PartialEq)]
pub enum ImfTypeError {
#[error("Invalid UUID '{0}': expected urn:uuid:<uuid> or bare UUID")]
InvalidUuid(String),
#[error("Invalid edit rate '{0}': expected 'numerator denominator'")]
InvalidEditRate(String),
#[error("Invalid hash: {0}")]
InvalidHash(String),
#[error("Invalid language tag '{0}': must be non-empty")]
InvalidLanguageTag(String),
#[error("Invalid SMPTE UL '{0}': expected 16 hex bytes in dotted groups")]
InvalidUl(String),
}
#[derive(Debug, Clone, Copy)]
pub struct SmpteUl(pub [u8; 16]);
impl SmpteUl {
pub fn parse(s: &str) -> Result<Self, ImfTypeError> {
let hex_part = s
.strip_prefix("urn:smpte:ul:")
.unwrap_or(s)
.trim();
let hex_str: String = hex_part.chars().filter(|c| *c != '.').collect();
if hex_str.len() != 32 {
return Err(ImfTypeError::InvalidUl(s.to_string()));
}
let mut bytes = [0u8; 16];
for i in 0..16 {
bytes[i] = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16)
.map_err(|_| ImfTypeError::InvalidUl(s.to_string()))?;
}
Ok(SmpteUl(bytes))
}
pub fn matches_ignoring_version(&self, other: &SmpteUl) -> bool {
for i in 0..16 {
if i == 7 {
continue; }
if self.0[i] != other.0[i] {
return false;
}
}
true
}
pub fn normalized(&self) -> Self {
let mut bytes = self.0;
bytes[7] = 0;
SmpteUl(bytes)
}
pub fn item_bytes(&self) -> &[u8] {
&self.0[8..]
}
}
impl PartialEq for SmpteUl {
fn eq(&self, other: &Self) -> bool {
self.matches_ignoring_version(other)
}
}
impl Eq for SmpteUl {}
impl std::hash::Hash for SmpteUl {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let norm = self.normalized();
norm.0.hash(state);
}
}
impl std::fmt::Display for SmpteUl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"urn:smpte:ul:{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}",
self.0[0], self.0[1], self.0[2], self.0[3],
self.0[4], self.0[5], self.0[6], self.0[7],
self.0[8], self.0[9], self.0[10], self.0[11],
self.0[12], self.0[13], self.0[14], self.0[15],
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(type = "string"))]
pub struct ImfUuid(pub Uuid);
impl ImfUuid {
pub fn parse(s: &str) -> Result<Self, ImfTypeError> {
let bare = s.strip_prefix("urn:uuid:").unwrap_or(s);
Uuid::parse_str(bare).map(ImfUuid).map_err(|_| ImfTypeError::InvalidUuid(s.to_string()))
}
pub fn to_urn(&self) -> String {
format!("urn:uuid:{}", self.0)
}
}
impl std::fmt::Display for ImfUuid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Serialize for ImfUuid {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for ImfUuid {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
ImfUuid::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssetHash {
pub algorithm: HashAlgorithm,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HashAlgorithm {
Sha1,
Sha256,
}
impl HashAlgorithm {
pub fn from_uri(uri: &str) -> Option<Self> {
match uri.trim() {
"http://www.w3.org/2000/09/xmldsig#sha1" => Some(Self::Sha1),
"http://www.w3.org/2001/04/xmlenc#sha256" => Some(Self::Sha256),
_ => None,
}
}
pub fn digest_len(&self) -> usize {
match self {
Self::Sha1 => 20,
Self::Sha256 => 32,
}
}
}
impl std::fmt::Display for HashAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sha1 => write!(f, "SHA-1"),
Self::Sha256 => write!(f, "SHA-256"),
}
}
}
impl AssetHash {
pub fn from_base64_sha1(b64: &str) -> Result<Self, ImfTypeError> {
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| ImfTypeError::InvalidHash(e.to_string()))?;
if bytes.len() != 20 {
return Err(ImfTypeError::InvalidHash(format!(
"SHA-1 digest must be 20 bytes, got {}",
bytes.len()
)));
}
Ok(Self { algorithm: HashAlgorithm::Sha1, bytes })
}
pub fn from_base64_sha256(b64: &str) -> Result<Self, ImfTypeError> {
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| ImfTypeError::InvalidHash(e.to_string()))?;
if bytes.len() != 32 {
return Err(ImfTypeError::InvalidHash(format!(
"SHA-256 digest must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(Self { algorithm: HashAlgorithm::Sha256, bytes })
}
pub fn from_base64(b64: &str, algorithm: HashAlgorithm) -> Result<Self, ImfTypeError> {
match algorithm {
HashAlgorithm::Sha1 => Self::from_base64_sha1(b64),
HashAlgorithm::Sha256 => Self::from_base64_sha256(b64),
}
}
pub fn to_base64(&self) -> String {
base64::engine::general_purpose::STANDARD.encode(&self.bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MimeType {
TextXml,
ApplicationXml,
ApplicationMxf,
Other(String),
}
impl MimeType {
pub fn parse(s: &str) -> Self {
match s.trim() {
"text/xml" => Self::TextXml,
"application/xml" => Self::ApplicationXml,
"application/mxf" => Self::ApplicationMxf,
other => Self::Other(other.to_string()),
}
}
pub fn is_xml(&self) -> bool {
matches!(self, Self::TextXml | Self::ApplicationXml)
}
pub fn is_mxf(&self) -> bool {
matches!(self, Self::ApplicationMxf)
}
}
impl std::fmt::Display for MimeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TextXml => write!(f, "text/xml"),
Self::ApplicationXml => write!(f, "application/xml"),
Self::ApplicationMxf => write!(f, "application/mxf"),
Self::Other(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AssetMapNamespace {
Dci429_9,
Smpte2067_9_2016,
Smpte2067_9_2020,
Unknown(String),
}
impl AssetMapNamespace {
pub fn from_uri(uri: &str) -> Self {
match uri.trim() {
"http://www.smpte-ra.org/schemas/429-9/2007/AM" => Self::Dci429_9,
"http://www.smpte-ra.org/schemas/2067-9/2016" => Self::Smpte2067_9_2016,
"http://www.smpte-ra.org/ns/2067-9/2020" => Self::Smpte2067_9_2020,
other => Self::Unknown(other.to_string()),
}
}
pub fn spec_id(&self) -> &str {
match self {
Self::Dci429_9 => "ST 429-9:2007",
Self::Smpte2067_9_2016 => "ST 2067-9:2016",
Self::Smpte2067_9_2020 => "ST 2067-9:2020",
Self::Unknown(_) => "Unknown",
}
}
}
impl Default for AssetMapNamespace {
fn default() -> Self {
Self::Dci429_9
}
}
impl std::fmt::Display for AssetMapNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Dci429_9 => write!(f, "http://www.smpte-ra.org/schemas/429-9/2007/AM"),
Self::Smpte2067_9_2016 => write!(f, "http://www.smpte-ra.org/schemas/2067-9/2016"),
Self::Smpte2067_9_2020 => write!(f, "http://www.smpte-ra.org/ns/2067-9/2020"),
Self::Unknown(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PklNamespace {
Dci429_8,
Smpte2067_2_2013,
Smpte2067_2_2016,
Smpte2067_2_2016Pkl,
Smpte2067_2_2020,
Unknown(String),
}
impl PklNamespace {
pub fn from_uri(uri: &str) -> Self {
match uri.trim() {
"http://www.smpte-ra.org/schemas/429-8/2007/PKL" => Self::Dci429_8,
"http://www.smpte-ra.org/schemas/2067-2/2013" => Self::Smpte2067_2_2013,
"http://www.smpte-ra.org/schemas/2067-2/2016" => Self::Smpte2067_2_2016,
"http://www.smpte-ra.org/schemas/2067-2/2016/PKL" => Self::Smpte2067_2_2016Pkl,
"http://www.smpte-ra.org/ns/2067-2/2020" => Self::Smpte2067_2_2020,
other => Self::Unknown(other.to_string()),
}
}
pub fn spec_id(&self) -> &str {
match self {
Self::Dci429_8 => "ST 429-8:2007",
Self::Smpte2067_2_2013 => "ST 2067-2:2013",
Self::Smpte2067_2_2016 | Self::Smpte2067_2_2016Pkl => "ST 2067-2:2016",
Self::Smpte2067_2_2020 => "ST 2067-2:2020",
Self::Unknown(_) => "Unknown",
}
}
}
impl Default for PklNamespace {
fn default() -> Self {
Self::Dci429_8
}
}
impl std::fmt::Display for PklNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Dci429_8 => write!(f, "http://www.smpte-ra.org/schemas/429-8/2007/PKL"),
Self::Smpte2067_2_2013 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2013"),
Self::Smpte2067_2_2016 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2016"),
Self::Smpte2067_2_2016Pkl => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2016/PKL"),
Self::Smpte2067_2_2020 => write!(f, "http://www.smpte-ra.org/ns/2067-2/2020"),
Self::Unknown(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CoreConstraintsNamespace {
Smpte2067_2_2013,
Smpte2067_2_2016,
Smpte2067_2_2020,
Unknown(String),
}
impl CoreConstraintsNamespace {
pub fn from_uri(uri: &str) -> Self {
match uri.trim() {
"http://www.smpte-ra.org/schemas/2067-2/2013" => Self::Smpte2067_2_2013,
"http://www.smpte-ra.org/schemas/2067-2/2016" => Self::Smpte2067_2_2016,
"http://www.smpte-ra.org/ns/2067-2/2020" => Self::Smpte2067_2_2020,
other => Self::Unknown(other.to_string()),
}
}
pub fn spec_id(&self) -> &str {
match self {
Self::Smpte2067_2_2013 => "ST 2067-2:2013",
Self::Smpte2067_2_2016 => "ST 2067-2:2016",
Self::Smpte2067_2_2020 => "ST 2067-2:2020",
Self::Unknown(_) => "Unknown",
}
}
}
impl Default for CoreConstraintsNamespace {
fn default() -> Self {
Self::Smpte2067_2_2016
}
}
impl std::fmt::Display for CoreConstraintsNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Smpte2067_2_2013 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2013"),
Self::Smpte2067_2_2016 => write!(f, "http://www.smpte-ra.org/schemas/2067-2/2016"),
Self::Smpte2067_2_2020 => write!(f, "http://www.smpte-ra.org/ns/2067-2/2020"),
Self::Unknown(s) => write!(f, "{}", s),
}
}
}
pub fn detect_root_namespace(xml: &str) -> Option<String> {
let re = regex::Regex::new(r#"(?:^|[\s<])xmlns="([^"]*)""#).unwrap();
re.captures(xml).map(|cap| cap[1].to_string())
}
#[derive(Debug, Error)]
pub enum AssetMapParseError {
#[error("XML parse error: {0}")]
Xml(#[from] quick_xml::DeError),
#[error("Invalid field '{field}': {source}")]
Field {
field: &'static str,
#[source]
source: ImfTypeError,
},
}
mod raw {
use serde::Deserialize;
fn default_volume_index() -> u32 {
1
}
#[derive(Deserialize)]
pub struct AssetMap {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "AnnotationText", default)]
pub annotation_text: Option<String>,
#[serde(rename = "Creator", default)]
pub creator: Option<String>,
#[serde(rename = "VolumeCount")]
pub volume_count: u32,
#[serde(rename = "IssueDate")]
pub issue_date: String,
#[serde(rename = "Issuer", default)]
pub issuer: Option<String>,
#[serde(rename = "AssetList")]
pub asset_list: AssetList,
}
#[derive(Deserialize)]
pub struct AssetList {
#[serde(rename = "Asset")]
pub assets: Vec<Asset>,
}
#[derive(Deserialize)]
pub struct Asset {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "PackingList", default)]
pub packing_list: Option<bool>,
#[serde(rename = "ChunkList")]
pub chunk_list: ChunkList,
}
#[derive(Deserialize)]
pub struct ChunkList {
#[serde(rename = "Chunk")]
pub chunks: Vec<Chunk>,
}
#[derive(Deserialize)]
pub struct Chunk {
#[serde(rename = "Path")]
pub path: String,
#[serde(rename = "VolumeIndex", default = "default_volume_index")]
pub volume_index: u32,
}
#[derive(Deserialize)]
pub struct OutputProfileList {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "Annotation", default)]
pub annotation: Option<String>,
#[serde(rename = "IssueDate")]
pub issue_date: String,
#[serde(rename = "Issuer", default)]
pub issuer: Option<String>,
#[serde(rename = "Creator", default)]
pub creator: Option<String>,
#[serde(rename = "CompositionPlaylistId")]
pub composition_playlist_id: String,
}
#[derive(Deserialize)]
pub struct PackingList {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "AnnotationText", default)]
pub annotation_text: Option<String>,
#[serde(rename = "IssueDate")]
pub issue_date: String,
#[serde(rename = "Issuer", default)]
pub issuer: Option<String>,
#[serde(rename = "Creator", default)]
pub creator: Option<String>,
#[serde(rename = "GroupId", default)]
pub group_id: Option<String>,
#[serde(rename = "AssetList")]
pub asset_list: PklAssetList,
}
#[derive(Deserialize)]
pub struct PklAssetList {
#[serde(rename = "Asset")]
pub assets: Vec<PklAsset>,
}
#[derive(Deserialize)]
pub struct DigestMethod {
#[serde(rename = "@Algorithm")]
pub algorithm: String,
}
#[derive(Deserialize)]
pub struct PklAsset {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "AnnotationText", default)]
pub annotation_text: Option<String>,
#[serde(rename = "Hash")]
pub hash: String,
#[serde(rename = "Size")]
pub size: u64,
#[serde(rename = "Type")]
pub mime_type: String,
#[serde(rename = "OriginalFileName", default)]
pub original_file_name: Option<String>,
#[serde(rename = "HashAlgorithm", default)]
pub hash_algorithm: Option<DigestMethod>,
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct AssetMap {
#[serde(skip)]
pub namespace: AssetMapNamespace,
pub id: ImfUuid,
pub annotation_text: Option<String>,
pub creator: Option<String>,
pub volume_count: u32,
pub issue_date: String,
pub issuer: Option<String>,
pub asset_list: AssetList,
}
impl AssetMap {
fn from_raw(raw: raw::AssetMap, namespace: AssetMapNamespace) -> Result<Self, AssetMapParseError> {
Ok(Self {
namespace,
id: ImfUuid::parse(&raw.id)
.map_err(|source| AssetMapParseError::Field { field: "Id", source })?,
annotation_text: raw.annotation_text,
creator: raw.creator,
volume_count: raw.volume_count,
issue_date: raw.issue_date,
issuer: raw.issuer,
asset_list: AssetList::from_raw(raw.asset_list)?,
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct AssetList {
pub assets: Vec<Asset>,
}
impl AssetList {
fn from_raw(raw: raw::AssetList) -> Result<Self, AssetMapParseError> {
let assets = raw
.assets
.into_iter()
.map(Asset::from_raw)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { assets })
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Asset {
pub id: ImfUuid,
pub packing_list: Option<bool>,
pub chunk_list: ChunkList,
}
impl Asset {
fn from_raw(raw: raw::Asset) -> Result<Self, AssetMapParseError> {
Ok(Self {
id: ImfUuid::parse(&raw.id)
.map_err(|source| AssetMapParseError::Field { field: "Id", source })?,
packing_list: raw.packing_list,
chunk_list: ChunkList::from_raw(raw.chunk_list),
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ChunkList {
pub chunks: Vec<Chunk>,
}
impl ChunkList {
fn from_raw(raw: raw::ChunkList) -> Self {
Self {
chunks: raw
.chunks
.into_iter()
.map(|c| Chunk { path: c.path, volume_index: c.volume_index })
.collect(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, rename_all = "camelCase"))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Chunk {
pub path: String,
pub volume_index: u32,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct OutputProfileList {
pub id: ImfUuid,
pub annotation: Option<String>,
pub issue_date: String,
pub issuer: Option<String>,
pub creator: Option<String>,
pub composition_playlist_id: ImfUuid,
}
impl OutputProfileList {
fn from_raw(raw: raw::OutputProfileList) -> Result<Self, AssetMapParseError> {
Ok(Self {
id: ImfUuid::parse(&raw.id)
.map_err(|source| AssetMapParseError::Field { field: "Id", source })?,
annotation: raw.annotation,
issue_date: raw.issue_date,
issuer: raw.issuer,
creator: raw.creator,
composition_playlist_id: ImfUuid::parse(&raw.composition_playlist_id)
.map_err(|source| AssetMapParseError::Field { field: "CompositionPlaylistId", source })?,
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct PackingList {
#[serde(skip)]
pub namespace: PklNamespace,
pub id: ImfUuid,
pub annotation_text: Option<String>,
pub issue_date: String,
pub issuer: Option<String>,
pub creator: Option<String>,
pub group_id: Option<ImfUuid>,
pub asset_list: PklAssetList,
}
impl PackingList {
fn from_raw(raw: raw::PackingList, namespace: PklNamespace) -> Result<Self, AssetMapParseError> {
let group_id = raw
.group_id
.map(|s| ImfUuid::parse(&s))
.transpose()
.map_err(|source| AssetMapParseError::Field { field: "GroupId", source })?;
Ok(Self {
namespace,
id: ImfUuid::parse(&raw.id)
.map_err(|source| AssetMapParseError::Field { field: "Id", source })?,
annotation_text: raw.annotation_text,
issue_date: raw.issue_date,
issuer: raw.issuer,
creator: raw.creator,
group_id,
asset_list: PklAssetList::from_raw(raw.asset_list)?,
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct PklAssetList {
pub assets: Vec<PklAsset>,
}
impl PklAssetList {
fn from_raw(raw: raw::PklAssetList) -> Result<Self, AssetMapParseError> {
let assets = raw
.assets
.into_iter()
.map(PklAsset::from_raw)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { assets })
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct PklAsset {
pub id: ImfUuid,
pub annotation_text: Option<String>,
pub hash: AssetHash,
pub size: u64,
pub mime_type: MimeType,
pub original_file_name: Option<String>,
}
impl PklAsset {
fn from_raw(raw: raw::PklAsset) -> Result<Self, AssetMapParseError> {
let algorithm = match &raw.hash_algorithm {
Some(dm) => HashAlgorithm::from_uri(&dm.algorithm).ok_or_else(|| {
AssetMapParseError::Field {
field: "HashAlgorithm",
source: ImfTypeError::InvalidHash(format!(
"unsupported hash algorithm URI: {}",
dm.algorithm
)),
}
})?,
None => HashAlgorithm::Sha1,
};
Ok(Self {
id: ImfUuid::parse(&raw.id)
.map_err(|source| AssetMapParseError::Field { field: "Id", source })?,
annotation_text: raw.annotation_text,
hash: AssetHash::from_base64(&raw.hash, algorithm)
.map_err(|source| AssetMapParseError::Field { field: "Hash", source })?,
size: raw.size,
mime_type: MimeType::parse(&raw.mime_type),
original_file_name: raw.original_file_name,
})
}
}
pub fn parse_assetmap(xml_content: &str) -> Result<AssetMap, AssetMapParseError> {
let namespace = detect_root_namespace(xml_content)
.map(|uri| AssetMapNamespace::from_uri(&uri))
.unwrap_or_default();
let raw: raw::AssetMap = quick_xml::de::from_str(xml_content)?;
AssetMap::from_raw(raw, namespace)
}
pub fn parse_pkl(xml_content: &str) -> Result<PackingList, AssetMapParseError> {
let namespace = detect_root_namespace(xml_content)
.map(|uri| PklNamespace::from_uri(&uri))
.unwrap_or_default();
let raw: raw::PackingList = quick_xml::de::from_str(xml_content)?;
PackingList::from_raw(raw, namespace)
}
pub fn parse_opl(xml_content: &str) -> Result<OutputProfileList, AssetMapParseError> {
let raw: raw::OutputProfileList = quick_xml::de::from_str(xml_content)?;
OutputProfileList::from_raw(raw)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
fn test_data(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../test-data").join(name)
}
#[test]
fn uuid_parse_urn_form() {
let id = ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap();
assert_eq!(id.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
assert_eq!(id.to_urn(), "urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
}
#[test]
fn uuid_parse_bare_form() {
let id = ImfUuid::parse("0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap();
assert_eq!(id.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
}
#[test]
fn uuid_parse_invalid() {
assert!(ImfUuid::parse("not-a-uuid").is_err());
assert!(ImfUuid::parse("").is_err());
assert!(ImfUuid::parse("urn:uuid:not-valid").is_err());
}
#[test]
fn uuid_roundtrip_serde() {
let id = ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, r#""0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85""#);
let back: ImfUuid = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
#[test]
fn uuid_deserialize_urn_from_json() {
let back: ImfUuid =
serde_json::from_str(r#""urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85""#).unwrap();
assert_eq!(back.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
}
#[test]
fn smpte_ul_parse_4group() {
let ul = SmpteUl::parse("060e2b34.04010106.04010101.03030000").unwrap();
assert_eq!(ul.0[0], 0x06);
assert_eq!(ul.0[7], 0x06); assert_eq!(ul.0[12], 0x03);
}
#[test]
fn smpte_ul_parse_urn_form() {
let ul = SmpteUl::parse("urn:smpte:ul:060e2b34.04010106.04010101.03030000").unwrap();
assert_eq!(ul.0[0], 0x06);
}
#[test]
fn smpte_ul_parse_5group_variant() {
let ul = SmpteUl::parse("urn:smpte:ul:060e2b34.0401.0101.04010101.01020000").unwrap();
assert_eq!(ul.0[4], 0x04);
assert_eq!(ul.0[5], 0x01);
}
#[test]
fn smpte_ul_version_agnostic_equality() {
let v1 = SmpteUl::parse("060e2b34.04010101.04010101.03030000").unwrap();
let v6 = SmpteUl::parse("060e2b34.04010106.04010101.03030000").unwrap();
let vd = SmpteUl::parse("060e2b34.0401010d.04010101.03030000").unwrap();
assert_eq!(v1, v6, "version 01 == version 06");
assert_eq!(v6, vd, "version 06 == version 0d");
}
#[test]
fn smpte_ul_different_items_not_equal() {
let a = SmpteUl::parse("060e2b34.04010106.04010101.03030000").unwrap();
let b = SmpteUl::parse("060e2b34.04010106.04010101.03040000").unwrap();
assert_ne!(a, b);
}
#[test]
fn smpte_ul_display_roundtrip() {
let ul = SmpteUl::parse("urn:smpte:ul:060e2b34.04010106.04010101.03030000").unwrap();
let s = ul.to_string();
assert!(s.starts_with("urn:smpte:ul:"));
let ul2 = SmpteUl::parse(&s).unwrap();
assert_eq!(ul, ul2);
}
#[test]
fn smpte_ul_parse_invalid() {
assert!(SmpteUl::parse("not-a-ul").is_err());
assert!(SmpteUl::parse("060e2b34.04010106").is_err()); }
#[test]
fn asset_hash_sha1_roundtrip() {
let b64 = "2jmj7l5rSw0yVb/vlWAYkK/YBwk=";
let h = AssetHash::from_base64_sha1(b64).unwrap();
assert_eq!(h.algorithm, HashAlgorithm::Sha1);
assert_eq!(h.bytes.len(), 20);
assert_eq!(h.to_base64(), b64);
}
#[test]
fn asset_hash_sha1_wrong_length_rejected() {
let err = AssetHash::from_base64_sha1("AAAA").unwrap_err();
assert!(err.to_string().contains("20 bytes"));
}
#[test]
fn asset_hash_sha256_roundtrip() {
let b64 = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
let h = AssetHash::from_base64_sha256(b64).unwrap();
assert_eq!(h.algorithm, HashAlgorithm::Sha256);
assert_eq!(h.bytes.len(), 32);
assert_eq!(h.to_base64(), b64);
}
#[test]
fn asset_hash_sha256_wrong_length_rejected() {
let err = AssetHash::from_base64_sha256("2jmj7l5rSw0yVb/vlWAYkK/YBwk=").unwrap_err();
assert!(err.to_string().contains("32 bytes"));
}
#[test]
fn asset_hash_from_base64_routes_correctly() {
let sha1_b64 = "2jmj7l5rSw0yVb/vlWAYkK/YBwk=";
let sha256_b64 = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
let h1 = AssetHash::from_base64(sha1_b64, HashAlgorithm::Sha1).unwrap();
assert_eq!(h1.algorithm, HashAlgorithm::Sha1);
let h2 = AssetHash::from_base64(sha256_b64, HashAlgorithm::Sha256).unwrap();
assert_eq!(h2.algorithm, HashAlgorithm::Sha256);
}
#[test]
fn asset_hash_invalid_base64() {
assert!(AssetHash::from_base64_sha1("not-valid-base64!!!").is_err());
}
#[test]
fn hash_algorithm_from_uri() {
assert_eq!(
HashAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#sha1"),
Some(HashAlgorithm::Sha1)
);
assert_eq!(
HashAlgorithm::from_uri("http://www.w3.org/2001/04/xmlenc#sha256"),
Some(HashAlgorithm::Sha256)
);
assert_eq!(HashAlgorithm::from_uri("http://example.com/unknown"), None);
}
#[test]
fn hash_algorithm_digest_len() {
assert_eq!(HashAlgorithm::Sha1.digest_len(), 20);
assert_eq!(HashAlgorithm::Sha256.digest_len(), 32);
}
#[test]
fn hash_algorithm_display() {
assert_eq!(HashAlgorithm::Sha1.to_string(), "SHA-1");
assert_eq!(HashAlgorithm::Sha256.to_string(), "SHA-256");
}
#[test]
fn volindex_parses_index_element() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<VolumeIndex xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
<Index>1</Index>
</VolumeIndex>"#;
let result = parse_volindex(xml).unwrap();
assert_eq!(result.index, 1);
}
#[test]
fn assetmap_id_is_imf_uuid() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<AssetMap xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
<Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
<AnnotationText>MERIDIAN</AnnotationText>
<Creator>Clipster 6.1.0.0 Beta (build 111500)</Creator>
<VolumeCount>1</VolumeCount>
<IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
<Issuer>R&S</Issuer>
<AssetList>
<Asset>
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<ChunkList>
<Chunk>
<Path>CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml</Path>
<VolumeIndex>1</VolumeIndex>
</Chunk>
</ChunkList>
</Asset>
<Asset>
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<PackingList>true</PackingList>
<ChunkList>
<Chunk>
<Path>PKL_f5e93462-aed2-44ad-a4ba-2adb65823e7c.xml</Path>
<VolumeIndex>1</VolumeIndex>
</Chunk>
</ChunkList>
</Asset>
</AssetList>
</AssetMap>"#;
let result = parse_assetmap(xml).unwrap();
assert_eq!(
result.id,
ImfUuid::parse("urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7").unwrap()
);
assert_eq!(result.annotation_text, Some("MERIDIAN".to_string()));
assert_eq!(result.volume_count, 1);
assert_eq!(result.asset_list.assets.len(), 2);
let cpl_asset = &result.asset_list.assets[0];
assert_eq!(
cpl_asset.id,
ImfUuid::parse("urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85").unwrap()
);
assert_eq!(cpl_asset.packing_list, None);
assert_eq!(
cpl_asset.chunk_list.chunks[0].path,
"CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml"
);
let pkl_asset = &result.asset_list.assets[1];
assert_eq!(pkl_asset.packing_list, Some(true));
assert_eq!(
pkl_asset.chunk_list.chunks[0].path,
"PKL_f5e93462-aed2-44ad-a4ba-2adb65823e7c.xml"
);
}
#[test]
fn assetmap_invalid_uuid_returns_field_error() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<AssetMap xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
<Id>not-a-valid-uuid</Id>
<VolumeCount>1</VolumeCount>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<ChunkList><Chunk><Path>foo.xml</Path></Chunk></ChunkList>
</Asset></AssetList>
</AssetMap>"#;
let err = parse_assetmap(xml).unwrap_err();
assert!(
matches!(err, AssetMapParseError::Field { field: "Id", .. }),
"expected Field error for Id, got: {err}"
);
}
#[test]
fn pkl_parses_assets_with_strong_types() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<AnnotationText>MERIDIAN</AnnotationText>
<IssueDate>2016-10-06T08:35:02-00:00</IssueDate>
<Issuer>R&S</Issuer>
<Creator>Clipster 6.1.0.0 Beta (build 111500)</Creator>
<AssetList>
<Asset>
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<AnnotationText>Meridian UHD 5994P</AnnotationText>
<Hash>IW0J5IZBsAxLMCCmWtHvfHhjVUw=</Hash>
<Size>15214</Size>
<Type>text/xml</Type>
<OriginalFileName>CPL_0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85.xml</OriginalFileName>
</Asset>
<Asset>
<Id>urn:uuid:61d91654-2650-4abf-abbc-ad2c7f640bf8</Id>
<Hash>fL7SnTeNskm71I4otXqr/T0D5LQ=</Hash>
<Size>79486353</Size>
<Type>application/mxf</Type>
<OriginalFileName>MERIDIAN_Netflix_Photon_161006_00.mxf</OriginalFileName>
</Asset>
</AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(
result.id,
ImfUuid::parse("urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c").unwrap()
);
assert_eq!(result.annotation_text, Some("MERIDIAN".to_string()));
assert_eq!(result.issuer, Some("R&S".to_string()));
assert_eq!(result.asset_list.assets.len(), 2);
let cpl_asset = &result.asset_list.assets[0];
assert_eq!(cpl_asset.hash.algorithm, HashAlgorithm::Sha1);
assert_eq!(cpl_asset.hash.bytes.len(), 20);
assert_eq!(cpl_asset.hash.to_base64(), "IW0J5IZBsAxLMCCmWtHvfHhjVUw=");
assert_eq!(cpl_asset.size, 15214);
assert_eq!(cpl_asset.mime_type, MimeType::TextXml);
assert!(cpl_asset.mime_type.is_xml());
let mxf_asset = &result.asset_list.assets[1];
assert_eq!(mxf_asset.mime_type, MimeType::ApplicationMxf);
assert!(mxf_asset.mime_type.is_mxf());
}
#[test]
fn pkl_explicit_sha1_hash_algorithm() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
<HashAlgorithm Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(result.asset_list.assets[0].hash.algorithm, HashAlgorithm::Sha1);
}
#[test]
fn pkl_sha256_hash_algorithm() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
<HashAlgorithm Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(result.asset_list.assets[0].hash.algorithm, HashAlgorithm::Sha256);
assert_eq!(result.asset_list.assets[0].hash.bytes.len(), 32);
}
#[test]
fn pkl_missing_hash_algorithm_defaults_to_sha1() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(result.asset_list.assets[0].hash.algorithm, HashAlgorithm::Sha1);
}
#[test]
fn pkl_with_group_id() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<GroupId>urn:uuid:aabbccdd-1122-3344-5566-778899aabbcc</GroupId>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(
result.group_id,
Some(ImfUuid::parse("urn:uuid:aabbccdd-1122-3344-5566-778899aabbcc").unwrap())
);
}
#[test]
fn pkl_unknown_mime_type_preserved() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>512</Size>
<Type>application/octet-stream</Type>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(
result.asset_list.assets[0].mime_type,
MimeType::Other("application/octet-stream".to_string())
);
}
#[test]
fn pkl_parses_with_2067_2_2016_namespace() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/2067-2/2016">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(result.asset_list.assets.len(), 1);
assert_eq!(result.namespace, PklNamespace::Smpte2067_2_2016);
assert_eq!(result.namespace.spec_id(), "ST 2067-2:2016");
}
#[test]
fn pkl_parses_with_2067_2_2020_namespace() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/ns/2067-2/2020">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(result.namespace, PklNamespace::Smpte2067_2_2020);
}
#[test]
fn pkl_detects_dci_429_8_namespace() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<PackingList xmlns="http://www.smpte-ra.org/schemas/429-8/2007/PKL">
<Id>urn:uuid:f5e93462-aed2-44ad-a4ba-2adb65823e7c</Id>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList><Asset>
<Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>
<Size>1024</Size>
<Type>application/mxf</Type>
</Asset></AssetList>
</PackingList>"#;
let result = parse_pkl(xml).unwrap();
assert_eq!(result.namespace, PklNamespace::Dci429_8);
}
#[test]
fn assetmap_parses_with_2067_9_namespace() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<AssetMap xmlns="http://www.smpte-ra.org/schemas/2067-9/2016">
<Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
<VolumeCount>1</VolumeCount>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList>
<Asset>
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<ChunkList><Chunk><Path>test.xml</Path></Chunk></ChunkList>
</Asset>
</AssetList>
</AssetMap>"#;
let result = parse_assetmap(xml).unwrap();
assert_eq!(result.asset_list.assets.len(), 1);
assert_eq!(result.namespace, AssetMapNamespace::Smpte2067_9_2016);
}
#[test]
fn assetmap_parses_with_2020_namespace() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<AssetMap xmlns="http://www.smpte-ra.org/ns/2067-9/2020">
<Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
<VolumeCount>1</VolumeCount>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList>
<Asset>
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<ChunkList><Chunk><Path>test.xml</Path></Chunk></ChunkList>
</Asset>
</AssetList>
</AssetMap>"#;
let result = parse_assetmap(xml).unwrap();
assert_eq!(result.namespace, AssetMapNamespace::Smpte2067_9_2020);
}
#[test]
fn assetmap_detects_dci_429_9_namespace() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<AssetMap xmlns="http://www.smpte-ra.org/schemas/429-9/2007/AM">
<Id>urn:uuid:75864667-c65e-4aae-a5b2-fa5ea5fe31b7</Id>
<VolumeCount>1</VolumeCount>
<IssueDate>2024-01-01T00:00:00Z</IssueDate>
<AssetList>
<Asset>
<Id>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</Id>
<ChunkList><Chunk><Path>test.xml</Path></Chunk></ChunkList>
</Asset>
</AssetList>
</AssetMap>"#;
let result = parse_assetmap(xml).unwrap();
assert_eq!(result.namespace, AssetMapNamespace::Dci429_9);
}
#[test]
fn opl_parses_core_metadata() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<OutputProfileList xmlns="http://www.smpte-ra.org/schemas/2067-100/2014">
<Id>urn:uuid:8cf83c32-4949-4f00-b081-01e12b18932f</Id>
<Annotation>OPL Example</Annotation>
<IssueDate>2016-06-14T19:22:37-00:00</IssueDate>
<Issuer>Clipster</Issuer>
<Creator>Clipster 5.9.3.7</Creator>
<CompositionPlaylistId>urn:uuid:0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85</CompositionPlaylistId>
<AliasList/>
<MacroList/>
</OutputProfileList>"#;
let result = parse_opl(xml).unwrap();
assert_eq!(result.id.to_string(), "8cf83c32-4949-4f00-b081-01e12b18932f");
assert_eq!(result.annotation.as_deref(), Some("OPL Example"));
assert_eq!(result.issuer.as_deref(), Some("Clipster"));
assert_eq!(result.creator.as_deref(), Some("Clipster 5.9.3.7"));
assert_eq!(result.composition_playlist_id.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
}
#[test]
fn opl_parses_real_test_file() {
let xml = std::fs::read_to_string(
test_data("OPL/OPL_8cf83c32-4949-4f00-b081-01e12b18932f.xml")
).unwrap();
let result = parse_opl(&xml).unwrap();
assert_eq!(result.id.to_string(), "8cf83c32-4949-4f00-b081-01e12b18932f");
assert_eq!(result.composition_playlist_id.to_string(), "0eb3d1b9-b77b-4d3f-bbe5-7c69b15dca85");
}
#[test]
fn opl_parses_isxd_test_file() {
let xml = std::fs::read_to_string(
test_data("ISXD/CompleteIMP/OPL_af6b288d-27e8-441f-9a36-2c4ab9025d19.xml")
).unwrap();
let result = parse_opl(&xml).unwrap();
assert_eq!(result.id.to_string(), "af6b288d-27e8-441f-9a36-2c4ab9025d19");
assert_eq!(result.composition_playlist_id.to_string(), "b2d74f92-1990-41e0-869f-2179a50f7090");
}
}