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}