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