Skip to main content

zenmeta/
xmp.rs

1//! Namespace-aware XMP inspection. XML prefixes are aliases, never identities.
2//!
3//! This is a bounded XML reader with selected RDF property access, not a complete
4//! RDF reasoner or an Adobe alias-normalization engine. Unknown structures remain
5//! visible in the audit snapshot. DTDs/external entities are rejected. Reading
6//! metadata never reads, scans or allocates image pixels.
7pub mod container;
8pub mod gainmap;
9
10use alloc::{
11    collections::BTreeMap,
12    format,
13    string::{String, ToString},
14    vec::Vec,
15};
16use roxmltree::{Document, Node, ParsingOptions};
17
18pub(crate) const RDF: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
19
20/// Element nesting a packet may use; [`Packet::parse`] rejects deeper ones.
21const MAX_NESTING: usize = 64;
22
23/// Whether `xml` nests elements deeper than `limit`, found by a linear scan
24/// that uses no stack per level. It skips comments, CDATA sections,
25/// processing instructions, declarations and quoted attribute values, so it
26/// counts a well-formed document's element depth exactly; malformed input it
27/// cannot follow is left for the XML parser to reject.
28fn nests_deeper_than(xml: &str, limit: usize) -> bool {
29    fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
30        haystack.windows(needle.len()).position(|w| w == needle)
31    }
32    let b = xml.as_bytes();
33    let mut i = 0;
34    let mut depth = 0usize;
35    while let Some(lt) = b[i..].iter().position(|&c| c == b'<') {
36        i += lt;
37        let rest = &b[i..];
38        let skip = if rest.starts_with(b"<!--") {
39            find(rest, b"-->").map(|e| e + 3)
40        } else if rest.starts_with(b"<![CDATA[") {
41            find(rest, b"]]>").map(|e| e + 3)
42        } else if rest.starts_with(b"<?") {
43            find(rest, b"?>").map(|e| e + 2)
44        } else if rest.starts_with(b"<!") {
45            find(rest, b">").map(|e| e + 1)
46        } else {
47            None
48        };
49        if rest.starts_with(b"<!") || rest.starts_with(b"<?") {
50            match skip {
51                Some(n) => {
52                    i += n;
53                    continue;
54                }
55                None => return false,
56            }
57        }
58        // A start or end tag: find its `>` outside quoted attribute values.
59        let mut quote = None;
60        let mut end = None;
61        for (j, &c) in rest.iter().enumerate().skip(1) {
62            match quote {
63                Some(q) if c == q => quote = None,
64                Some(_) => {}
65                None if c == b'"' || c == b'\'' => quote = Some(c),
66                None if c == b'>' => {
67                    end = Some(j);
68                    break;
69                }
70                None => {}
71            }
72        }
73        let Some(end) = end else {
74            return false;
75        };
76        if rest.get(1) == Some(&b'/') {
77            depth = depth.saturating_sub(1);
78        } else if rest[end - 1] != b'/' {
79            depth += 1;
80            if depth > limit {
81                return true;
82            }
83        }
84        i += end + 1;
85    }
86    false
87}
88
89/// Invalid, ambiguous, unsupported or over-budget metadata.
90#[derive(Clone, Debug, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum Error {
93    /// Byte, node or nesting budget exceeded.
94    Limit,
95    /// XML syntax error (including forbidden DTDs).
96    Xml(String),
97    /// Multiple declarations of a property; choosing one could change rendering.
98    Duplicate(String),
99    /// Selected property uses an unsupported RDF shape.
100    Structure(String),
101}
102impl core::fmt::Display for Error {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        write!(f, "XMP: {self:?}")
105    }
106}
107impl core::error::Error for Error {}
108
109/// One XML field in a namespace-resolved structural snapshot.
110/// Paths include sibling occurrence indexes, preserving duplicates and arrays.
111#[derive(Clone, Debug, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct Field {
114    pub(crate) path: String,
115    pub(crate) value: String,
116}
117impl Field {
118    /// Expanded namespace path; prefix spelling is immaterial.
119    pub fn path(&self) -> &str {
120        &self.path
121    }
122    /// Decoded attribute/text value, or an empty value for an element marker.
123    pub fn value(&self) -> &str {
124        &self.value
125    }
126}
127
128/// A namespace-qualified scalar in an RDF resource structure.
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct Property {
131    namespace: String,
132    name: String,
133    value: String,
134}
135impl Property {
136    /// Namespace URI, independent of prefix spelling.
137    pub fn namespace(&self) -> &str {
138        &self.namespace
139    }
140    /// Local property name.
141    pub fn name(&self) -> &str {
142        &self.name
143    }
144    /// Decoded scalar value.
145    pub fn value(&self) -> &str {
146        &self.value
147    }
148}
149
150// Source positions stay separate from semantic equality of public Property values.
151pub(crate) struct SpannedProperty {
152    pub property: Property,
153    pub source: core::ops::Range<usize>,
154    pub attribute: bool,
155}
156
157/// Bounded parsed XMP packet, borrowing its source XML.
158#[derive(Debug)]
159pub struct Packet<'a> {
160    doc: Document<'a>,
161}
162impl<'a> Packet<'a> {
163    /// Parse at most 16 MiB / 65,536 XML nodes / 64 nested elements.
164    /// Unknown XML is inspectable, but selected property access is strict.
165    pub fn parse(xml: &'a str) -> Result<Self, Error> {
166        if xml.len() > 16 * 1024 * 1024 {
167            return Err(Error::Limit);
168        }
169        // roxmltree's parser recurses once per open element, so the nesting
170        // check after parsing comes too late: a packet nesting thousands of
171        // elements (closed or not) exhausts the thread's stack first, and the
172        // process aborts. Check the depth before handing it the XML.
173        if nests_deeper_than(xml, MAX_NESTING) {
174            return Err(Error::Limit);
175        }
176        let doc = Document::parse_with_options(
177            xml,
178            ParsingOptions {
179                allow_dtd: false,
180                nodes_limit: 65_536,
181                ..Default::default()
182            },
183        )
184        .map_err(|e| Error::Xml(e.to_string()))?;
185        if doc
186            .descendants()
187            .any(|n| n.ancestors().take(66).count() > 65)
188        {
189            return Err(Error::Limit);
190        }
191        Ok(Self { doc })
192    }
193
194    /// Read a property of the document's empty-subject RDF Description.
195    /// Accepts a scalar attribute/element or an rdf:Seq array of plain values.
196    /// Duplicate declarations, alternate subjects, qualifiers and mixed content
197    /// refuse instead of selecting a possibly different rendering interpretation.
198    pub fn property(&self, namespace: &str, name: &str) -> Result<Option<Vec<String>>, Error> {
199        let mut result = None;
200        for d in self.descriptions() {
201            let attribute = d.attribute((namespace, name));
202            let children: Vec<_> = d
203                .children()
204                .filter(|n| n.has_tag_name((namespace, name)))
205                .collect();
206            if attribute.is_none() && children.is_empty() {
207                continue;
208            }
209            empty_subject(d)?;
210            if let Some(value) = attribute
211                && result.replace(alloc::vec![value.into()]).is_some()
212            {
213                return Err(Error::Duplicate(name.into()));
214            }
215            for n in children {
216                if result.replace(property_values(n)?).is_some() {
217                    return Err(Error::Duplicate(name.into()));
218                }
219            }
220        }
221        Ok(result)
222    }
223
224    fn descriptions(&self) -> impl Iterator<Item = Node<'_, 'a>> {
225        self.doc.descendants().filter(|n| {
226            n.has_tag_name((RDF, "Description"))
227                && n.parent().is_some_and(|p| {
228                    p.has_tag_name((RDF, "RDF"))
229                        && (p == self.doc.root_element()
230                            || p.parent().is_some_and(|root| {
231                                root == self.doc.root_element()
232                                    && root.has_tag_name(("adobe:ns:meta/", "xmpmeta"))
233                            }))
234                })
235        })
236    }
237
238    /// Read a sequence of resource structures (e.g. GContainer Directory).
239    /// Each item contains namespace-qualified scalar properties. Unknown scalar
240    /// fields are returned too; the caller chooses its schema, never its prefix.
241    pub fn resource_sequence(
242        &self,
243        namespace: &str,
244        name: &str,
245        item_namespace: &str,
246        item_name: &str,
247    ) -> Result<Vec<Vec<Property>>, Error> {
248        self.resource_sequence_spanned(namespace, name, item_namespace, item_name, 4096)
249            .map(|items| {
250                items
251                    .into_iter()
252                    .map(|fields| fields.into_iter().map(|f| f.property).collect())
253                    .collect()
254            })
255    }
256
257    pub(crate) fn resource_sequence_spanned(
258        &self,
259        namespace: &str,
260        name: &str,
261        item_namespace: &str,
262        item_name: &str,
263        max_items: usize,
264    ) -> Result<Vec<Vec<SpannedProperty>>, Error> {
265        if self
266            .descriptions()
267            .any(|d| d.attribute((namespace, name)).is_some())
268        {
269            return Err(Error::Structure(name.into()));
270        }
271        let nodes: Vec<_> = self
272            .descriptions()
273            .flat_map(|d| d.children())
274            .filter(|n| n.has_tag_name((namespace, name)))
275            .collect();
276        if nodes.len() > 1 {
277            return Err(Error::Duplicate(name.into()));
278        }
279        let Some(node) = nodes.first() else {
280            return Ok(Vec::new());
281        };
282        empty_subject(node.parent().ok_or_else(|| Error::Structure(name.into()))?)?;
283        structural_node(*node, false)?;
284        let seq = only_child(*node)
285            .filter(|n| n.has_tag_name((RDF, "Seq")))
286            .ok_or_else(|| Error::Structure(name.into()))?;
287        structural_node(seq, false)?;
288        let mut items = Vec::new();
289        for li in seq.children().filter(Node::is_element) {
290            if !li.has_tag_name((RDF, "li")) {
291                return Err(Error::Structure(name.into()));
292            }
293            if items.len() >= max_items {
294                return Err(Error::Limit);
295            }
296            structural_node(li, true)?;
297            let item = only_child(li).ok_or_else(|| Error::Structure(name.into()))?;
298            if !item.has_tag_name((item_namespace, item_name))
299                || item
300                    .children()
301                    .any(|c| c.is_text() && c.text().is_some_and(|s| !s.trim().is_empty()))
302            {
303                return Err(Error::Structure(name.into()));
304            }
305            let mut fields = Vec::new();
306            for a in item.attributes() {
307                if a.namespace() == Some(RDF) {
308                    return Err(Error::Structure(name.into()));
309                }
310                fields.push(SpannedProperty {
311                    property: Property {
312                        namespace: a.namespace().unwrap_or("").into(),
313                        name: a.name().into(),
314                        value: a.value().into(),
315                    },
316                    source: a.range(),
317                    attribute: true,
318                });
319            }
320            for child in item.children().filter(Node::is_element) {
321                fields.push(SpannedProperty {
322                    property: Property {
323                        namespace: child.tag_name().namespace().unwrap_or("").into(),
324                        name: child.tag_name().name().into(),
325                        value: plain_text(child)?,
326                    },
327                    source: child.range(),
328                    attribute: false,
329                });
330            }
331            if fields.len() > 256 {
332                return Err(Error::Limit);
333            }
334            for (i, a) in fields.iter().enumerate() {
335                if fields[..i].iter().any(|b| {
336                    a.property.namespace == b.property.namespace
337                        && a.property.name == b.property.name
338                }) {
339                    return Err(Error::Duplicate(a.property.name.clone()));
340                }
341            }
342            items.push(fields);
343        }
344        Ok(items)
345    }
346
347    /// Full XML structural snapshot: elements, attributes, text, comments and
348    /// processing instructions. Namespace declarations are represented by resolved
349    /// names, not prefix spelling. No unknown application property is discarded.
350    /// This is structural comparison, not equivalence of arbitrary RDF graphs.
351    pub fn fields(&self) -> Result<Vec<Field>, Error> {
352        let mut out = Vec::new();
353        let mut paths = BTreeMap::<u32, String>::new();
354        let mut occurrences = BTreeMap::<(u32, String), usize>::new();
355        let mut budget = 0usize;
356        for n in self.doc.descendants().filter(|n| !n.is_root()) {
357            let parent = n.parent().map(|p| p.id().get()).unwrap_or(0);
358            let name = if n.is_element() {
359                format!(
360                    "{{{}}}{}",
361                    n.tag_name().namespace().unwrap_or(""),
362                    n.tag_name().name()
363                )
364            } else if n.is_comment() {
365                "#comment".into()
366            } else if n.is_pi() {
367                "#pi".into()
368            } else {
369                "#text".into()
370            };
371            let index = occurrences.entry((parent, name.clone())).or_default();
372            let path = format!(
373                "{}/{name}[{index}]",
374                paths.get(&parent).map(String::as_str).unwrap_or("")
375            );
376            *index += 1;
377            budget = budget.saturating_add(path.len() * 2);
378            if budget > 16 * 1024 * 1024 {
379                return Err(Error::Limit);
380            }
381            paths.insert(n.id().get(), path.clone());
382            if n.is_element() {
383                out.push(Field {
384                    path: path.clone(),
385                    value: String::new(),
386                });
387                for a in n.attributes() {
388                    budget = budget.saturating_add(
389                        path.len()
390                            + a.namespace().unwrap_or("").len()
391                            + a.name().len()
392                            + a.value().len(),
393                    );
394                    if budget > 16 * 1024 * 1024 {
395                        return Err(Error::Limit);
396                    }
397                    out.push(Field {
398                        path: format!("{path}/@{{{}}}{}", a.namespace().unwrap_or(""), a.name()),
399                        value: a.value().into(),
400                    });
401                }
402            } else if let Some(pi) = n.pi() {
403                out.push(Field {
404                    path,
405                    value: format!("{} {}", pi.target, pi.value.unwrap_or("")),
406                });
407            } else if let Some(text) = n.text() {
408                // Formatting whitespace between elements is not a property.
409                if n.is_comment()
410                    || !text.trim().is_empty()
411                    || n.parent()
412                        .is_some_and(|p| !p.children().any(|c| c.is_element()))
413                {
414                    out.push(Field {
415                        path,
416                        value: text.into(),
417                    });
418                }
419            }
420        }
421        out.sort_by(|a, b| a.path.cmp(&b.path));
422        Ok(out)
423    }
424}
425fn only_child<'a, 'input>(n: Node<'a, 'input>) -> Option<Node<'a, 'input>> {
426    let mut it = n.children().filter(Node::is_element);
427    let first = it.next()?;
428    it.next().is_none().then_some(first)
429}
430fn plain_text(n: Node<'_, '_>) -> Result<String, Error> {
431    if n.children().any(|c| c.is_element()) || n.attributes().len() != 0 {
432        return Err(Error::Structure(n.tag_name().name().into()));
433    }
434    Ok(n.children()
435        .filter(Node::is_text)
436        .filter_map(|c| c.text())
437        .collect())
438}
439fn property_values(n: Node<'_, '_>) -> Result<Vec<String>, Error> {
440    if n.children().any(|c| c.is_element()) {
441        if n.attributes().len() != 0
442            || n.children()
443                .any(|c| c.is_text() && c.text().is_some_and(|s| !s.trim().is_empty()))
444        {
445            return Err(Error::Structure(n.tag_name().name().into()));
446        }
447        let seq = only_child(n)
448            .filter(|c| c.has_tag_name((RDF, "Seq")))
449            .ok_or_else(|| Error::Structure(n.tag_name().name().into()))?;
450        structural_node(seq, false)?;
451        let mut values = Vec::new();
452        for li in seq.children().filter(Node::is_element) {
453            if !li.has_tag_name((RDF, "li")) {
454                return Err(Error::Structure(n.tag_name().name().into()));
455            }
456            values.push(plain_text(li)?);
457        }
458        Ok(values)
459    } else {
460        Ok(alloc::vec![plain_text(n)?])
461    }
462}
463
464fn structural_node(n: Node<'_, '_>, allow_resource: bool) -> Result<(), Error> {
465    if n.attributes().any(|a| {
466        !(allow_resource
467            && a.namespace() == Some(RDF)
468            && a.name() == "parseType"
469            && a.value() == "Resource")
470    }) || n
471        .children()
472        .any(|c| c.is_text() && c.text().is_some_and(|s| !s.trim().is_empty()))
473    {
474        return Err(Error::Structure(n.tag_name().name().into()));
475    }
476    Ok(())
477}
478
479fn empty_subject(n: Node<'_, '_>) -> Result<(), Error> {
480    if n.attributes()
481        .any(|a| a.namespace() == Some(RDF) && (a.name() != "about" || !a.value().is_empty()))
482    {
483        return Err(Error::Structure("RDF subject/qualifier".into()));
484    }
485    Ok(())
486}
487
488#[cfg(test)]
489mod nesting_tests {
490    extern crate std;
491    use super::*;
492
493    /// Parse `xml` on a thread with a small stack; `None` if it overflowed.
494    fn parse_on_small_stack(xml: String) -> Result<(), Error> {
495        std::thread::Builder::new()
496            .stack_size(256 * 1024)
497            .spawn(move || Packet::parse(&xml).map(|_| ()))
498            .unwrap()
499            .join()
500            .expect("parse overflowed a 256 KiB stack")
501    }
502
503    #[test]
504    fn deep_nesting_is_rejected_without_exhausting_the_stack() {
505        // roxmltree 0.21 overflowed a 2 MiB stack at 8,848 levels, closed or
506        // not: a crafted 1 MB XMP packet aborted the process.
507        for depth in [100usize, 8_848, 200_000] {
508            let open = "<a>".repeat(depth);
509            let closed = format!("<x>{open}{}</x>", "</a>".repeat(depth));
510            assert_eq!(parse_on_small_stack(closed), Err(Error::Limit), "{depth}");
511            assert_eq!(
512                parse_on_small_stack(format!("<x>{open}")),
513                Err(Error::Limit),
514                "{depth}"
515            );
516        }
517    }
518
519    #[test]
520    fn the_scan_counts_elements_not_markup_that_looks_like_them() {
521        // Self-closing tags, end tags, comments, CDATA, processing
522        // instructions and `>` or `/>` inside attribute values do not nest.
523        let inner = r#"<b c='x > y' d="a/>b"/><!-- <c><c> --><![CDATA[<d><d>]]><?pi <e>?>"#;
524        let mut xml = String::from("<a>");
525        for _ in 0..62 {
526            xml.push_str("<n>");
527        }
528        xml.push_str(inner);
529        for _ in 0..62 {
530            xml.push_str("</n>");
531        }
532        xml.push_str("</a>");
533        assert!(!nests_deeper_than(&xml, MAX_NESTING));
534        assert!(nests_deeper_than(&xml, 62));
535        assert!(Packet::parse(&xml).is_ok());
536    }
537}