Skip to main content

pptx_parse/
xml.rs

1use std::collections::BTreeMap;
2
3use quick_xml::Reader;
4use quick_xml::events::{BytesStart, Event};
5
6use crate::PptxError;
7
8pub(crate) const DRAWINGML_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
9pub(crate) const PRESENTATIONML_NS: &str =
10    "http://schemas.openxmlformats.org/presentationml/2006/main";
11
12#[derive(Clone, Debug)]
13pub struct ParseLimits {
14    pub max_xml_bytes: usize,
15    pub max_xml_events: usize,
16    pub max_xml_text_bytes: usize,
17    pub max_xml_depth: usize,
18    pub max_attributes_per_element: usize,
19    pub max_attribute_bytes: usize,
20    pub max_relationships: usize,
21    pub max_shapes: usize,
22    pub max_paragraphs: usize,
23    pub max_runs: usize,
24    pub max_comments: usize,
25}
26
27impl Default for ParseLimits {
28    fn default() -> Self {
29        Self {
30            max_xml_bytes: 128 * 1024 * 1024,
31            max_xml_events: 4_000_000,
32            max_xml_text_bytes: 128 * 1024 * 1024,
33            max_xml_depth: 256,
34            max_attributes_per_element: 1_024,
35            max_attribute_bytes: 4 * 1024 * 1024,
36            max_relationships: 250_000,
37            max_shapes: 100_000,
38            max_paragraphs: 500_000,
39            max_runs: 2_000_000,
40            max_comments: 100_000,
41        }
42    }
43}
44
45#[derive(Debug)]
46pub(crate) struct ParseBudget<'a> {
47    limits: &'a ParseLimits,
48    xml_bytes: usize,
49    xml_events: usize,
50    xml_text_bytes: usize,
51    relationships: usize,
52    shapes: usize,
53    paragraphs: usize,
54    runs: usize,
55    comments: usize,
56}
57
58impl<'a> ParseBudget<'a> {
59    pub fn new(limits: &'a ParseLimits) -> Self {
60        Self {
61            limits,
62            xml_bytes: 0,
63            xml_events: 0,
64            xml_text_bytes: 0,
65            relationships: 0,
66            shapes: 0,
67            paragraphs: 0,
68            runs: 0,
69            comments: 0,
70        }
71    }
72
73    /// XML events this budget has been charged, including the one that broke it.
74    pub fn xml_events_spent(&self) -> usize {
75        self.xml_events
76    }
77
78    pub fn charge_relationship(&mut self, part: &str) -> Result<(), PptxError> {
79        charge(
80            &mut self.relationships,
81            1,
82            self.limits.max_relationships,
83            "relationships",
84            part,
85        )
86    }
87
88    pub fn charge_shape(&mut self, part: &str) -> Result<(), PptxError> {
89        charge(&mut self.shapes, 1, self.limits.max_shapes, "shapes", part)
90    }
91
92    pub fn charge_paragraph(&mut self, part: &str) -> Result<(), PptxError> {
93        charge(
94            &mut self.paragraphs,
95            1,
96            self.limits.max_paragraphs,
97            "paragraphs",
98            part,
99        )
100    }
101
102    pub fn charge_run(&mut self, part: &str) -> Result<(), PptxError> {
103        charge(&mut self.runs, 1, self.limits.max_runs, "runs", part)
104    }
105
106    pub fn charge_comment(&mut self, part: &str) -> Result<(), PptxError> {
107        charge(
108            &mut self.comments,
109            1,
110            self.limits.max_comments,
111            "comments",
112            part,
113        )
114    }
115}
116
117fn charge(
118    used: &mut usize,
119    amount: usize,
120    maximum: usize,
121    kind: &'static str,
122    part: &str,
123) -> Result<(), PptxError> {
124    *used = used
125        .checked_add(amount)
126        .ok_or_else(|| PptxError::ResourceLimit {
127            part: part.to_owned(),
128            kind,
129        })?;
130    if *used > maximum {
131        return Err(PptxError::ResourceLimit {
132            part: part.to_owned(),
133            kind,
134        });
135    }
136    Ok(())
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub(crate) struct XmlElement {
141    pub name: String,
142    pub attributes: BTreeMap<String, String>,
143    pub children: Vec<XmlNode>,
144}
145
146impl XmlElement {
147    pub fn local_name(&self) -> &str {
148        local_name(&self.name)
149    }
150
151    pub fn attribute(&self, name: &str) -> Option<&str> {
152        self.attributes.get(name).map(String::as_str)
153    }
154
155    pub fn attribute_local(&self, name: &str) -> Option<&str> {
156        self.attributes
157            .iter()
158            .find(|(key, _)| local_name(key) == name)
159            .map(|(_, value)| value.as_str())
160    }
161
162    pub fn child(&self, name: &str) -> Option<&XmlElement> {
163        self.child_elements()
164            .find(|child| child.local_name() == name)
165    }
166
167    pub fn children_named<'a>(
168        &'a self,
169        name: &'a str,
170    ) -> impl Iterator<Item = &'a XmlElement> + 'a {
171        self.child_elements()
172            .filter(move |child| child.local_name() == name)
173    }
174
175    pub fn child_elements(&self) -> impl Iterator<Item = &XmlElement> {
176        self.children.iter().filter_map(|child| match child {
177            XmlNode::Element(element) => Some(element),
178            XmlNode::Text(_) => None,
179        })
180    }
181
182    pub fn descendants_named<'a>(&'a self, name: &'a str) -> Vec<&'a XmlElement> {
183        let mut output = Vec::new();
184        collect_descendants(self, name, &mut output);
185        output
186    }
187
188    pub fn text_content(&self) -> String {
189        let mut output = String::new();
190        append_text(self, &mut output);
191        output
192    }
193
194    pub fn new(name: impl Into<String>) -> Self {
195        Self {
196            name: name.into(),
197            attributes: BTreeMap::new(),
198            children: Vec::new(),
199        }
200    }
201
202    pub fn with_attribute(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
203        self.attributes.insert(name.into(), value.into());
204        self
205    }
206
207    pub fn with_child(mut self, child: XmlElement) -> Self {
208        self.children.push(XmlNode::Element(child));
209        self
210    }
211
212    pub fn with_text(mut self, text: impl Into<String>) -> Self {
213        self.children.push(XmlNode::Text(text.into()));
214        self
215    }
216
217    pub fn set_attribute(&mut self, name: impl Into<String>, value: impl Into<String>) {
218        self.attributes.insert(name.into(), value.into());
219    }
220
221    pub fn child_mut(&mut self, name: &str) -> Option<&mut XmlElement> {
222        self.children.iter_mut().find_map(|child| match child {
223            XmlNode::Element(element) if element.local_name() == name => Some(element),
224            _ => None,
225        })
226    }
227}
228
229#[derive(Clone, Debug, PartialEq, Eq)]
230pub(crate) enum XmlNode {
231    Element(XmlElement),
232    Text(String),
233}
234
235fn collect_descendants<'a>(element: &'a XmlElement, name: &str, output: &mut Vec<&'a XmlElement>) {
236    for child in element.child_elements() {
237        if child.local_name() == name {
238            output.push(child);
239        }
240        collect_descendants(child, name, output);
241    }
242}
243
244fn append_text(element: &XmlElement, output: &mut String) {
245    for child in &element.children {
246        match child {
247            XmlNode::Element(element) => append_text(element, output),
248            XmlNode::Text(text) => output.push_str(text),
249        }
250    }
251}
252
253/// The `mc:AlternateContent` branch this crate reads: the first `mc:Choice`
254/// whose `Requires` namespaces are all supported, else the `mc:Fallback`.
255pub(crate) fn alternate_content_branch(element: &XmlElement) -> Option<&XmlElement> {
256    match &element.children[alternate_content_branch_index(element)?] {
257        XmlNode::Element(branch) => Some(branch),
258        XmlNode::Text(_) => None,
259    }
260}
261
262/// Index into `children` of the branch [`alternate_content_branch`] reads.
263pub(crate) fn alternate_content_branch_index(element: &XmlElement) -> Option<usize> {
264    let mut fallback = None;
265    for (index, child) in element.children.iter().enumerate() {
266        let XmlNode::Element(branch) = child else {
267            continue;
268        };
269        match branch.local_name() {
270            "Choice" if choice_is_supported(element, branch) => return Some(index),
271            "Fallback" if fallback.is_none() => fallback = Some(index),
272            _ => {}
273        }
274    }
275    fallback
276}
277
278fn choice_is_supported(alternate: &XmlElement, choice: &XmlElement) -> bool {
279    let Some(requires) = choice.attribute("Requires") else {
280        return false;
281    };
282    let mut prefixes = requires.split_whitespace().peekable();
283    prefixes.peek().is_some()
284        && prefixes.all(|prefix| {
285            let declaration = format!("xmlns:{prefix}");
286            choice
287                .attribute(&declaration)
288                .or_else(|| alternate.attribute(&declaration))
289                .is_some_and(|uri| [DRAWINGML_NS, PRESENTATIONML_NS].contains(&uri))
290        })
291}
292
293pub(crate) fn local_name(name: &str) -> &str {
294    name.rsplit_once(':').map_or(name, |(_, local)| local)
295}
296
297pub(crate) fn serialize_xml(root: &XmlElement) -> Vec<u8> {
298    let mut output =
299        String::from("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
300    write_element(root, &mut output);
301    output.into_bytes()
302}
303
304pub(crate) fn serialize_xml_fragment(root: &XmlElement) -> Vec<u8> {
305    let mut output = String::new();
306    write_element(root, &mut output);
307    output.into_bytes()
308}
309
310fn write_element(element: &XmlElement, output: &mut String) {
311    output.push('<');
312    output.push_str(&element.name);
313    for (key, value) in &element.attributes {
314        output.push(' ');
315        output.push_str(key);
316        output.push_str("=\"");
317        escape_into(value, true, output);
318        output.push('"');
319    }
320    if element.children.is_empty() {
321        output.push_str("/>");
322        return;
323    }
324    output.push('>');
325    for child in &element.children {
326        match child {
327            XmlNode::Element(child) => write_element(child, output),
328            XmlNode::Text(text) => escape_into(text, false, output),
329        }
330    }
331    output.push_str("</");
332    output.push_str(&element.name);
333    output.push('>');
334}
335
336fn escape_into(value: &str, attribute: bool, output: &mut String) {
337    for character in value.chars() {
338        match character {
339            '&' => output.push_str("&amp;"),
340            '<' => output.push_str("&lt;"),
341            '>' => output.push_str("&gt;"),
342            '"' if attribute => output.push_str("&quot;"),
343            // A literal CR would be normalized away on reparse.
344            '\r' => output.push_str("&#xD;"),
345            '\n' | '\t' if attribute => {
346                output.push_str(&format!("&#x{:X};", character as u32));
347            }
348            _ => output.push(character),
349        }
350    }
351}
352
353pub(crate) fn parse_xml(
354    xml: &[u8],
355    part: &str,
356    budget: &mut ParseBudget<'_>,
357) -> Result<XmlElement, PptxError> {
358    charge(
359        &mut budget.xml_bytes,
360        xml.len(),
361        budget.limits.max_xml_bytes,
362        "xmlBytes",
363        part,
364    )?;
365    let mut reader = Reader::from_reader(xml);
366    reader.config_mut().trim_text(false);
367    reader.config_mut().check_end_names = true;
368    let mut roots = Vec::new();
369    let mut stack = Vec::new();
370
371    loop {
372        let event = reader
373            .read_event()
374            .map_err(|error| malformed(&reader, part, error.to_string()))?;
375        charge(
376            &mut budget.xml_events,
377            1,
378            budget.limits.max_xml_events,
379            "xmlEvents",
380            part,
381        )?;
382        match event {
383            Event::Start(start) => {
384                check_depth(stack.len() + 1, budget, part)?;
385                stack.push(decode_element(&reader, start, part, budget)?);
386            }
387            Event::Empty(start) => {
388                check_depth(stack.len() + 1, budget, part)?;
389                let element = decode_element(&reader, start, part, budget)?;
390                append_element(element, &mut stack, &mut roots, part)?;
391            }
392            Event::End(_) => {
393                let element = stack
394                    .pop()
395                    .ok_or_else(|| malformed(&reader, part, "unexpected closing element"))?;
396                append_element(element, &mut stack, &mut roots, part)?;
397            }
398            Event::Text(text) => {
399                let decoded = text
400                    .decode()
401                    .map_err(|error| malformed(&reader, part, error.to_string()))?;
402                let unescaped = quick_xml::escape::unescape(&decoded)
403                    .map_err(|error| malformed(&reader, part, error.to_string()))?;
404                charge(
405                    &mut budget.xml_text_bytes,
406                    unescaped.len(),
407                    budget.limits.max_xml_text_bytes,
408                    "xmlTextBytes",
409                    part,
410                )?;
411                append_text_node(unescaped.into_owned(), &mut stack, &reader, part)?;
412            }
413            Event::CData(text) => {
414                let decoded = text
415                    .decode()
416                    .map_err(|error| malformed(&reader, part, error.to_string()))?
417                    .into_owned();
418                charge(
419                    &mut budget.xml_text_bytes,
420                    decoded.len(),
421                    budget.limits.max_xml_text_bytes,
422                    "xmlTextBytes",
423                    part,
424                )?;
425                append_text_node(decoded, &mut stack, &reader, part)?;
426            }
427            Event::DocType(_) => {
428                return Err(PptxError::UnsafeXml {
429                    part: part.to_owned(),
430                    kind: "DTD/entity declarations are forbidden",
431                });
432            }
433            Event::GeneralRef(reference) => {
434                let decoded = reference
435                    .decode()
436                    .map_err(|error| malformed(&reader, part, error.to_string()))?;
437                let resolved = if reference.is_char_ref() {
438                    reference
439                        .resolve_char_ref()
440                        .map_err(|error| malformed(&reader, part, error.to_string()))?
441                        .filter(|character| is_legal_xml_character(*character))
442                        .map(|character| character.to_string())
443                } else {
444                    quick_xml::escape::resolve_predefined_entity(&decoded).map(str::to_owned)
445                };
446                let Some(resolved) = resolved else {
447                    return Err(PptxError::UnsafeXml {
448                        part: part.to_owned(),
449                        kind: "non-predefined or illegal entity reference",
450                    });
451                };
452                charge(
453                    &mut budget.xml_text_bytes,
454                    resolved.len(),
455                    budget.limits.max_xml_text_bytes,
456                    "xmlTextBytes",
457                    part,
458                )?;
459                append_text_node(resolved, &mut stack, &reader, part)?;
460            }
461            Event::Decl(_) | Event::PI(_) | Event::Comment(_) => {}
462            Event::Eof => break,
463        }
464    }
465
466    if !stack.is_empty() {
467        return Err(malformed(&reader, part, "unclosed element"));
468    }
469    if roots.len() != 1 {
470        return Err(malformed(
471            &reader,
472            part,
473            "XML part must have exactly one root element",
474        ));
475    }
476    Ok(roots.pop().expect("root count checked"))
477}
478
479fn check_depth(depth: usize, budget: &ParseBudget<'_>, part: &str) -> Result<(), PptxError> {
480    if depth > budget.limits.max_xml_depth {
481        return Err(PptxError::ResourceLimit {
482            part: part.to_owned(),
483            kind: "xmlDepth",
484        });
485    }
486    Ok(())
487}
488
489fn decode_element(
490    reader: &Reader<&[u8]>,
491    start: BytesStart<'_>,
492    part: &str,
493    budget: &mut ParseBudget<'_>,
494) -> Result<XmlElement, PptxError> {
495    let name = reader
496        .decoder()
497        .decode(start.name().as_ref())
498        .map_err(|error| malformed(reader, part, error.to_string()))?
499        .into_owned();
500    let mut attributes = BTreeMap::new();
501    let mut attribute_bytes = 0usize;
502    for (index, attribute) in start.attributes().enumerate() {
503        if index >= budget.limits.max_attributes_per_element {
504            return Err(PptxError::ResourceLimit {
505                part: part.to_owned(),
506                kind: "attributesPerElement",
507            });
508        }
509        let attribute = attribute.map_err(|error| malformed(reader, part, error.to_string()))?;
510        let key = reader
511            .decoder()
512            .decode(attribute.key.as_ref())
513            .map_err(|error| malformed(reader, part, error.to_string()))?
514            .into_owned();
515        #[allow(deprecated)]
516        let value = attribute
517            .decode_and_unescape_value(reader.decoder())
518            .map_err(|error| malformed(reader, part, error.to_string()))?
519            .into_owned();
520        attribute_bytes = attribute_bytes
521            .checked_add(key.len() + value.len())
522            .ok_or_else(|| PptxError::ResourceLimit {
523                part: part.to_owned(),
524                kind: "attributeBytes",
525            })?;
526        if attribute_bytes > budget.limits.max_attribute_bytes {
527            return Err(PptxError::ResourceLimit {
528                part: part.to_owned(),
529                kind: "attributeBytes",
530            });
531        }
532        charge(
533            &mut budget.xml_text_bytes,
534            key.len() + value.len(),
535            budget.limits.max_xml_text_bytes,
536            "xmlTextBytes",
537            part,
538        )?;
539        if attributes.insert(key.clone(), value).is_some() {
540            return Err(malformed(
541                reader,
542                part,
543                format!("duplicate attribute {key}"),
544            ));
545        }
546    }
547    Ok(XmlElement {
548        name,
549        attributes,
550        children: Vec::new(),
551    })
552}
553
554fn append_element(
555    element: XmlElement,
556    stack: &mut [XmlElement],
557    roots: &mut Vec<XmlElement>,
558    part: &str,
559) -> Result<(), PptxError> {
560    if let Some(parent) = stack.last_mut() {
561        parent.children.push(XmlNode::Element(element));
562    } else if roots.is_empty() {
563        roots.push(element);
564    } else {
565        return Err(PptxError::MalformedXml {
566            part: part.to_owned(),
567            offset: 0,
568            message: "multiple root elements".to_owned(),
569        });
570    }
571    Ok(())
572}
573
574fn append_text_node(
575    text: String,
576    stack: &mut [XmlElement],
577    reader: &Reader<&[u8]>,
578    part: &str,
579) -> Result<(), PptxError> {
580    if let Some(parent) = stack.last_mut() {
581        parent.children.push(XmlNode::Text(text));
582    } else if !text.trim().is_empty() {
583        return Err(malformed(reader, part, "text outside root element"));
584    }
585    Ok(())
586}
587
588fn malformed(reader: &Reader<&[u8]>, part: &str, message: impl Into<String>) -> PptxError {
589    PptxError::MalformedXml {
590        part: part.to_owned(),
591        offset: reader.buffer_position(),
592        message: message.into(),
593    }
594}
595
596fn is_legal_xml_character(character: char) -> bool {
597    matches!(character, '\u{9}' | '\u{a}' | '\u{d}')
598        || ('\u{20}'..='\u{d7ff}').contains(&character)
599        || ('\u{e000}'..='\u{fffd}').contains(&character)
600        || ('\u{10000}'..='\u{10ffff}').contains(&character)
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn rejects_dtd_unknown_entities_and_depth_overflow() {
609        let limits = ParseLimits::default();
610        let mut budget = ParseBudget::new(&limits);
611        assert!(matches!(
612            parse_xml(b"<!DOCTYPE x><x/>", "x.xml", &mut budget),
613            Err(PptxError::UnsafeXml { .. })
614        ));
615
616        let mut budget = ParseBudget::new(&limits);
617        assert!(matches!(
618            parse_xml(b"<x>&file;</x>", "x.xml", &mut budget),
619            Err(PptxError::UnsafeXml { .. })
620        ));
621
622        let shallow = ParseLimits {
623            max_xml_depth: 1,
624            ..ParseLimits::default()
625        };
626        let mut budget = ParseBudget::new(&shallow);
627        assert!(matches!(
628            parse_xml(b"<x><y/></x>", "x.xml", &mut budget),
629            Err(PptxError::ResourceLimit {
630                kind: "xmlDepth",
631                ..
632            })
633        ));
634    }
635}