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)]
32 pub struct SidecarCompositionMap {
33 #[serde(rename = "Id")]
34 pub id: String,
35 #[serde(rename = "IssueDate")]
36 pub issue_date: String,
37 #[serde(rename = "Issuer")]
38 pub issuer: Option<String>,
39 #[serde(rename = "Creator")]
40 pub creator: Option<String>,
41 #[serde(rename = "Annotation")]
42 pub annotation: Option<String>,
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 SidecarAssetList {
55 #[serde(rename = "SidecarAsset", default)]
56 pub sidecar_assets: Vec<SidecarAsset>,
57 }
58
59 #[derive(Deserialize)]
62 pub struct SidecarAsset {
63 #[serde(rename = "Id")]
64 pub id: String,
65 #[serde(rename = "AssociatedCPLList")]
66 pub associated_cpl_list: Option<AssociatedCplList>,
67 }
68
69 #[derive(Deserialize, Default)]
70 pub struct AssociatedCplList {
71 #[serde(rename = "CPLId", default)]
72 pub cpl_ids: Vec<String>,
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, serde::Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct SidecarCompositionMap {
82 pub id: ImfUuid,
84 pub issue_date: String,
85 pub issuer: Option<String>,
86 pub creator: Option<String>,
87 pub annotation: Option<String>,
88 pub has_signer: bool,
90 pub has_signature: bool,
92 pub sidecar_assets: Vec<SidecarAsset>,
94}
95
96#[derive(Debug, Clone, PartialEq, serde::Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct SidecarAsset {
100 pub id: ImfUuid,
102 pub cpl_ids: Vec<ImfUuid>,
104}
105
106#[derive(Debug, Error)]
109pub enum ScmParseError {
110 #[error("XML parse error: {0}")]
111 Xml(#[from] quick_xml::DeError),
112 #[error("Invalid UUID '{0}': {1}")]
113 InvalidUuid(String, String),
114}
115
116pub fn parse_scm(xml: &str) -> Result<SidecarCompositionMap, ScmParseError> {
120 let raw: raw::SidecarCompositionMap = quick_xml::de::from_str(xml)?;
121
122 let id = ImfUuid::parse(&raw.id)
123 .map_err(|e| ScmParseError::InvalidUuid(raw.id.clone(), e.to_string()))?;
124
125 let sidecar_assets = raw
126 .sidecar_asset_list
127 .map(|list| list.sidecar_assets)
128 .unwrap_or_default()
129 .into_iter()
130 .map(|a| {
131 let asset_id = ImfUuid::parse(&a.id)
132 .map_err(|e| ScmParseError::InvalidUuid(a.id.clone(), e.to_string()))?;
133 let cpl_ids = a
134 .associated_cpl_list
135 .unwrap_or_default()
136 .cpl_ids
137 .into_iter()
138 .map(|s| {
139 ImfUuid::parse(&s)
140 .map_err(|e| ScmParseError::InvalidUuid(s.clone(), e.to_string()))
141 })
142 .collect::<Result<Vec<_>, ScmParseError>>()?;
143 Ok(SidecarAsset {
144 id: asset_id,
145 cpl_ids,
146 })
147 })
148 .collect::<Result<Vec<_>, ScmParseError>>()?;
149
150 Ok(SidecarCompositionMap {
151 id,
152 issue_date: raw.issue_date,
153 issuer: raw.issuer,
154 creator: raw.creator,
155 annotation: raw.annotation,
156 has_signer: raw.signer.is_some(),
157 has_signature: raw.signature.is_some(),
158 sidecar_assets,
159 })
160}
161
162#[cfg(test)]
165mod tests {
166 use super::*;
167 use pretty_assertions::assert_eq;
168
169 const MINIMAL_SCM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
170<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
171 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
172 <IssueDate>2024-01-01T00:00:00</IssueDate>
173 <SidecarAssetList>
174 <SidecarAsset>
175 <Id>urn:uuid:11111111-2222-3333-4444-555555555555</Id>
176 <AssociatedCPLList>
177 <CPLId>urn:uuid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CPLId>
178 </AssociatedCPLList>
179 </SidecarAsset>
180 </SidecarAssetList>
181</SidecarCompositionMap>"#;
182
183 #[test]
184 fn parses_minimal_scm() {
185 let scm = parse_scm(MINIMAL_SCM).unwrap();
186 assert_eq!(scm.id.to_string(), "a1b2c3d4-e5f6-7890-abcd-ef1234567890");
187 assert_eq!(scm.issue_date, "2024-01-01T00:00:00");
188 assert_eq!(scm.sidecar_assets.len(), 1);
189 assert_eq!(
190 scm.sidecar_assets[0].id.to_string(),
191 "11111111-2222-3333-4444-555555555555"
192 );
193 assert_eq!(scm.sidecar_assets[0].cpl_ids.len(), 1);
194 assert_eq!(
195 scm.sidecar_assets[0].cpl_ids[0].to_string(),
196 "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
197 );
198 }
199
200 #[test]
201 fn parses_multiple_cpl_ids() {
202 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
203<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
204 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
205 <IssueDate>2024-01-01T00:00:00</IssueDate>
206 <SidecarAssetList>
207 <SidecarAsset>
208 <Id>urn:uuid:11111111-2222-3333-4444-555555555555</Id>
209 <AssociatedCPLList>
210 <CPLId>urn:uuid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CPLId>
211 <CPLId>urn:uuid:bbbbbbbb-cccc-dddd-eeee-ffffffffffff</CPLId>
212 </AssociatedCPLList>
213 </SidecarAsset>
214 </SidecarAssetList>
215</SidecarCompositionMap>"#;
216 let scm = parse_scm(xml).unwrap();
217 assert_eq!(scm.sidecar_assets[0].cpl_ids.len(), 2);
218 assert_eq!(
219 scm.sidecar_assets[0].cpl_ids[1].to_string(),
220 "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
221 );
222 }
223
224 #[test]
225 fn parses_optional_fields() {
226 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
227<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
228 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
229 <IssueDate>2024-06-15T12:00:00</IssueDate>
230 <Issuer>Test Facility</Issuer>
231 <Creator>Test Tool 1.0</Creator>
232 <Annotation>IAB sidecar for main feature</Annotation>
233 <SidecarAssetList/>
234</SidecarCompositionMap>"#;
235 let scm = parse_scm(xml).unwrap();
236 assert_eq!(scm.issuer.as_deref(), Some("Test Facility"));
237 assert_eq!(scm.creator.as_deref(), Some("Test Tool 1.0"));
238 assert_eq!(
239 scm.annotation.as_deref(),
240 Some("IAB sidecar for main feature")
241 );
242 assert!(scm.sidecar_assets.is_empty());
243 assert!(!scm.has_signer);
244 assert!(!scm.has_signature);
245 }
246
247 #[test]
248 fn detects_signer_presence() {
249 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
250<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
251 <Id>urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
252 <IssueDate>2024-01-01T00:00:00</IssueDate>
253 <Signer><X509Data/></Signer>
254 <SidecarAssetList/>
255</SidecarCompositionMap>"#;
256 let scm = parse_scm(xml).unwrap();
257 assert!(scm.has_signer);
258 assert!(!scm.has_signature);
259 }
260
261 #[test]
262 fn rejects_malformed_xml() {
263 assert!(parse_scm("<not valid xml").is_err());
264 }
265
266 #[test]
267 fn rejects_invalid_uuid() {
268 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
269<SidecarCompositionMap xmlns="http://www.smpte-ra.org/ns/2067-9/2018">
270 <Id>not-a-uuid</Id>
271 <IssueDate>2024-01-01T00:00:00</IssueDate>
272</SidecarCompositionMap>"#;
273 assert!(parse_scm(xml).is_err());
274 }
275}