1mod directory;
4pub use directory::Directory;
5
6use alloc::string::String;
7use alloc::vec::Vec;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum ItemSemantic {
21 Primary,
23 GainMap,
25 DepthMap,
27 ConfidenceMap,
29 Other(String),
31}
32
33impl ItemSemantic {
34 #[must_use]
36 pub fn from_xmp(s: &str) -> Self {
37 match s {
38 "Primary" => Self::Primary,
39 "GainMap" => Self::GainMap,
40 "DepthMap" => Self::DepthMap,
41 "ConfidenceMap" => Self::ConfidenceMap,
42 other => Self::Other(String::from(other)),
43 }
44 }
45
46 #[must_use]
48 pub fn as_xmp_str(&self) -> &str {
49 match self {
50 Self::Primary => "Primary",
51 Self::GainMap => "GainMap",
52 Self::DepthMap => "DepthMap",
53 Self::ConfidenceMap => "ConfidenceMap",
54 Self::Other(s) => s.as_str(),
55 }
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
68#[non_exhaustive]
69pub struct ContainerItem {
70 pub semantic: ItemSemantic,
72 pub mime: String,
74 pub length: Option<usize>,
77 pub padding: Option<usize>,
80}
81
82impl ContainerItem {
83 #[must_use]
85 pub fn primary(mime: impl Into<String>) -> Self {
86 Self {
87 semantic: ItemSemantic::Primary,
88 mime: mime.into(),
89 length: None,
90 padding: None,
91 }
92 }
93
94 #[must_use]
96 pub fn secondary(semantic: ItemSemantic, mime: impl Into<String>, length: usize) -> Self {
97 Self {
98 semantic,
99 mime: mime.into(),
100 length: Some(length),
101 padding: None,
102 }
103 }
104}
105
106#[must_use]
114pub fn parse_container_items(xmp: &str) -> Vec<ContainerItem> {
115 let wrapped;
118 let xmp = if xmp.trim_start().starts_with("<Container:Directory") {
119 wrapped = alloc::format!(
120 r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:Container="http://ns.google.com/photos/1.0/container/" xmlns:Item="http://ns.google.com/photos/1.0/container/item/">{xmp}</rdf:Description></rdf:RDF>"#
121 );
122 wrapped.as_str()
123 } else {
124 xmp
125 };
126 crate::xmp::Packet::parse(xmp)
127 .ok()
128 .and_then(|p| {
129 let gain = super::gainmap::checked_container_items(&p).ok()?;
130 let depth = super::gainmap::checked_items_in_namespace(
131 &p,
132 "http://ns.google.com/photos/dd/1.0/container/",
133 "http://ns.google.com/photos/dd/1.0/item/",
134 )
135 .ok()?;
136 if gain.is_empty() {
137 Some(depth)
138 } else if depth.is_empty() {
139 Some(gain)
140 } else {
141 None
142 }
143 })
144 .unwrap_or_default()
145}
146
147#[must_use]
152pub fn generate_container_directory(items: &[ContainerItem]) -> String {
153 let mut xml = String::from(" <Container:Directory>\n <rdf:Seq>\n");
154 for item in items {
155 xml.push_str(" <rdf:li rdf:parseType=\"Resource\">\n");
156 xml.push_str(" <Container:Item\n");
157 xml.push_str(&alloc::format!(
158 " Item:Semantic=\"{}\"\n",
159 escape_xml_attribute(item.semantic.as_xmp_str())
160 ));
161 xml.push_str(&alloc::format!(
162 " Item:Mime=\"{}\"",
163 escape_xml_attribute(&item.mime)
164 ));
165 if let Some(length) = item.length {
166 xml.push_str(&alloc::format!(
167 "\n Item:Length=\"{length}\""
168 ));
169 }
170 if let Some(padding) = item.padding {
171 xml.push_str(&alloc::format!(
172 "\n Item:Padding=\"{padding}\""
173 ));
174 }
175 xml.push_str("/>\n");
176 xml.push_str(" </rdf:li>\n");
177 }
178 xml.push_str(" </rdf:Seq>\n </Container:Directory>");
179 xml
180}
181
182fn escape_xml_attribute(value: &str) -> String {
183 value
184 .replace('&', "&")
185 .replace('<', "<")
186 .replace('"', """)
187 .replace('\r', " ")
188 .replace('\n', " ")
189 .replace('\t', "	")
190}
191
192#[cfg(test)]
197mod tests {
198 use super::*;
199 use alloc::vec;
200
201 #[test]
202 fn item_semantic_roundtrip_known() {
203 for s in ["Primary", "GainMap", "DepthMap", "ConfidenceMap"] {
204 let parsed = ItemSemantic::from_xmp(s);
205 assert_eq!(parsed.as_xmp_str(), s);
206 }
207 }
208
209 #[test]
210 fn item_semantic_roundtrip_custom() {
211 let other = ItemSemantic::from_xmp("CustomVendorSemantic");
212 assert_eq!(other.as_xmp_str(), "CustomVendorSemantic");
213 }
214
215 #[test]
216 fn item_semantic_empty_string_is_other() {
217 let e = ItemSemantic::from_xmp("");
218 assert_eq!(e.as_xmp_str(), "");
219 }
220
221 #[test]
222 fn container_item_primary_constructor_has_no_length() {
223 let p = ContainerItem::primary("image/jpeg");
224 assert_eq!(p.semantic, ItemSemantic::Primary);
225 assert_eq!(p.mime, "image/jpeg");
226 assert!(p.length.is_none());
227 assert!(p.padding.is_none());
228 }
229
230 #[test]
231 fn container_item_secondary_constructor_sets_length() {
232 let g = ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", 5000);
233 assert_eq!(g.length, Some(5000));
234 assert_eq!(g.semantic, ItemSemantic::GainMap);
235 }
236
237 #[test]
238 fn parse_container_items_ultrahdr_shape() {
239 let xmp = r#"
240 <Container:Directory>
241 <rdf:Seq>
242 <rdf:li rdf:parseType="Resource">
243 <Container:Item
244 Item:Semantic="Primary"
245 Item:Mime="image/jpeg"/>
246 </rdf:li>
247 <rdf:li rdf:parseType="Resource">
248 <Container:Item
249 Item:Semantic="GainMap"
250 Item:Mime="image/jpeg"
251 Item:Length="5000"/>
252 </rdf:li>
253 </rdf:Seq>
254 </Container:Directory>"#;
255 let items = parse_container_items(xmp);
256 assert_eq!(items.len(), 2);
257 assert_eq!(items[0].semantic, ItemSemantic::Primary);
258 assert_eq!(items[0].mime, "image/jpeg");
259 assert!(items[0].length.is_none());
260 assert_eq!(items[1].semantic, ItemSemantic::GainMap);
261 assert_eq!(items[1].length, Some(5000));
262 }
263
264 #[test]
265 fn parse_container_items_android_ddf_shape() {
266 let xmp = r#"
267 <Container:Directory>
268 <rdf:Seq>
269 <rdf:li rdf:parseType="Resource">
270 <Container:Item Item:Semantic="Primary" Item:Mime="image/jpeg"/>
271 </rdf:li>
272 <rdf:li rdf:parseType="Resource">
273 <Container:Item Item:Semantic="DepthMap" Item:Mime="image/jpeg" Item:Length="8000"/>
274 </rdf:li>
275 <rdf:li rdf:parseType="Resource">
276 <Container:Item Item:Semantic="ConfidenceMap" Item:Mime="image/jpeg" Item:Length="3000"/>
277 </rdf:li>
278 </rdf:Seq>
279 </Container:Directory>"#;
280 let items = parse_container_items(xmp);
281 assert_eq!(items.len(), 3);
282 assert_eq!(items[1].semantic, ItemSemantic::DepthMap);
283 assert_eq!(items[2].semantic, ItemSemantic::ConfidenceMap);
284 }
285
286 #[test]
287 fn parse_container_items_with_padding() {
288 let xmp = r#"
289 <Container:Directory>
290 <rdf:Seq>
291 <rdf:li rdf:parseType="Resource">
292 <Container:Item Item:Semantic="Primary" Item:Mime="image/jpeg"/>
293 </rdf:li>
294 <rdf:li rdf:parseType="Resource">
295 <Container:Item Item:Semantic="GainMap" Item:Mime="image/jpeg" Item:Length="5000" Item:Padding="16"/>
296 </rdf:li>
297 </rdf:Seq>
298 </Container:Directory>"#;
299 let items = parse_container_items(xmp);
300 assert_eq!(items[1].padding, Some(16));
301 }
302
303 #[test]
304 fn parse_container_items_empty_string() {
305 assert!(parse_container_items("").is_empty());
306 }
307
308 #[test]
309 fn parse_container_items_missing_attrs_skips_item() {
310 let xmp = r#"
313 <Container:Directory>
314 <rdf:Seq>
315 <rdf:li rdf:parseType="Resource">
316 <Container:Item Item:Mime="image/jpeg"/>
317 </rdf:li>
318 </rdf:Seq>
319 </Container:Directory>"#;
320 assert!(parse_container_items(xmp).is_empty());
321 }
322
323 #[test]
324 fn generate_container_directory_roundtrip_two_items() {
325 let items = vec![
326 ContainerItem::primary("image/jpeg"),
327 ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", 5000),
328 ];
329 let xml = generate_container_directory(&items);
330 let parsed = parse_container_items(&xml);
331 assert_eq!(parsed.len(), 2);
332 assert_eq!(parsed[0].semantic, ItemSemantic::Primary);
333 assert_eq!(parsed[1].semantic, ItemSemantic::GainMap);
334 assert_eq!(parsed[1].length, Some(5000));
335 }
336
337 #[test]
338 fn generate_container_directory_roundtrip_preserves_padding() {
339 let items = vec![
340 ContainerItem::primary("image/jpeg"),
341 ContainerItem {
342 semantic: ItemSemantic::GainMap,
343 mime: String::from("image/jpeg"),
344 length: Some(1000),
345 padding: Some(32),
346 },
347 ];
348 let xml = generate_container_directory(&items);
349 let parsed = parse_container_items(&xml);
350 assert_eq!(parsed[1].padding, Some(32));
351 }
352
353 #[test]
354 fn generate_container_directory_includes_all_fields() {
355 let items = vec![
356 ContainerItem::primary("image/jpeg"),
357 ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", 5000),
358 ContainerItem::secondary(ItemSemantic::DepthMap, "image/png", 12000),
359 ];
360 let xml = generate_container_directory(&items);
361 assert!(xml.contains("GainMap"));
362 assert!(xml.contains("DepthMap"));
363 assert!(xml.contains("image/png"));
364 assert!(xml.contains("12000"));
365 }
366
367 use proptest::prelude::*;
372
373 fn arb_item_semantic() -> impl Strategy<Value = ItemSemantic> {
374 prop_oneof![
375 Just(ItemSemantic::Primary),
376 Just(ItemSemantic::GainMap),
377 Just(ItemSemantic::DepthMap),
378 Just(ItemSemantic::ConfidenceMap),
379 "[A-Za-z][A-Za-z0-9_-]{0,15}".prop_map(ItemSemantic::Other),
383 ]
384 }
385
386 fn arb_container_item() -> impl Strategy<Value = ContainerItem> {
387 (
388 arb_item_semantic(),
389 prop_oneof![
390 Just(String::from("image/jpeg")),
391 Just(String::from("image/png")),
392 Just(String::from("image/heic")),
393 Just(String::from("application/octet-stream")),
394 ],
395 proptest::option::of(1usize..10_000_000),
396 proptest::option::of(0usize..1024),
397 )
398 .prop_map(|(semantic, mime, length, padding)| ContainerItem {
399 semantic,
400 mime,
401 length,
402 padding,
403 })
404 }
405
406 proptest! {
407 #[test]
414 fn container_directory_roundtrip(
415 items in proptest::collection::vec(arb_container_item(), 1..5),
416 ) {
417 let xml = generate_container_directory(&items);
418 let parsed = parse_container_items(&xml);
419 prop_assert_eq!(parsed.len(), items.len());
420 for (i, (orig, got)) in items.iter().zip(parsed.iter()).enumerate() {
421 prop_assert_eq!(
422 &orig.semantic, &got.semantic,
423 "item {}: semantic", i,
424 );
425 prop_assert_eq!(&orig.mime, &got.mime, "item {}: mime", i);
426 prop_assert_eq!(orig.length, got.length, "item {}: length", i);
427 prop_assert_eq!(orig.padding, got.padding, "item {}: padding", i);
428 }
429 }
430 }
431}