zenmeta/xmp/container/
directory.rs1use super::{ContainerItem, ItemSemantic};
4use crate::xmp::{Error, Packet, SpannedProperty};
5use alloc::{
6 string::{String, ToString},
7 vec::Vec,
8};
9use core::ops::Range;
10
11const MAX_ITEMS: usize = 256;
12
13struct Entry {
14 item: ContainerItem,
15 length: Option<Range<usize>>,
16 padding: Option<Range<usize>>,
17}
18
19pub struct Directory<'a> {
26 xml: &'a str,
27 entries: Vec<Entry>,
28}
29
30impl<'a> Directory<'a> {
31 pub fn parse(xml: &'a str) -> Result<Self, Error> {
34 let packet = Packet::parse(xml)?;
35 let mut entries = Vec::new();
36 for (container_ns, item_ns) in [
37 (
38 super::super::gainmap::CONTAINER_NAMESPACE,
39 super::super::gainmap::ITEM_NAMESPACE,
40 ),
41 (
42 "http://ns.google.com/photos/dd/1.0/container/",
43 "http://ns.google.com/photos/dd/1.0/item/",
44 ),
45 ] {
46 let count = packet
47 .doc
48 .descendants()
49 .filter(|n| n.has_tag_name((container_ns, "Directory")))
50 .count();
51 if count > 1
52 || packet
53 .doc
54 .descendants()
55 .any(|n| n.attribute((container_ns, "Directory")).is_some())
56 {
57 return Err(Error::Duplicate("Directory".into()));
58 }
59 let items = packet.resource_sequence_spanned(
60 container_ns,
61 "Directory",
62 container_ns,
63 "Item",
64 MAX_ITEMS,
65 )?;
66 if count != 0 && items.is_empty() {
67 return Err(Error::Structure("empty or misplaced Directory".into()));
68 }
69 if !entries.is_empty() && !items.is_empty() {
70 return Err(Error::Duplicate("Directory namespaces".into()));
71 }
72 for fields in items {
73 let field = |name| {
74 fields
75 .iter()
76 .find(|f| f.property.namespace() == item_ns && f.property.name() == name)
77 };
78 let required = |name| {
79 field(name)
80 .map(|f| f.property.value())
81 .filter(|s| !s.is_empty())
82 .ok_or_else(|| Error::Structure(alloc::format!("missing item {name}")))
83 };
84 let semantic = required("Semantic")?;
85 let mime = required("Mime")?;
86 if entries
87 .iter()
88 .any(|e: &Entry| e.item.semantic.as_xmp_str() == semantic)
89 {
90 return Err(Error::Duplicate("item semantic".into()));
91 }
92 if (semantic == "Primary") != entries.is_empty() {
93 return Err(Error::Structure(
94 "primary must be the first directory item".into(),
95 ));
96 }
97 let number = |name| -> Result<Option<usize>, Error> {
98 field(name)
99 .map(|f| {
100 let value = f.property.value();
101 if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
102 return Err(Error::Structure(alloc::format!(
103 "invalid item {name}"
104 )));
105 }
106 value.parse().map_err(|_| {
107 Error::Structure(alloc::format!("invalid item {name}"))
108 })
109 })
110 .transpose()
111 };
112 let length = number("Length")?;
113 let padding = number("Padding")?;
114 if semantic != "Primary" && length.is_none() {
115 return Err(Error::Structure("secondary item requires Length".into()));
116 }
117 entries.push(Entry {
118 item: ContainerItem {
119 semantic: ItemSemantic::from_xmp(semantic),
120 mime: mime.into(),
121 length,
122 padding,
123 },
124 length: field("Length").map(|f| value_range(xml, f)).transpose()?,
125 padding: field("Padding").map(|f| value_range(xml, f)).transpose()?,
126 });
127 }
128 }
129 Ok(Self { xml, entries })
130 }
131
132 pub fn items(&self) -> impl ExactSizeIterator<Item = &ContainerItem> {
134 self.entries.iter().map(|e| &e.item)
135 }
136
137 pub fn rewrite_item_lengths(&self, secondaries: &[(&str, usize)]) -> Result<String, Error> {
145 let mut edits = Vec::new();
146 for entry in &self.entries {
147 if entry.item.semantic == ItemSemantic::Primary {
148 continue;
149 }
150 let mut matches = secondaries
151 .iter()
152 .filter(|(semantic, _)| *semantic == entry.item.semantic.as_xmp_str());
153 let (_, length) = matches
154 .next()
155 .ok_or_else(|| Error::Structure("directory item has no carried payload".into()))?;
156 if matches.next().is_some() {
157 return Err(Error::Duplicate("carried item semantic".into()));
158 }
159 edits.push((
160 entry
161 .length
162 .clone()
163 .ok_or_else(|| Error::Structure("secondary Length".into()))?,
164 length.to_string(),
165 ));
166 if let Some(range) = &entry.padding {
167 edits.push((range.clone(), "0".into()));
168 }
169 }
170 edits.sort_unstable_by_key(|(r, _)| r.start);
171 let mut size = self.xml.len();
172 let mut last = 0;
173 for (range, replacement) in &edits {
174 if range.start < last || range.end > self.xml.len() || range.start > range.end {
175 return Err(Error::Structure("overlapping directory edits".into()));
176 }
177 size = size
178 .checked_sub(range.len())
179 .and_then(|s| s.checked_add(replacement.len()))
180 .ok_or(Error::Limit)?;
181 last = range.end;
182 }
183 if size > super::super::gainmap::MAX_XMP_LENGTH {
184 return Err(Error::Limit);
185 }
186 let mut out = String::new();
187 out.try_reserve_exact(size).map_err(|_| Error::Limit)?;
188 last = 0;
189 for (range, replacement) in edits {
190 out.push_str(&self.xml[last..range.start]);
191 out.push_str(&replacement);
192 last = range.end;
193 }
194 out.push_str(&self.xml[last..]);
195 Ok(out)
196 }
197}
198
199fn value_range(xml: &str, field: &SpannedProperty) -> Result<Range<usize>, Error> {
203 let source = &xml[field.source.clone()];
204 let invalid = || Error::Structure("directory value span".into());
205 if field.attribute {
206 let first = source
207 .bytes()
208 .position(|b| b == b'\'' || b == b'"')
209 .ok_or_else(invalid)?;
210 if source.as_bytes().last() != source.as_bytes().get(first) {
211 return Err(invalid());
212 }
213 Ok(field.source.start + first + 1..field.source.end - 1)
214 } else {
215 let mut quote = None;
216 let mut begin = None;
217 for (i, b) in source.bytes().enumerate() {
218 if let Some(q) = quote {
219 if b == q {
220 quote = None;
221 }
222 } else if b == b'\'' || b == b'"' {
223 quote = Some(b);
224 } else if b == b'>' {
225 begin = Some(i + 1);
226 break;
227 }
228 }
229 let start = begin.ok_or_else(invalid)?;
230 let end = source.rfind("</").ok_or_else(invalid)?;
231 if start > end {
232 return Err(invalid());
233 }
234 Ok(field.source.start + start..field.source.start + end)
235 }
236}