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