1pub mod codes;
10
11use crate::assetmap::ImfUuid;
12use thiserror::Error;
13
14mod raw {
17 use serde::{Deserialize, Deserializer};
18
19 #[derive(Debug)]
22 pub struct Present;
23
24 impl<'de> Deserialize<'de> for Present {
25 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
26 serde::de::IgnoredAny::deserialize(d)?;
27 Ok(Present)
28 }
29 }
30
31 #[derive(Deserialize)]
38 pub struct SidecarCompositionMap {
39 #[serde(rename = "Id")]
40 pub id: String,
41 #[serde(rename = "Properties")]
42 pub properties: Properties,
43 #[serde(rename = "Signer")]
45 pub signer: Option<Present>,
46 #[serde(rename = "Signature")]
48 pub signature: Option<Present>,
49 #[serde(rename = "SidecarAssetList")]
50 pub sidecar_asset_list: Option<SidecarAssetList>,
51 }
52
53 #[derive(Deserialize)]
54 pub struct Properties {
55 #[serde(rename = "Annotation")]
56 pub annotation: Option<String>,
57 #[serde(rename = "IssueDate")]
58 pub issue_date: String,
59 #[serde(rename = "Issuer")]
60 pub issuer: Option<String>,
61 }
62
63 #[derive(Deserialize)]
64 pub struct SidecarAssetList {
65 #[serde(rename = "SidecarAsset", default)]
66 pub sidecar_assets: Vec<SidecarAsset>,
67 }
68
69 #[derive(Deserialize)]
72 pub struct SidecarAsset {
73 #[serde(rename = "Id")]
74 pub id: String,
75 #[serde(rename = "AssociatedCPLList")]
76 pub associated_cpl_list: Option<AssociatedCplList>,
77 }
78
79 #[derive(Deserialize, Default)]
80 pub struct AssociatedCplList {
81 #[serde(rename = "CPLId", default)]
82 pub cpl_ids: Vec<String>,
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, serde::Serialize)]
95#[serde(rename_all = "camelCase")]
96pub struct SidecarCompositionMap {
97 pub id: ImfUuid,
99 pub issue_date: String,
100 pub issuer: Option<String>,
101 pub annotation: Option<String>,
102 pub has_signer: bool,
104 pub has_signature: bool,
106 pub sidecar_assets: Vec<SidecarAsset>,
108}
109
110#[derive(Debug, Clone, PartialEq, serde::Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct SidecarAsset {
114 pub id: ImfUuid,
116 pub cpl_ids: Vec<ImfUuid>,
118}
119
120#[derive(Debug, Error)]
123pub enum ScmParseError {
124 #[error("XML parse error: {0}")]
125 Xml(#[from] quick_xml::DeError),
126 #[error("Invalid UUID '{0}': {1}")]
127 InvalidUuid(String, String),
128}
129
130pub fn parse_scm(xml: &str) -> Result<SidecarCompositionMap, ScmParseError> {
134 let raw: raw::SidecarCompositionMap = quick_xml::de::from_str(xml)?;
135
136 let id = ImfUuid::parse_urn(&raw.id)
138 .map_err(|e| ScmParseError::InvalidUuid(raw.id.clone(), e.to_string()))?;
139
140 let sidecar_assets = raw
141 .sidecar_asset_list
142 .map(|list| list.sidecar_assets)
143 .unwrap_or_default()
144 .into_iter()
145 .map(|a| {
146 let asset_id = ImfUuid::parse_urn(&a.id)
147 .map_err(|e| ScmParseError::InvalidUuid(a.id.clone(), e.to_string()))?;
148 let cpl_ids = a
149 .associated_cpl_list
150 .unwrap_or_default()
151 .cpl_ids
152 .into_iter()
153 .map(|s| {
154 ImfUuid::parse_urn(&s)
155 .map_err(|e| ScmParseError::InvalidUuid(s.clone(), e.to_string()))
156 })
157 .collect::<Result<Vec<_>, ScmParseError>>()?;
158 Ok(SidecarAsset {
159 id: asset_id,
160 cpl_ids,
161 })
162 })
163 .collect::<Result<Vec<_>, ScmParseError>>()?;
164
165 Ok(SidecarCompositionMap {
166 id,
167 issue_date: raw.properties.issue_date,
168 issuer: raw.properties.issuer,
169 annotation: raw.properties.annotation,
170 has_signer: raw.signer.is_some(),
171 has_signature: raw.signature.is_some(),
172 sidecar_assets,
173 })
174}
175
176#[cfg(test)]
179mod tests {
180 use super::*;
181 use pretty_assertions::assert_eq;
182
183 const MINIMAL_SCM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
184<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
185 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
186 <Properties>
187 <IssueDate>2024-01-01T00:00:00</IssueDate>
188 </Properties>
189 <SidecarAssetList>
190 <SidecarAsset>
191 <Id>urn:uuid:11111111-2222-3333-4444-555555555555</Id>
192 <AssociatedCPLList>
193 <CPLId>urn:uuid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CPLId>
194 </AssociatedCPLList>
195 </SidecarAsset>
196 </SidecarAssetList>
197</SidecarCompositionMap>"#;
198
199 #[test]
200 fn parses_minimal_scm() {
201 let scm = parse_scm(MINIMAL_SCM).unwrap();
202 assert_eq!(scm.id.to_string(), "a1b2c3d4-e5f6-7890-abcd-ef1234567890");
203 assert_eq!(scm.issue_date, "2024-01-01T00:00:00");
204 assert_eq!(scm.sidecar_assets.len(), 1);
205 assert_eq!(
206 scm.sidecar_assets[0].id.to_string(),
207 "11111111-2222-3333-4444-555555555555"
208 );
209 assert_eq!(scm.sidecar_assets[0].cpl_ids.len(), 1);
210 assert_eq!(
211 scm.sidecar_assets[0].cpl_ids[0].to_string(),
212 "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
213 );
214 }
215
216 #[test]
217 fn parses_multiple_cpl_ids() {
218 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
219<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
220 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
221 <Properties>
222 <IssueDate>2024-01-01T00:00:00</IssueDate>
223 </Properties>
224 <SidecarAssetList>
225 <SidecarAsset>
226 <Id>urn:uuid:11111111-2222-3333-4444-555555555555</Id>
227 <AssociatedCPLList>
228 <CPLId>urn:uuid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CPLId>
229 <CPLId>urn:uuid:bbbbbbbb-cccc-dddd-eeee-ffffffffffff</CPLId>
230 </AssociatedCPLList>
231 </SidecarAsset>
232 </SidecarAssetList>
233</SidecarCompositionMap>"#;
234 let scm = parse_scm(xml).unwrap();
235 assert_eq!(scm.sidecar_assets[0].cpl_ids.len(), 2);
236 assert_eq!(
237 scm.sidecar_assets[0].cpl_ids[1].to_string(),
238 "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
239 );
240 }
241
242 #[test]
243 fn parses_optional_fields() {
244 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
249<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
250 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
251 <Properties>
252 <Annotation>IAB sidecar for main feature</Annotation>
253 <IssueDate>2024-06-15T12:00:00</IssueDate>
254 <Issuer>Test Facility</Issuer>
255 </Properties>
256 <SidecarAssetList/>
257</SidecarCompositionMap>"#;
258 let scm = parse_scm(xml).unwrap();
259 assert_eq!(scm.issuer.as_deref(), Some("Test Facility"));
260 assert_eq!(
261 scm.annotation.as_deref(),
262 Some("IAB sidecar for main feature")
263 );
264 assert!(scm.sidecar_assets.is_empty());
265 assert!(!scm.has_signer);
266 assert!(!scm.has_signature);
267 }
268
269 #[test]
270 fn detects_signer_presence() {
271 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
272<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
273 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
274 <Properties>
275 <IssueDate>2024-01-01T00:00:00</IssueDate>
276 </Properties>
277 <SidecarAssetList/>
278 <Signer><X509Data/></Signer>
279</SidecarCompositionMap>"#;
280 let scm = parse_scm(xml).unwrap();
281 assert!(scm.has_signer);
282 assert!(!scm.has_signature);
283 }
284
285 #[test]
286 fn rejects_malformed_xml() {
287 assert!(parse_scm("<not valid xml").is_err());
288 }
289
290 #[test]
291 fn rejects_invalid_uuid() {
292 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
293<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
294 <Id>not-a-uuid</Id>
295 <Properties>
296 <IssueDate>2024-01-01T00:00:00</IssueDate>
297 </Properties>
298</SidecarCompositionMap>"#;
299 assert!(parse_scm(xml).is_err());
300 }
301}