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    pub fn charge_relationship(&mut self, part: &str) -> Result<(), PptxError> {
66        charge(
67            &mut self.relationships,
68            1,
69            self.limits.max_relationships,
70            "relationships",
71            part,
72        )
73    }
74
75    pub fn charge_shape(&mut self, part: &str) -> Result<(), PptxError> {
76        charge(&mut self.shapes, 1, self.limits.max_shapes, "shapes", part)
77    }
78
79    pub fn charge_paragraph(&mut self, part: &str) -> Result<(), PptxError> {
80        charge(
81            &mut self.paragraphs,
82            1,
83            self.limits.max_paragraphs,
84            "paragraphs",
85            part,
86        )
87    }
88
89    pub fn charge_run(&mut self, part: &str) -> Result<(), PptxError> {
90        charge(&mut self.runs, 1, self.limits.max_runs, "runs", part)
91    }
92}
93
94fn charge(
95    used: &mut usize,
96    amount: usize,
97    maximum: usize,
98    kind: &'static str,
99    part: &str,
100) -> Result<(), PptxError> {
101    *used = used
102        .checked_add(amount)
103        .ok_or_else(|| PptxError::ResourceLimit {
104            part: part.to_owned(),
105            kind,
106        })?;
107    if *used > maximum {
108        return Err(PptxError::ResourceLimit {
109            part: part.to_owned(),
110            kind,
111        });
112    }
113    Ok(())
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub(crate) struct XmlElement {
118    pub name: String,
119    pub attributes: BTreeMap<String, String>,
120    pub children: Vec<XmlNode>,
121}
122
123impl XmlElement {
124    pub fn local_name(&self) -> &str {
125        local_name(&self.name)
126    }
127
128    pub fn attribute(&self, name: &str) -> Option<&str> {
129        self.attributes.get(name).map(String::as_str)
130    }
131
132    pub fn attribute_local(&self, name: &str) -> Option<&str> {
133        self.attributes
134            .iter()
135            .find(|(key, _)| local_name(key) == name)
136            .map(|(_, value)| value.as_str())
137    }
138
139    pub fn child(&self, name: &str) -> Option<&XmlElement> {
140        self.child_elements()
141            .find(|child| child.local_name() == name)
142    }
143
144    pub fn children_named<'a>(
145        &'a self,
146        name: &'a str,
147    ) -> impl Iterator<Item = &'a XmlElement> + 'a {
148        self.child_elements()
149            .filter(move |child| child.local_name() == name)
150    }
151
152    pub fn child_elements(&self) -> impl Iterator<Item = &XmlElement> {
153        self.children.iter().filter_map(|child| match child {
154            XmlNode::Element(element) => Some(element),
155            XmlNode::Text(_) => None,
156        })
157    }
158
159    pub fn descendants_named<'a>(&'a self, name: &'a str) -> Vec<&'a XmlElement> {
160        let mut output = Vec::new();
161        collect_descendants(self, name, &mut output);
162        output
163    }
164
165    pub fn text_content(&self) -> String {
166        let mut output = String::new();
167        append_text(self, &mut output);
168        output
169    }
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
173pub(crate) enum XmlNode {
174    Element(XmlElement),
175    Text(String),
176}
177
178fn collect_descendants<'a>(element: &'a XmlElement, name: &str, output: &mut Vec<&'a XmlElement>) {
179    for child in element.child_elements() {
180        if child.local_name() == name {
181            output.push(child);
182        }
183        collect_descendants(child, name, output);
184    }
185}
186
187fn append_text(element: &XmlElement, output: &mut String) {
188    for child in &element.children {
189        match child {
190            XmlNode::Element(element) => append_text(element, output),
191            XmlNode::Text(text) => output.push_str(text),
192        }
193    }
194}
195
196pub(crate) fn local_name(name: &str) -> &str {
197    name.rsplit_once(':').map_or(name, |(_, local)| local)
198}
199
200pub(crate) fn parse_xml(
201    xml: &[u8],
202    part: &str,
203    budget: &mut ParseBudget<'_>,
204) -> Result<XmlElement, PptxError> {
205    charge(
206        &mut budget.xml_bytes,
207        xml.len(),
208        budget.limits.max_xml_bytes,
209        "xmlBytes",
210        part,
211    )?;
212    let mut reader = Reader::from_reader(xml);
213    reader.config_mut().trim_text(false);
214    reader.config_mut().check_end_names = true;
215    let mut roots = Vec::new();
216    let mut stack = Vec::new();
217
218    loop {
219        let event = reader
220            .read_event()
221            .map_err(|error| malformed(&reader, part, error.to_string()))?;
222        charge(
223            &mut budget.xml_events,
224            1,
225            budget.limits.max_xml_events,
226            "xmlEvents",
227            part,
228        )?;
229        match event {
230            Event::Start(start) => {
231                check_depth(stack.len() + 1, budget, part)?;
232                stack.push(decode_element(&reader, start, part, budget)?);
233            }
234            Event::Empty(start) => {
235                check_depth(stack.len() + 1, budget, part)?;
236                let element = decode_element(&reader, start, part, budget)?;
237                append_element(element, &mut stack, &mut roots, part)?;
238            }
239            Event::End(_) => {
240                let element = stack
241                    .pop()
242                    .ok_or_else(|| malformed(&reader, part, "unexpected closing element"))?;
243                append_element(element, &mut stack, &mut roots, part)?;
244            }
245            Event::Text(text) => {
246                let decoded = text
247                    .decode()
248                    .map_err(|error| malformed(&reader, part, error.to_string()))?;
249                let unescaped = quick_xml::escape::unescape(&decoded)
250                    .map_err(|error| malformed(&reader, part, error.to_string()))?;
251                charge(
252                    &mut budget.xml_text_bytes,
253                    unescaped.len(),
254                    budget.limits.max_xml_text_bytes,
255                    "xmlTextBytes",
256                    part,
257                )?;
258                append_text_node(unescaped.into_owned(), &mut stack, &reader, part)?;
259            }
260            Event::CData(text) => {
261                let decoded = text
262                    .decode()
263                    .map_err(|error| malformed(&reader, part, error.to_string()))?
264                    .into_owned();
265                charge(
266                    &mut budget.xml_text_bytes,
267                    decoded.len(),
268                    budget.limits.max_xml_text_bytes,
269                    "xmlTextBytes",
270                    part,
271                )?;
272                append_text_node(decoded, &mut stack, &reader, part)?;
273            }
274            Event::DocType(_) => {
275                return Err(PptxError::UnsafeXml {
276                    part: part.to_owned(),
277                    kind: "DTD/entity declarations are forbidden",
278                });
279            }
280            Event::GeneralRef(reference) => {
281                let decoded = reference
282                    .decode()
283                    .map_err(|error| malformed(&reader, part, error.to_string()))?;
284                let resolved = if reference.is_char_ref() {
285                    reference
286                        .resolve_char_ref()
287                        .map_err(|error| malformed(&reader, part, error.to_string()))?
288                        .filter(|character| is_legal_xml_character(*character))
289                        .map(|character| character.to_string())
290                } else {
291                    quick_xml::escape::resolve_predefined_entity(&decoded).map(str::to_owned)
292                };
293                let Some(resolved) = resolved else {
294                    return Err(PptxError::UnsafeXml {
295                        part: part.to_owned(),
296                        kind: "non-predefined or illegal entity reference",
297                    });
298                };
299                charge(
300                    &mut budget.xml_text_bytes,
301                    resolved.len(),
302                    budget.limits.max_xml_text_bytes,
303                    "xmlTextBytes",
304                    part,
305                )?;
306                append_text_node(resolved, &mut stack, &reader, part)?;
307            }
308            Event::Decl(_) | Event::PI(_) | Event::Comment(_) => {}
309            Event::Eof => break,
310        }
311    }
312
313    if !stack.is_empty() {
314        return Err(malformed(&reader, part, "unclosed element"));
315    }
316    if roots.len() != 1 {
317        return Err(malformed(
318            &reader,
319            part,
320            "XML part must have exactly one root element",
321        ));
322    }
323    Ok(roots.pop().expect("root count checked"))
324}
325
326fn check_depth(depth: usize, budget: &ParseBudget<'_>, part: &str) -> Result<(), PptxError> {
327    if depth > budget.limits.max_xml_depth {
328        return Err(PptxError::ResourceLimit {
329            part: part.to_owned(),
330            kind: "xmlDepth",
331        });
332    }
333    Ok(())
334}
335
336fn decode_element(
337    reader: &Reader<&[u8]>,
338    start: BytesStart<'_>,
339    part: &str,
340    budget: &mut ParseBudget<'_>,
341) -> Result<XmlElement, PptxError> {
342    let name = reader
343        .decoder()
344        .decode(start.name().as_ref())
345        .map_err(|error| malformed(reader, part, error.to_string()))?
346        .into_owned();
347    let mut attributes = BTreeMap::new();
348    let mut attribute_bytes = 0usize;
349    for (index, attribute) in start.attributes().enumerate() {
350        if index >= budget.limits.max_attributes_per_element {
351            return Err(PptxError::ResourceLimit {
352                part: part.to_owned(),
353                kind: "attributesPerElement",
354            });
355        }
356        let attribute = attribute.map_err(|error| malformed(reader, part, error.to_string()))?;
357        let key = reader
358            .decoder()
359            .decode(attribute.key.as_ref())
360            .map_err(|error| malformed(reader, part, error.to_string()))?
361            .into_owned();
362        #[allow(deprecated)]
363        let value = attribute
364            .decode_and_unescape_value(reader.decoder())
365            .map_err(|error| malformed(reader, part, error.to_string()))?
366            .into_owned();
367        attribute_bytes = attribute_bytes
368            .checked_add(key.len() + value.len())
369            .ok_or_else(|| PptxError::ResourceLimit {
370                part: part.to_owned(),
371                kind: "attributeBytes",
372            })?;
373        if attribute_bytes > budget.limits.max_attribute_bytes {
374            return Err(PptxError::ResourceLimit {
375                part: part.to_owned(),
376                kind: "attributeBytes",
377            });
378        }
379        charge(
380            &mut budget.xml_text_bytes,
381            key.len() + value.len(),
382            budget.limits.max_xml_text_bytes,
383            "xmlTextBytes",
384            part,
385        )?;
386        if attributes.insert(key.clone(), value).is_some() {
387            return Err(malformed(
388                reader,
389                part,
390                format!("duplicate attribute {key}"),
391            ));
392        }
393    }
394    Ok(XmlElement {
395        name,
396        attributes,
397        children: Vec::new(),
398    })
399}
400
401fn append_element(
402    element: XmlElement,
403    stack: &mut [XmlElement],
404    roots: &mut Vec<XmlElement>,
405    part: &str,
406) -> Result<(), PptxError> {
407    if let Some(parent) = stack.last_mut() {
408        parent.children.push(XmlNode::Element(element));
409    } else if roots.is_empty() {
410        roots.push(element);
411    } else {
412        return Err(PptxError::MalformedXml {
413            part: part.to_owned(),
414            offset: 0,
415            message: "multiple root elements".to_owned(),
416        });
417    }
418    Ok(())
419}
420
421fn append_text_node(
422    text: String,
423    stack: &mut [XmlElement],
424    reader: &Reader<&[u8]>,
425    part: &str,
426) -> Result<(), PptxError> {
427    if let Some(parent) = stack.last_mut() {
428        parent.children.push(XmlNode::Text(text));
429    } else if !text.trim().is_empty() {
430        return Err(malformed(reader, part, "text outside root element"));
431    }
432    Ok(())
433}
434
435fn malformed(reader: &Reader<&[u8]>, part: &str, message: impl Into<String>) -> PptxError {
436    PptxError::MalformedXml {
437        part: part.to_owned(),
438        offset: reader.buffer_position(),
439        message: message.into(),
440    }
441}
442
443fn is_legal_xml_character(character: char) -> bool {
444    matches!(character, '\u{9}' | '\u{a}' | '\u{d}')
445        || ('\u{20}'..='\u{d7ff}').contains(&character)
446        || ('\u{e000}'..='\u{fffd}').contains(&character)
447        || ('\u{10000}'..='\u{10ffff}').contains(&character)
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn rejects_dtd_unknown_entities_and_depth_overflow() {
456        let limits = ParseLimits::default();
457        let mut budget = ParseBudget::new(&limits);
458        assert!(matches!(
459            parse_xml(b"<!DOCTYPE x><x/>", "x.xml", &mut budget),
460            Err(PptxError::UnsafeXml { .. })
461        ));
462
463        let mut budget = ParseBudget::new(&limits);
464        assert!(matches!(
465            parse_xml(b"<x>&file;</x>", "x.xml", &mut budget),
466            Err(PptxError::UnsafeXml { .. })
467        ));
468
469        let shallow = ParseLimits {
470            max_xml_depth: 1,
471            ..ParseLimits::default()
472        };
473        let mut budget = ParseBudget::new(&shallow);
474        assert!(matches!(
475            parse_xml(b"<x><y/></x>", "x.xml", &mut budget),
476            Err(PptxError::ResourceLimit {
477                kind: "xmlDepth",
478                ..
479            })
480        ));
481    }
482}