Skip to main content

cadmpeg_core/decode/
tree.rs

1// SPDX-License-Identifier: Apache-2.0
2//! XML and JSON parsing with nesting depth charged to the decode session.
3//!
4//! Each parser charges the document's nesting depth through
5//! [`DecodeContext::enter_nested`] before it parses, so a deep document
6//! refuses with the session's recursion-depth dimension before the parser or
7//! other code walks it recursively. A scan of the text gives the depth and
8//! bounds the tree's storage. A tree that could need more than the
9//! per-expansion allowance is [`CodecError::NotImplemented`] and leaves the
10//! session open.
11
12use std::collections::HashSet;
13
14use super::{u64_from_index, DecodeContext, DepthGuard};
15use crate::CodecError;
16
17/// Attributes in one XML tag. roxmltree compares each attribute with the
18/// earlier ones in its tag.
19const MAX_XML_TAG_ATTRIBUTES: u64 = 256;
20/// Distinct namespace prefixes declared in one XML text. roxmltree searches
21/// the namespaces in scope for each element and prefixed attribute, and
22/// copies them into each element that declares a namespace.
23const MAX_XML_NAMESPACE_PREFIXES: u64 = 256;
24/// One roxmltree 0.21.1 `NodeData` with the `positions` feature: four links,
25/// the node kind with an inline expanded name, and the source range.
26const XML_NODE_BYTES: u64 = 72;
27/// One roxmltree 0.21.1 `AttributeData` with the `positions` feature.
28const XML_ATTRIBUTE_BYTES: u64 = 72;
29/// One roxmltree 0.21.1 `TempAttributeData`.
30const XML_TEMP_ATTRIBUTE_BYTES: u64 = 80;
31/// Bounds one roxmltree 0.21.1 `Namespace` with its sorted index.
32const XML_NAMESPACE_BYTES: u64 = 64;
33/// One nesting level: a slot in roxmltree's prefix stack (16 bytes) and in
34/// its list of nodes that wait for a subtree link (4 bytes), and one depth
35/// guard (8 bytes). A growing vector can hold its old and its doubled
36/// storage at once, three times its length.
37const XML_LEVEL_BYTES: u64 = 3 * (16 + 4 + 8);
38/// One text piece that roxmltree keeps to merge with adjacent CDATA, a
39/// `Cow<str>` in a growing vector.
40const XML_TEXT_PIECE_BYTES: u64 = 3 * 24;
41/// The initial text buffer and the padded shared-string header of one
42/// copied text or attribute value.
43const XML_COPY_BYTES: u64 = 32 + 24;
44/// While roxmltree copies one text or value, it can hold the growing
45/// buffer, the merged pieces, their join and two shared strings. None is
46/// longer than the source of that text, and earlier copies keep one shared
47/// string each.
48const XML_COPY_FACTOR: u64 = 5;
49/// Parser state, the scan's prefix set, and the temporary attributes of one
50/// tag in a growing vector.
51const XML_FIXED_BYTES: u64 =
52    1024 + 16 * 1024 + 3 * MAX_XML_TAG_ATTRIBUTES / 2 * XML_TEMP_ATTRIBUTE_BYTES;
53/// One 32-byte `serde_json::Value` in an array that may hold twice its
54/// length.
55const JSON_VALUE_BYTES: u64 = 64;
56/// Bounds one `BTreeMap<String, Value>` node of `serde_json` 1.0.151 without
57/// `preserve_order`: eleven keys and values, twelve edges and parent links.
58const JSON_MAP_NODE_BYTES: u64 = 768;
59/// The allocation of one short JSON string, with allocator rounding.
60const JSON_STRING_BYTES: u64 = 32;
61/// Deserializer state and initial capacities.
62const JSON_FIXED_BYTES: u64 = 1024;
63
64impl DecodeContext<'_> {
65    /// Holds `depth` nesting guards for the duration of `run`.
66    fn at_depth<T>(
67        &self,
68        depth: u64,
69        operation: &'static str,
70        run: impl FnOnce() -> Result<T, CodecError>,
71    ) -> Result<T, CodecError> {
72        let mut guards: Vec<DepthGuard<'_>> = Vec::new();
73        for _ in 0..depth {
74            guards.push(self.enter_nested(operation)?);
75        }
76        let result = run();
77        drop(guards);
78        result
79    }
80
81    fn tree_malformed(error: impl std::fmt::Display) -> CodecError {
82        CodecError::malformed(format_args!("{error}"))
83    }
84
85    /// The session holds the document's depth, so the parser's own
86    /// recursion limit does not apply.
87    fn deserialize_json<T: serde::de::DeserializeOwned>(text: &str) -> Result<T, CodecError> {
88        let mut deserializer = serde_json::Deserializer::from_str(text);
89        deserializer.disable_recursion_limit();
90        T::deserialize(&mut deserializer)
91            .and_then(|value| deserializer.end().map(|()| value))
92            .map_err(Self::tree_malformed)
93    }
94
95    /// Checks that a `serde_json::Value` tree of `text` fits the
96    /// per-expansion allowance and returns the depth the parse can reach.
97    fn admit_json(&self, text: &str, operation: &'static str) -> Result<u64, CodecError> {
98        let shape = json_shape(text);
99        let allowance = self.per_expand_allowance();
100        if json_storage_bytes(u64_from_index(text.len()), &shape)
101            .is_none_or(|bytes| bytes > allowance)
102        {
103            return Err(tree_too_large("JSON", allowance, operation));
104        }
105        Ok(shape.depth)
106    }
107
108    /// Parses XML with DTD processing disabled and charges its node depth.
109    /// The scan bounds the storage that roxmltree reserves and copies, and
110    /// the rest of the per-expansion allowance limits its nodes.
111    pub fn parse_xml<'input>(
112        &self,
113        text: &'input str,
114        operation: &'static str,
115    ) -> Result<roxmltree::Document<'input>, CodecError> {
116        let shape = xml_shape(text);
117        if shape.tag_attributes > MAX_XML_TAG_ATTRIBUTES {
118            return Err(CodecError::NotImplemented(format!(
119                "{operation}: an XML tag has more than {MAX_XML_TAG_ATTRIBUTES} attributes"
120            )));
121        }
122        let prefixes = u64_from_index(shape.prefixes.len());
123        if prefixes > MAX_XML_NAMESPACE_PREFIXES {
124            return Err(CodecError::NotImplemented(format!(
125                "{operation}: the XML text declares more than \
126                 {MAX_XML_NAMESPACE_PREFIXES} namespace prefixes"
127            )));
128        }
129        let allowance = self.per_expand_allowance();
130        let nodes_limit = xml_storage_bytes(&shape, prefixes)
131            .and_then(|bytes| allowance.checked_sub(bytes))
132            .and_then(|rest| xml_nodes_limit(rest, shape.less_than))
133            .ok_or_else(|| tree_too_large("XML", allowance, operation))?;
134        // roxmltree node ids are `u32`; a larger limit leaves the parser's own.
135        let mut options = roxmltree::ParsingOptions::default();
136        if let Ok(limit) = u32::try_from(nodes_limit) {
137            options.nodes_limit = limit;
138        }
139        // roxmltree recurses once per element level.
140        self.at_depth(shape.depth, operation, || {
141            roxmltree::Document::parse_with_options(text, options).map_err(|error| match error {
142                roxmltree::Error::NodesLimitReached => tree_too_large("XML", allowance, operation),
143                error => Self::tree_malformed(error),
144            })
145        })
146    }
147
148    /// Parses a JSON value tree after charging its nesting depth.
149    pub fn parse_json_value(
150        &self,
151        text: &str,
152        operation: &'static str,
153    ) -> Result<serde_json::Value, CodecError> {
154        let depth = self.admit_json(text, operation)?;
155        self.at_depth(depth, operation, || Self::deserialize_json(text))
156    }
157
158    /// Parses a type with a derived `Deserialize` after charging the source's
159    /// nesting depth. Its storage is bounded as a `serde_json::Value` tree of
160    /// the same text.
161    pub fn parse_json<T: serde::de::DeserializeOwned>(
162        &self,
163        text: &str,
164        operation: &'static str,
165    ) -> Result<T, CodecError> {
166        let depth = self.admit_json(text, operation)?;
167        self.at_depth(depth, operation, || Self::deserialize_json(text))
168    }
169}
170
171fn tree_too_large(kind: &str, allowance: u64, operation: &str) -> CodecError {
172    CodecError::NotImplemented(format!(
173        "{operation}: the {kind} tree could need more than the \
174         {allowance}-byte per-expansion allowance"
175    ))
176}
177
178#[derive(Clone, Copy)]
179enum XmlScan {
180    Text,
181    Tag { quote: u8 },
182    Comment,
183    Cdata,
184    Instruction,
185    Declaration,
186}
187
188/// Upper bounds on what roxmltree reserves, copies and keeps per nesting
189/// level while it parses an XML text.
190#[derive(Default)]
191struct XmlShape<'text> {
192    /// `<` anywhere in the text. roxmltree reserves one node for each.
193    less_than: u64,
194    /// `=` anywhere in the text. roxmltree reserves one attribute for each.
195    equals: u64,
196    /// `=` outside quoted values in tags, one per attribute.
197    attributes: u64,
198    /// The most attributes in one tag.
199    tag_attributes: u64,
200    /// `xmlns` outside quoted values in tags, one per namespace declaration.
201    declarations: u64,
202    /// Names that follow `xmlns:` in tags, kept up to one past
203    /// [`MAX_XML_NAMESPACE_PREFIXES`].
204    prefixes: HashSet<&'text [u8]>,
205    /// The deepest node, counted as roxmltree counts ancestors: the root
206    /// element is one, and an element, text, CDATA, comment or instruction
207    /// is one more than the element that contains it.
208    depth: u64,
209    /// CDATA sections. roxmltree merges each with adjacent text.
210    cdata: u64,
211    /// Texts and attribute values that roxmltree copies: a text run with a
212    /// reference, a carriage return or CDATA, and a value with a reference,
213    /// a tab or a line break.
214    copies: u64,
215    /// The source bytes of those texts and values.
216    copied: u64,
217}
218
219impl<'text> XmlShape<'text> {
220    fn copy(&mut self, copied: bool, source: &[u8]) {
221        if copied {
222            self.copies += 1;
223            self.copied += u64_from_index(source.len());
224        }
225    }
226
227    /// Counts a node inside `open` elements.
228    fn contains(&mut self, open: u64) {
229        self.depth = self.depth.max(open + 1);
230    }
231
232    /// Keeps one prefix past the limit, enough for the limit to refuse.
233    fn prefix(&mut self, name: &'text [u8]) {
234        if u64_from_index(self.prefixes.len()) <= MAX_XML_NAMESPACE_PREFIXES {
235            self.prefixes.insert(name);
236        }
237    }
238}
239
240/// Scans `text` once. Comments, CDATA, processing instructions and
241/// declarations end at their first terminator, never after the parser's, so
242/// the counts do not fall below what the parser stores. A text run ends at
243/// markup other than CDATA, where roxmltree stops merging text.
244fn xml_shape(text: &str) -> XmlShape<'_> {
245    let bytes = text.as_bytes();
246    let mut shape = XmlShape::default();
247    let mut state = XmlScan::Text;
248    let mut tag_attributes = 0_u64;
249    let mut prefix_start = None::<usize>;
250    // Open elements; whether the current tag closes one; its last byte
251    // outside whitespace, which is `/` for an empty element.
252    let mut open = 0_u64;
253    let mut closing = false;
254    let mut last = 0_u8;
255    // Start of the current text run or attribute value, and whether
256    // roxmltree copies it.
257    let mut run_start = 0_usize;
258    let mut copied = false;
259    for (index, &byte) in bytes.iter().enumerate() {
260        shape.less_than += u64::from(byte == b'<');
261        shape.equals += u64::from(byte == b'=');
262        let tail = bytes.get(index..).unwrap_or_default();
263        state = match state {
264            XmlScan::Text if byte == b'<' => {
265                if tail.starts_with(b"<![CDATA[") {
266                    shape.cdata += 1;
267                    shape.contains(open);
268                    copied = true;
269                    XmlScan::Cdata
270                } else {
271                    shape.copy(copied, bytes.get(run_start..index).unwrap_or_default());
272                    copied = false;
273                    if tail.starts_with(b"<!--") {
274                        shape.contains(open);
275                        XmlScan::Comment
276                    } else if tail.starts_with(b"<?") {
277                        shape.contains(open);
278                        XmlScan::Instruction
279                    } else if tail.starts_with(b"<!") {
280                        XmlScan::Declaration
281                    } else {
282                        tag_attributes = 0;
283                        closing = tail.get(1) == Some(&b'/');
284                        XmlScan::Tag { quote: 0 }
285                    }
286                }
287            }
288            XmlScan::Text => {
289                copied |= matches!(byte, b'&' | b'\r');
290                // Text outside the root element is not a node.
291                if open != 0 {
292                    shape.contains(open);
293                }
294                XmlScan::Text
295            }
296            XmlScan::Tag { quote: 0 } => {
297                let delimiter =
298                    byte.is_ascii_whitespace() || matches!(byte, b'=' | b'/' | b'>' | b'\'' | b'"');
299                if let Some(start) = prefix_start.filter(|_| delimiter) {
300                    shape.prefix(bytes.get(start..index).unwrap_or_default());
301                    prefix_start = None;
302                }
303                if tail.starts_with(b"xmlns") {
304                    shape.declarations += 1;
305                    if prefix_start.is_none() && tail.get(5) == Some(&b':') {
306                        prefix_start = Some(index + 6);
307                    }
308                }
309                let next = match byte {
310                    b'\'' | b'"' => {
311                        run_start = index + 1;
312                        copied = false;
313                        XmlScan::Tag { quote: byte }
314                    }
315                    b'>' => {
316                        if closing {
317                            if let Some(outer) = open.checked_sub(1) {
318                                open = outer;
319                            }
320                        } else {
321                            shape.contains(open);
322                            if last != b'/' {
323                                open += 1;
324                            }
325                        }
326                        run_start = index + 1;
327                        XmlScan::Text
328                    }
329                    b'=' => {
330                        tag_attributes += 1;
331                        shape.attributes += 1;
332                        shape.tag_attributes = shape.tag_attributes.max(tag_attributes);
333                        XmlScan::Tag { quote: 0 }
334                    }
335                    _ => XmlScan::Tag { quote: 0 },
336                };
337                if !byte.is_ascii_whitespace() {
338                    last = byte;
339                }
340                next
341            }
342            XmlScan::Tag { quote } if byte == quote => {
343                shape.copy(copied, bytes.get(run_start..index).unwrap_or_default());
344                copied = false;
345                last = byte;
346                XmlScan::Tag { quote: 0 }
347            }
348            XmlScan::Tag { quote } => {
349                copied |= matches!(byte, b'&' | b'\t' | b'\n' | b'\r');
350                XmlScan::Tag { quote }
351            }
352            XmlScan::Comment if tail.starts_with(b"-->") => {
353                run_start = index + 3;
354                XmlScan::Text
355            }
356            XmlScan::Cdata if tail.starts_with(b"]]>") => XmlScan::Text,
357            XmlScan::Instruction if tail.starts_with(b"?>") => {
358                run_start = index + 2;
359                XmlScan::Text
360            }
361            XmlScan::Declaration if byte == b'>' => {
362                run_start = index + 1;
363                XmlScan::Text
364            }
365            other => other,
366        };
367    }
368    // A text run or CDATA section that the text ends in.
369    shape.copy(copied, bytes.get(run_start..).unwrap_or_default());
370    if let Some(start) = prefix_start {
371        shape.prefix(bytes.get(start..).unwrap_or_default());
372    }
373    shape
374}
375
376/// Bytes that roxmltree 0.21.1 and this module hold while they parse a text
377/// of `shape`, besides nodes past the reserved slots. roxmltree reserves a
378/// node for each `<` and an attribute for each `=` before it parses, and
379/// shrinking the attribute vector copies the attributes. Each element that
380/// declares a namespace stores its declarations and copies the namespaces
381/// in scope: at most one per declared prefix, the default namespace and
382/// `xml`.
383fn xml_storage_bytes(shape: &XmlShape<'_>, prefixes: u64) -> Option<u64> {
384    let namespace_indices = shape
385        .declarations
386        .checked_mul(prefixes.checked_add(3)?)?
387        .checked_add(1)?;
388    XML_FIXED_BYTES
389        .checked_add(shape.less_than.checked_mul(XML_NODE_BYTES)?)?
390        .checked_add(
391            shape
392                .equals
393                .checked_add(shape.attributes)?
394                .checked_mul(XML_ATTRIBUTE_BYTES)?,
395        )?
396        .checked_add(shape.depth.checked_add(2)?.checked_mul(XML_LEVEL_BYTES)?)?
397        .checked_add(
398            shape
399                .cdata
400                .checked_mul(2)?
401                .checked_add(1)?
402                .checked_mul(XML_TEXT_PIECE_BYTES)?,
403        )?
404        .checked_add(shape.copied.checked_mul(XML_COPY_FACTOR)?)?
405        .checked_add(shape.copies.checked_mul(XML_COPY_BYTES)?)?
406        .checked_add(
407            shape
408                .declarations
409                .checked_add(1)?
410                .checked_mul(2 * XML_NAMESPACE_BYTES)?,
411        )?
412        .checked_add(namespace_indices.checked_mul(3 * 2)?)
413}
414
415/// The most nodes whose storage fits in `rest` bytes besides the `reserved`
416/// slots. Up to `reserved` nodes, shrinking the node vector copies each node
417/// once. Past them the vector doubles, and growing or shrinking it can hold
418/// three slots per node.
419fn xml_nodes_limit(rest: u64, reserved: u64) -> Option<u64> {
420    let grown = rest.checked_add(reserved.checked_mul(XML_NODE_BYTES)?)? / (3 * XML_NODE_BYTES);
421    Some(if grown > reserved {
422        grown
423    } else {
424        reserved.min(rest / XML_NODE_BYTES)
425    })
426}
427
428/// Upper bounds on a JSON parse: the nesting depth it can reach and the
429/// parts of a `serde_json::Value` tree.
430#[derive(Default)]
431struct JsonShape {
432    depth: u64,
433    /// Array elements and the root value.
434    values: u64,
435    /// Object members.
436    members: u64,
437    maps: u64,
438    strings: u64,
439}
440
441/// Scans `text` once, counting delimiters outside strings. With the
442/// `raw_value` feature, a string that spells the private raw-value token is
443/// parsed again, so its escaped delimiters can add depth and values that the
444/// visible delimiters do not show; when such a string occurs, every delimiter
445/// and escape counts.
446fn json_shape(text: &str) -> JsonShape {
447    let mut depth = 0_u64;
448    let mut visible = JsonShape {
449        values: 1,
450        ..JsonShape::default()
451    };
452    let mut every = JsonShape {
453        values: 1,
454        ..JsonShape::default()
455    };
456    let mut raw = false;
457    let mut string = JsonStringScan::default();
458    for &byte in text.as_bytes() {
459        let opening = u64::from(matches!(byte, b'[' | b'{'));
460        let escape = u64::from(byte == b'\\');
461        every.depth += opening + escape;
462        every.values += u64::from(byte == b',') + opening + escape;
463        every.members += u64::from(byte == b':') + escape;
464        every.maps += u64::from(byte == b'{') + escape;
465        every.strings += u64::from(byte == b'"') + escape;
466        if string.active {
467            raw |= string.byte(byte);
468            continue;
469        }
470        match byte {
471            b'"' => {
472                visible.strings += 1;
473                string = JsonStringScan {
474                    active: true,
475                    possible: true,
476                    ..JsonStringScan::default()
477                }
478            }
479            b'[' | b'{' => {
480                depth += 1;
481                visible.depth = visible.depth.max(depth);
482                visible.values += 1;
483                visible.maps += u64::from(byte == b'{');
484            }
485            b']' | b'}' if depth != 0 => depth -= 1,
486            b',' => visible.values += 1,
487            b':' => visible.members += 1,
488            _ => {}
489        }
490    }
491    if raw {
492        every
493    } else {
494        visible
495    }
496}
497
498/// Bytes that a `serde_json::Value` tree of `shape` may need. String
499/// contents, the deserializer's scratch buffer, which may hold twice the
500/// longest string, and a replayed raw value each stay within the text
501/// length. Every map node but a root holds at least five members.
502fn json_storage_bytes(length: u64, shape: &JsonShape) -> Option<u64> {
503    let map_nodes = shape.maps.checked_add(shape.members / 5)?;
504    JSON_FIXED_BYTES
505        .checked_add(length.checked_mul(4)?)?
506        .checked_add(shape.values.checked_mul(JSON_VALUE_BYTES)?)?
507        .checked_add(map_nodes.checked_mul(JSON_MAP_NODE_BYTES)?)?
508        .checked_add(shape.strings.checked_mul(JSON_STRING_BYTES)?)
509}
510
511const RAW_VALUE_TOKEN: &[u8] = b"$serde_json::private::RawValue";
512
513#[derive(Default)]
514struct JsonStringScan {
515    active: bool,
516    escaped: bool,
517    unicode_digits: u8,
518    unicode_value: u32,
519    matched: usize,
520    possible: bool,
521}
522
523impl JsonStringScan {
524    fn decoded(&mut self, byte: Option<u8>) {
525        if byte.is_some() && byte == RAW_VALUE_TOKEN.get(self.matched).copied() {
526            self.matched += 1;
527        } else {
528            self.possible = false;
529        }
530    }
531
532    /// Returns true only when a completed string spells the raw-value token.
533    fn byte(&mut self, byte: u8) -> bool {
534        if self.unicode_digits != 0 {
535            if let Some(digit) = char::from(byte).to_digit(16) {
536                self.unicode_value = self.unicode_value * 16 + digit;
537            } else {
538                self.possible = false;
539            }
540            self.unicode_digits -= 1;
541            if self.unicode_digits == 0 {
542                self.decoded(u8::try_from(self.unicode_value).ok());
543            }
544        } else if self.escaped {
545            self.escaped = false;
546            if byte == b'u' {
547                self.unicode_digits = 4;
548                self.unicode_value = 0;
549            } else {
550                self.decoded(match byte {
551                    b'"' | b'\\' | b'/' => Some(byte),
552                    b'b' => Some(8),
553                    b'f' => Some(12),
554                    b'n' => Some(b'\n'),
555                    b'r' => Some(b'\r'),
556                    b't' => Some(b'\t'),
557                    _ => None,
558                });
559            }
560        } else if byte == b'\\' {
561            self.escaped = true;
562        } else if byte == b'"' {
563            self.active = false;
564            return self.possible && self.matched == RAW_VALUE_TOKEN.len();
565        } else {
566            self.decoded(Some(byte));
567        }
568        false
569    }
570}
571
572#[cfg(test)]
573mod tests {
574    use std::fmt::Write as _;
575
576    use super::{
577        json_shape, xml_nodes_limit, xml_shape, xml_storage_bytes, MAX_XML_NAMESPACE_PREFIXES,
578        MAX_XML_TAG_ATTRIBUTES, XML_NODE_BYTES,
579    };
580    use crate::decode::{
581        u64_from_index, DecodeArena, DecodeContext, DecodePolicy, ResourceDimension,
582    };
583    use crate::CodecError;
584
585    /// Per-thread heap observation for the storage-bound tests.
586    mod heap {
587        use std::alloc::{GlobalAlloc, Layout, System};
588        use std::cell::Cell;
589
590        use crate::decode::u64_from_index;
591
592        thread_local! {
593            static TRACK: Cell<bool> = const { Cell::new(false) };
594            static LIVE: Cell<i128> = const { Cell::new(0) };
595            static PEAK: Cell<i128> = const { Cell::new(0) };
596        }
597
598        struct PeakAllocator;
599
600        #[global_allocator]
601        static ALLOCATOR: PeakAllocator = PeakAllocator;
602
603        /// A reallocation holds the old and the new block at once.
604        fn record(acquired: usize, released: usize) {
605            if TRACK.try_with(Cell::get).unwrap_or(false) {
606                let _recorded = LIVE.try_with(|live| {
607                    let held = live.get() + i128::from(u64_from_index(acquired));
608                    let _peaked = PEAK.try_with(|peak| peak.set(peak.get().max(held)));
609                    live.set(held - i128::from(u64_from_index(released)));
610                });
611            }
612        }
613
614        unsafe impl GlobalAlloc for PeakAllocator {
615            unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
616                let pointer = unsafe { System.alloc(layout) };
617                if !pointer.is_null() {
618                    record(layout.size(), 0);
619                }
620                pointer
621            }
622
623            unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
624                let pointer = unsafe { System.alloc_zeroed(layout) };
625                if !pointer.is_null() {
626                    record(layout.size(), 0);
627                }
628                pointer
629            }
630
631            unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
632                let resized = unsafe { System.realloc(pointer, layout, new_size) };
633                if !resized.is_null() {
634                    record(new_size, layout.size());
635                }
636                resized
637            }
638
639            unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
640                unsafe { System.dealloc(pointer, layout) };
641                record(0, layout.size());
642            }
643        }
644
645        /// The most heap bytes that `action` held at once on this thread,
646        /// besides what it returns.
647        pub(super) fn peak<R>(action: impl FnOnce() -> R) -> (R, u64) {
648            LIVE.with(|live| live.set(0));
649            PEAK.with(|peak| peak.set(0));
650            TRACK.with(|track| track.set(true));
651            let result = action();
652            TRACK.with(|track| track.set(false));
653            let peak = u64::try_from(PEAK.with(Cell::get)).expect("peak fits u64");
654            (result, peak)
655        }
656    }
657
658    /// Node storage as [`xml_nodes_limit`] inverts it.
659    fn xml_node_storage(nodes: u64, reserved: u64) -> u64 {
660        if nodes <= reserved {
661            nodes * XML_NODE_BYTES
662        } else {
663            3 * nodes * XML_NODE_BYTES - reserved * XML_NODE_BYTES
664        }
665    }
666
667    /// What `parse_xml` admits `text` against: storage and its nodes.
668    fn xml_estimate(text: &str, nodes: u64) -> u64 {
669        let shape = xml_shape(text);
670        let storage = xml_storage_bytes(&shape, u64_from_index(shape.prefixes.len()))
671            .expect("storage fits u64");
672        storage + xml_node_storage(nodes, shape.less_than)
673    }
674
675    fn repeated(count: usize, part: impl Fn(usize) -> String) -> String {
676        (0..count).map(part).collect()
677    }
678
679    /// A FreeCAD-shaped document: indented objects with typed properties.
680    fn realistic_document(objects: usize) -> String {
681        let declarations = repeated(objects, |index| {
682            format!("\n        <Object type=\"Part::Feature\" name=\"Box{index}\" id=\"{index}\"/>")
683        });
684        let data = repeated(objects, |index| {
685            format!(
686                "\n        <Object name=\"Box{index}\">\n            <Properties Count=\"2\">\
687                 \n                <Property name=\"Label\" type=\"App::PropertyString\">\
688                 \n                    <String value=\"Box &amp; {index}\"/>\
689                 \n                </Property>\
690                 \n                <Property name=\"Length\" type=\"App::PropertyLength\">\
691                 \n                    <Float value=\"10.0\"/>\
692                 \n                </Property>\n            </Properties>\n        </Object>"
693            )
694        });
695        format!(
696            "<?xml version='1.0' encoding='utf-8'?>\n<Document SchemaVersion=\"4\">\
697             \n    <Objects Count=\"{objects}\">{declarations}\n    </Objects>\
698             \n    <ObjectData Count=\"{objects}\">{data}\n    </ObjectData>\n</Document>\n"
699        )
700    }
701
702    #[derive(Debug, serde::Deserialize, PartialEq)]
703    struct JsonRecord {
704        name: String,
705        values: Vec<u64>,
706    }
707
708    fn with_depth<T>(depth: u64, run: impl FnOnce(&DecodeContext<'_>) -> T) -> T {
709        let arena = DecodeArena::new();
710        let mut policy = DecodePolicy::default();
711        policy.limits.max_recursion_depth = depth;
712        let (ctx, _) = DecodeContext::from_root_bytes(b"", &arena, &policy).expect("valid fixture");
713        run(&ctx)
714    }
715
716    fn with_expand_ceiling<T>(bytes: u64, run: impl FnOnce(&DecodeContext<'_>) -> T) -> T {
717        let arena = DecodeArena::new();
718        let mut policy = DecodePolicy::default();
719        policy.limits.max_decompressed_bytes_per_expand = bytes;
720        let (ctx, _) = DecodeContext::from_root_bytes(b"", &arena, &policy).expect("valid fixture");
721        run(&ctx)
722    }
723
724    fn is_depth_refusal<T: std::fmt::Debug>(result: &Result<T, CodecError>) -> bool {
725        matches!(result, Err(CodecError::ResourceLimit(limit)) if limit.dimension == ResourceDimension::RecursionDepth)
726    }
727
728    fn is_unsupported<T: std::fmt::Debug>(result: &Result<T, CodecError>, text: &str) -> bool {
729        matches!(result, Err(CodecError::NotImplemented(message)) if message.contains(text))
730    }
731
732    #[test]
733    fn xml_parses_and_rejects_malformed_text() {
734        with_depth(256, |ctx| {
735            let document = ctx
736                .parse_xml("<r a=\"1\"><c/></r>", "XML")
737                .expect("valid XML");
738            assert_eq!(document.root_element().attribute("a"), Some("1"));
739            assert!(matches!(
740                ctx.parse_xml("<r>", "XML"),
741                Err(CodecError::Malformed(_))
742            ));
743        });
744    }
745
746    #[test]
747    fn xml_depth_is_charged_to_the_session() {
748        let nested = "<a><b><c/></b></a>";
749        with_depth(3, |ctx| assert!(ctx.parse_xml(nested, "XML").is_ok()));
750        with_depth(2, |ctx| {
751            assert!(is_depth_refusal(&ctx.parse_xml(nested, "XML")));
752        });
753    }
754
755    #[test]
756    fn xml_shape_counts_tag_attributes_and_declared_prefixes() {
757        let shape = xml_shape(
758            "<?xml version=\"1.0\"?><r xmlns=\"u\" xmlns:p=\"u\" a=\"x=y\"><!-- b=c xmlns:q='u' -->\
759             <p:c xmlns:p ='v' d='1'/><![CDATA[e=f]]>g=h</r>",
760        );
761        assert_eq!(shape.attributes, 5);
762        assert_eq!(shape.tag_attributes, 3);
763        assert_eq!(shape.declarations, 3);
764        assert_eq!(shape.prefixes.into_iter().collect::<Vec<_>>(), [&b"p"[..]]);
765    }
766
767    #[test]
768    fn xml_shape_counts_reserved_slots_and_copied_text() {
769        let shape = xml_shape(
770            "<r a='x&amp;y' b='p\tq' c='z=w'><!-- < = -->t&lt;u<![CDATA[v]]>w<s/>\r\n</r>",
771        );
772        assert_eq!(shape.less_than, 6);
773        assert_eq!(shape.equals, 5);
774        assert_eq!(shape.cdata, 1);
775        // Two values, the run that joins the CDATA, and the run with `\r`.
776        assert_eq!(shape.copies, 4);
777        assert_eq!(shape.copied, 7 + 3 + 20 + 2);
778    }
779
780    #[test]
781    fn xml_shape_depth_matches_the_parsed_tree() {
782        for text in [
783            "<a/>",
784            "<a>x</a>",
785            "<a></a>",
786            "<a><b> </b></a>",
787            "<a><b/></a>",
788            "<a><b></b><c><d>e</d></c></a>",
789            "<!--c--><a/><?p?>",
790            "<?xml version='1.0'?><a><b><!--c--></b></a>",
791            "<a><b><?p x?></b></a>",
792            "<a><b><![CDATA[x]]></b></a>",
793            "<a b='/' c=\">\"><d e='1' /></a >",
794            "<p:a xmlns:p='u'><p:b>t</p:b></p:a>",
795            "<a>\r\n<b>\r\n</b>\r\n</a>\n",
796        ] {
797            let document = roxmltree::Document::parse(text).expect(text);
798            let depth = document
799                .descendants()
800                .map(|node| u64_from_index(node.ancestors().count()) - 1)
801                .max();
802            assert_eq!(Some(xml_shape(text).depth), depth, "{text}");
803        }
804    }
805
806    #[test]
807    fn xml_nodes_limit_inverts_the_node_storage() {
808        for reserved in [0, 1, 7, 1000] {
809            for rest in [
810                0,
811                71,
812                72,
813                215,
814                216,
815                1000,
816                144_000,
817                144_215,
818                144_216,
819                1 << 20,
820            ] {
821                let limit = xml_nodes_limit(rest, reserved).expect("limit fits u64");
822                assert!(
823                    xml_node_storage(limit, reserved) <= rest,
824                    "{reserved} {rest}"
825                );
826                assert!(
827                    xml_node_storage(limit + 1, reserved) > rest,
828                    "{reserved} {rest}"
829                );
830            }
831        }
832    }
833
834    #[test]
835    fn xml_storage_bound_covers_the_parse_peak() {
836        let count = 1 << 12;
837        let root_prefixes = repeated(100, |index| format!(" xmlns:q{index}='u'"));
838        let documents = [
839            ("elements", format!("<r>{}</r>", "<a/>".repeat(count))),
840            (
841                "attributes",
842                format!("<r>{}</r>", "<a b='1' c='2' d='3' e='4'/>".repeat(count)),
843            ),
844            (
845                "equals in text",
846                format!("<r>{}</r>", "=".repeat(16 * count)),
847            ),
848            (
849                "less-than in a comment",
850                format!("<r><!--{}--></r>", "<".repeat(16 * count)),
851            ),
852            (
853                "text around CDATA",
854                format!("<r>{}</r>", "x<![CDATA[y]]>".repeat(count)),
855            ),
856            (
857                "references in text",
858                format!("<r>{}</r>", "<a>&amp;&lt;</a>".repeat(count)),
859            ),
860            (
861                "references in values",
862                format!("<r>{}</r>", "<a b='&amp;x&#10;'/>".repeat(count)),
863            ),
864            (
865                "carriage returns",
866                format!("<r>{}</r>", "a\r\n".repeat(4 * count)),
867            ),
868            (
869                "nesting",
870                format!("{}{}", "<a>".repeat(count), "</a>".repeat(count)),
871            ),
872            (
873                "namespaces in scope",
874                format!(
875                    "<r xmlns='u'{root_prefixes}>{}</r>",
876                    "<p:a xmlns:p='v' p:b='1'/>".repeat(count / 4)
877                ),
878            ),
879            ("realistic", realistic_document(count / 4)),
880        ];
881        // roxmltree recurses once per level, past a test thread's stack.
882        std::thread::Builder::new()
883            .stack_size(64 << 20)
884            .spawn(move || {
885                with_depth(1 << 12, |ctx| {
886                    for (label, text) in &documents {
887                        let (document, peak) = heap::peak(|| ctx.parse_xml(text, "XML"));
888                        let nodes = u64_from_index(document.expect(label).descendants().count());
889                        let estimate = xml_estimate(text, nodes);
890                        assert!(
891                            peak <= estimate,
892                            "{label}: peak {peak} > estimate {estimate}"
893                        );
894                    }
895                });
896            })
897            .expect("spawn the parsing thread")
898            .join()
899            .expect("every peak within its estimate");
900    }
901
902    #[cfg(target_pointer_width = "64")]
903    #[test]
904    fn xml_storage_bound_stays_near_a_realistic_parse_peak() {
905        let text = realistic_document(1 << 10);
906        with_depth(256, |ctx| {
907            let (document, peak) = heap::peak(|| ctx.parse_xml(&text, "XML"));
908            let nodes = u64_from_index(document.expect("realistic XML").descendants().count());
909            let estimate = xml_estimate(&text, nodes);
910            assert!(
911                estimate <= 2 * peak,
912                "estimate {estimate} > twice peak {peak}"
913            );
914        });
915    }
916
917    #[test]
918    fn xml_depth_refuses_before_the_parser_recurses() {
919        let levels = 1 << 15;
920        let nested = format!("{}{}", "<a>".repeat(levels), "</a>".repeat(levels));
921        with_depth(256, |ctx| {
922            assert!(is_depth_refusal(&ctx.parse_xml(&nested, "XML")));
923        });
924    }
925
926    #[test]
927    fn xml_tree_past_the_per_expand_allowance_is_unsupported() {
928        let document = |elements: usize| format!("<r>{}</r>", "<a/>".repeat(elements));
929        with_expand_ceiling(1 << 20, |ctx| {
930            assert!(ctx.parse_xml(&document(1 << 10), "XML").is_ok());
931            assert!(is_unsupported(
932                &ctx.parse_xml(&document(1 << 14), "XML"),
933                "the XML tree could need more than the 1048576-byte per-expansion allowance"
934            ));
935            assert_eq!(ctx.resource_refusal(), None);
936        });
937    }
938
939    #[test]
940    fn xml_tag_attributes_past_their_limit_are_unsupported() {
941        let tag = |attributes: u64| {
942            (0..attributes).fold(String::from("<r"), |mut tag, index| {
943                write!(tag, " a{index}=''").expect("write attribute");
944                tag + if index + 1 == attributes { "/>" } else { "" }
945            })
946        };
947        with_depth(256, |ctx| {
948            assert!(ctx.parse_xml(&tag(MAX_XML_TAG_ATTRIBUTES), "XML").is_ok());
949            assert!(is_unsupported(
950                &ctx.parse_xml(&tag(MAX_XML_TAG_ATTRIBUTES + 1), "XML"),
951                "an XML tag has more than 256 attributes"
952            ));
953            assert_eq!(ctx.resource_refusal(), None);
954        });
955    }
956
957    #[test]
958    fn xml_namespace_prefixes_past_their_limit_are_unsupported() {
959        let document = |prefixes: u64| {
960            let mut text = String::from("<r>");
961            for index in 0..prefixes {
962                write!(text, "<c xmlns:p{index}='u'/>").expect("write declaration");
963            }
964            text + "</r>"
965        };
966        with_depth(256, |ctx| {
967            assert!(ctx
968                .parse_xml(&document(MAX_XML_NAMESPACE_PREFIXES), "XML")
969                .is_ok());
970            assert!(is_unsupported(
971                &ctx.parse_xml(&document(MAX_XML_NAMESPACE_PREFIXES + 1), "XML"),
972                "the XML text declares more than 256 namespace prefixes"
973            ));
974            assert_eq!(ctx.resource_refusal(), None);
975        });
976    }
977
978    #[test]
979    fn json_depth_follows_the_session_past_the_parser_limit() {
980        let nested = format!("{}{}", "[".repeat(200), "]".repeat(200));
981        with_depth(256, |ctx| {
982            assert!(ctx.parse_json_value(&nested, "JSON").is_ok());
983            assert!(ctx.parse_json::<serde_json::Value>(&nested, "JSON").is_ok());
984        });
985        with_depth(199, |ctx| {
986            assert!(is_depth_refusal(&ctx.parse_json_value(&nested, "JSON")));
987        });
988    }
989
990    #[test]
991    fn json_tree_past_the_per_expand_allowance_is_unsupported() {
992        let document = |maps: usize| format!("[{}]", vec![r#"{"a":0}"#; maps].join(","));
993        with_expand_ceiling(1 << 20, |ctx| {
994            assert!(ctx.parse_json_value(&document(1 << 8), "JSON").is_ok());
995            let message =
996                "the JSON tree could need more than the 1048576-byte per-expansion allowance";
997            assert!(is_unsupported(
998                &ctx.parse_json_value(&document(1 << 11), "JSON"),
999                message
1000            ));
1001            assert!(is_unsupported(
1002                &ctx.parse_json::<serde_json::Value>(&document(1 << 11), "JSON"),
1003                message
1004            ));
1005            assert_eq!(ctx.resource_refusal(), None);
1006        });
1007    }
1008
1009    #[test]
1010    fn json_value_and_typed_parse() {
1011        with_depth(256, |ctx| {
1012            let value = ctx
1013                .parse_json_value(r#"{"a":[1,2]}"#, "JSON")
1014                .expect("valid JSON");
1015            assert_eq!(value, serde_json::json!({"a": [1, 2]}));
1016            let record: JsonRecord = ctx
1017                .parse_json(r#"{"name":"r","values":[3]}"#, "JSON")
1018                .expect("valid JSON");
1019            assert_eq!(
1020                record,
1021                JsonRecord {
1022                    name: "r".into(),
1023                    values: vec![3],
1024                }
1025            );
1026            assert!(matches!(
1027                ctx.parse_json_value("{", "JSON"),
1028                Err(CodecError::Malformed(_))
1029            ));
1030        });
1031    }
1032
1033    #[test]
1034    fn typed_json_keeps_duplicate_field_errors() {
1035        with_depth(256, |ctx| {
1036            let text = r#"{"name":"a","name":"b","values":[]}"#;
1037            assert!(matches!(
1038                ctx.parse_json::<JsonRecord>(text, "JSON"),
1039                Err(CodecError::Malformed(_))
1040            ));
1041            let value = ctx.parse_json_value(text, "JSON").expect("valid JSON");
1042            assert_eq!(value["name"], "b");
1043        });
1044    }
1045
1046    #[test]
1047    fn typed_json_ignores_raw_carrier_extensions() {
1048        with_depth(256, |ctx| {
1049            let text = r#"{"name":"r","values":[],"extension":{"$serde_json::private::RawValue":"not-json"}}"#;
1050            let record: JsonRecord = ctx.parse_json(text, "JSON").expect("valid JSON");
1051            assert_eq!(record.name, "r");
1052        });
1053    }
1054
1055    #[test]
1056    fn json_shape_counts_delimiters_outside_strings_and_raw_replay() {
1057        assert_eq!(json_shape(r#"["[[["]"#).depth, 1);
1058        let shape = json_shape(r#"{"a":[[1,2]],"b":"x,y:z"}"#);
1059        assert_eq!(shape.depth, 3);
1060        assert_eq!(shape.values, 6);
1061        assert_eq!(shape.members, 2);
1062        assert_eq!(shape.maps, 1);
1063        assert_eq!(shape.strings, 3);
1064        let raw = r#"{"$serde_json::private::RawValue":"\u005b0\u005d"}"#;
1065        assert!(json_shape(raw).depth >= 2);
1066        let escaped_key = r#"{"\u0024serde_json::private::RawValue":"[0]"}"#;
1067        assert!(json_shape(escaped_key).depth >= 2);
1068        with_depth(1, |ctx| {
1069            assert!(is_depth_refusal(&ctx.parse_json_value(raw, "JSON")));
1070        });
1071        with_depth(256, |ctx| {
1072            let value = ctx
1073                .parse_json_value(raw, "JSON")
1074                .expect("valid raw carrier");
1075            assert_eq!(value, serde_json::json!([0]));
1076        });
1077    }
1078}