Skip to main content

xml_sec/xml/dom/
tree.rs

1//! Parser-independent retained XML tree consumed by XML Security algorithms.
2
3use std::{
4    hash::{Hash, Hasher},
5    ops::Range,
6};
7
8use super::storage::{Native, Storage, Values};
9use super::{ParseError, ParsingOptions, XmlBackend};
10
11#[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
12pub(super) mod comparison;
13
14const XML_NAMESPACE_URI: &str = "http://www.w3.org/XML/1998/namespace";
15
16/// Stable arena index for a node in one parsed document.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct NodeId(u32);
19
20impl NodeId {
21    pub(crate) const fn get(self) -> u32 {
22        self.0
23    }
24    pub(crate) fn get_usize(self) -> usize {
25        self.index()
26    }
27    fn index(self) -> usize {
28        self.0 as usize
29    }
30}
31
32impl From<u32> for NodeId {
33    fn from(value: u32) -> Self {
34        Self(value)
35    }
36}
37
38/// Kind of a semantic XML node.
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum NodeType {
41    /// Synthetic document root.
42    Root,
43    /// XML element.
44    Element,
45    /// Expanded character data.
46    Text,
47    /// XML comment.
48    Comment,
49    /// Processing instruction.
50    PI,
51}
52
53pub(super) struct NodeData<S: Storage = Native> {
54    pub(super) parent: Option<NodeId>,
55    pub(super) children: S::Values<NodeId>,
56    pub(super) kind: NodeKind<S>,
57    pub(super) range: Range<usize>,
58    pub(super) range_actionable: bool,
59    pub(super) subtree_end: u32,
60    // Document-order XPath index: the node itself, then its attribute and
61    // namespace nodes, so every subtree occupies one contiguous index range.
62    #[cfg(feature = "xmldsig")]
63    pub(super) xpath_start: u32,
64}
65
66pub(super) enum NodeKind<S: Storage = Native> {
67    Root,
68    Element {
69        name: S::Text,
70        namespace: Option<S::Text>,
71        prefix: Option<S::Text>,
72        attributes: S::Values<AttributeData<S>>,
73        namespaces: S::Values<NamespaceData<S>>,
74    },
75    Text(S::Text),
76    Comment(S::Text),
77    PI {
78        target: S::Text,
79        value: Option<S::Text>,
80    },
81}
82
83pub(super) struct AttributeData<S: Storage = Native> {
84    pub(super) name: S::Text,
85    pub(super) namespace: Option<S::Text>,
86    pub(super) prefix: Option<S::Text>,
87    pub(super) value: S::Text,
88    pub(super) is_id: bool,
89    pub(super) xml_id_error: Option<XmlIdError>,
90}
91
92/// Non-fatal xml:id constraint diagnostic; ID type assignment is preserved.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum XmlIdError {
95    /// The normalized value is not an XML NCName.
96    InvalidNcName,
97    /// A declaration assigns a type other than ID to this xml:id attribute.
98    InvalidDeclaredType,
99    /// Both the normalized value and a declared type violate the constraints.
100    InvalidNcNameAndDeclaredType,
101}
102
103pub(super) struct NamespaceData<S: Storage = Native> {
104    pub(super) prefix: Option<S::Text>,
105    pub(super) uri: S::Text,
106}
107
108impl<S: Storage> std::fmt::Debug for AttributeData<S> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("AttributeData")
111            .field("name", &self.name)
112            .field("namespace", &self.namespace)
113            .field("prefix", &self.prefix)
114            .field("value", &self.value)
115            .field("is_id", &self.is_id)
116            .field("xml_id_error", &self.xml_id_error)
117            .finish()
118    }
119}
120impl<S: Storage> PartialEq for AttributeData<S> {
121    fn eq(&self, other: &Self) -> bool {
122        self.name.as_ref() == other.name.as_ref()
123            && self.namespace.as_deref() == other.namespace.as_deref()
124            && self.prefix.as_deref() == other.prefix.as_deref()
125            && self.value.as_ref() == other.value.as_ref()
126            && self.is_id == other.is_id
127            && self.xml_id_error == other.xml_id_error
128    }
129}
130impl<S: Storage> Eq for AttributeData<S> {}
131impl<S: Storage> std::fmt::Debug for NamespaceData<S> {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_struct("NamespaceData")
134            .field("prefix", &self.prefix)
135            .field("uri", &self.uri)
136            .finish()
137    }
138}
139impl<S: Storage> PartialEq for NamespaceData<S> {
140    fn eq(&self, other: &Self) -> bool {
141        self.prefix.as_deref() == other.prefix.as_deref() && self.uri.as_ref() == other.uri.as_ref()
142    }
143}
144impl<S: Storage> Eq for NamespaceData<S> {}
145impl<S: Storage> Hash for NamespaceData<S> {
146    fn hash<H: Hasher>(&self, state: &mut H) {
147        self.prefix.as_deref().hash(state);
148        self.uri.as_ref().hash(state);
149    }
150}
151impl<S: Storage> Clone for NamespaceData<S>
152where
153    S::Text: Clone,
154{
155    fn clone(&self) -> Self {
156        Self {
157            prefix: self.prefix.clone(),
158            uri: self.uri.clone(),
159        }
160    }
161}
162impl<S: Storage> std::fmt::Debug for NodeKind<S> {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            Self::Root => f.write_str("Root"),
166            Self::Text(value) => f.debug_tuple("Text").field(value).finish(),
167            Self::Comment(value) => f.debug_tuple("Comment").field(value).finish(),
168            Self::PI { target, value } => f
169                .debug_struct("PI")
170                .field("target", target)
171                .field("value", value)
172                .finish(),
173            Self::Element {
174                name,
175                namespace,
176                prefix,
177                attributes,
178                namespaces,
179            } => f
180                .debug_struct("Element")
181                .field("name", name)
182                .field("namespace", namespace)
183                .field("prefix", prefix)
184                .field("attributes", &&attributes[..])
185                .field("namespaces", &&namespaces[..])
186                .finish(),
187        }
188    }
189}
190impl<S: Storage> PartialEq for NodeKind<S> {
191    fn eq(&self, other: &Self) -> bool {
192        match (self, other) {
193            (Self::Root, Self::Root) => true,
194            (Self::Text(a), Self::Text(b)) | (Self::Comment(a), Self::Comment(b)) => {
195                a.as_ref() == b.as_ref()
196            }
197            (
198                Self::PI {
199                    target: a,
200                    value: av,
201                },
202                Self::PI {
203                    target: b,
204                    value: bv,
205                },
206            ) => a.as_ref() == b.as_ref() && av.as_deref() == bv.as_deref(),
207            (
208                Self::Element {
209                    name: a,
210                    namespace: an,
211                    prefix: ap,
212                    attributes: aa,
213                    namespaces: ans,
214                },
215                Self::Element {
216                    name: b,
217                    namespace: bn,
218                    prefix: bp,
219                    attributes: ba,
220                    namespaces: bns,
221                },
222            ) => {
223                a.as_ref() == b.as_ref()
224                    && an.as_deref() == bn.as_deref()
225                    && ap.as_deref() == bp.as_deref()
226                    && aa[..] == ba[..]
227                    && ans[..] == bns[..]
228            }
229            _ => false,
230        }
231    }
232}
233impl<S: Storage> Eq for NodeKind<S> {}
234impl<S: Storage> std::fmt::Debug for NodeData<S> {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        f.debug_struct("NodeData")
237            .field("parent", &self.parent)
238            .field("children", &&self.children[..])
239            .field("kind", &self.kind)
240            .field("range", &self.range)
241            .field("range_actionable", &self.range_actionable)
242            .field("subtree_end", &self.subtree_end)
243            .finish()
244    }
245}
246
247/// Builder used by parser adapters to populate the shared semantic arena.
248pub(super) struct TreeBuilder<'input, S: Storage = Native> {
249    input: &'input str,
250    nodes: S::Values<NodeData<S>>,
251    storage: S,
252}
253
254impl<'input, S: Storage> TreeBuilder<'input, S> {
255    pub(super) fn storage(&self) -> S {
256        self.storage
257    }
258    pub(super) fn new(input: &'input str, capacity: usize, storage: S) -> Result<Self, ParseError> {
259        Ok(Self {
260            input,
261            nodes: storage.values(capacity)?,
262            storage,
263        })
264    }
265
266    #[cfg(feature = "xml-backend-xmloxide")]
267    pub(super) fn push(
268        &mut self,
269        parent: Option<NodeId>,
270        kind: NodeKind<S>,
271        range: Range<usize>,
272    ) -> Result<NodeId, ParseError> {
273        self.push_with_actionability(parent, kind, range, true)
274    }
275
276    pub(super) fn push_with_actionability(
277        &mut self,
278        parent: Option<NodeId>,
279        kind: NodeKind<S>,
280        range: Range<usize>,
281        range_actionable: bool,
282    ) -> Result<NodeId, ParseError> {
283        let id = NodeId(
284            u32::try_from(self.nodes.len()).expect("bounded XML node count must fit into u32"),
285        );
286        self.nodes.push(NodeData {
287            parent,
288            children: self.storage.values(0)?,
289            kind,
290            range,
291            range_actionable,
292            subtree_end: id.0 + 1,
293            #[cfg(feature = "xmldsig")]
294            xpath_start: 0,
295        })?;
296        if let Some(parent) = parent {
297            self.nodes[parent.index()].children.push(id)?;
298        }
299        Ok(id)
300    }
301
302    pub(super) fn finish_subtree(&mut self, node: NodeId) {
303        self.nodes[node.index()].subtree_end =
304            u32::try_from(self.nodes.len()).expect("bounded XML node count must fit into u32");
305    }
306
307    #[cfg(feature = "xml-backend-xmloxide")]
308    pub(super) fn has_text_tail(&self, parent: NodeId) -> bool {
309        self.nodes[parent.index()]
310            .children
311            .last()
312            .is_some_and(|last| matches!(self.nodes[last.index()].kind, NodeKind::Text(_)))
313    }
314
315    #[cfg(feature = "xml-backend-xmloxide")]
316    pub(super) fn append_text(
317        &mut self,
318        parent: NodeId,
319        value: &str,
320        range: Range<usize>,
321        range_actionable: bool,
322    ) -> Result<(), ParseError> {
323        if let Some(last) = self.nodes[parent.index()].children.last().copied()
324            && let node = &mut self.nodes[last.index()]
325            && let NodeKind::Text(existing) = &mut node.kind
326        {
327            self.storage.append(existing, value)?;
328            // One semantic text node can fold several adjacent lexical tokens
329            // (plain text, CDATA, and references). Its source range must cover
330            // every token so mutation never splices only a semantic prefix.
331            node.range.start = node.range.start.min(range.start);
332            node.range.end = node.range.end.max(range.end);
333            node.range_actionable &= range_actionable;
334            return Ok(());
335        }
336        self.push_with_actionability(
337            Some(parent),
338            NodeKind::Text(self.storage.text(value)?),
339            range,
340            range_actionable,
341        )?;
342        Ok(())
343    }
344
345    #[cfg(feature = "xml-backend-xmloxide")]
346    pub(super) fn len(&self) -> usize {
347        self.nodes.len()
348    }
349
350    #[cfg(feature = "xml-backend-xmloxide")]
351    pub(super) fn namespaces(&self, node: NodeId) -> Option<&[NamespaceData<S>]> {
352        match &self.nodes[node.index()].kind {
353            NodeKind::Element { namespaces, .. } => Some(namespaces),
354            _ => None,
355        }
356    }
357
358    pub(super) fn finish<P: Storage>(
359        mut self,
360        preflight: &super::LexicalPreflight<'_, P>,
361    ) -> Result<Document<'input, S>, ParseError> {
362        self.storage.charge(self.nodes.len())?;
363        #[cfg(feature = "xmldsig")]
364        let mut xpath_nodes = 0_u32;
365        for node in &mut *self.nodes {
366            #[cfg(feature = "xmldsig")]
367            {
368                node.xpath_start = xpath_nodes;
369                let axes = match &node.kind {
370                    NodeKind::Element {
371                        attributes,
372                        namespaces,
373                        ..
374                    } => attributes.len().checked_add(namespaces.len()),
375                    _ => Some(0),
376                };
377                xpath_nodes = axes
378                    .and_then(|axes| u32::try_from(axes).ok())
379                    .and_then(|axes| axes.checked_add(1))
380                    .and_then(|count| xpath_nodes.checked_add(count))
381                    .ok_or(ParseError::NodesLimitReached)?;
382            }
383            if let NodeKind::Element {
384                name,
385                prefix,
386                attributes,
387                ..
388            } = &mut node.kind
389            {
390                self.storage.charge(attributes.len())?;
391                for attribute in &mut **attributes {
392                    let xml_id = attribute.namespace.as_deref() == Some(XML_NAMESPACE_URI)
393                        && attribute.name.as_ref() == "id";
394                    let declaration = preflight.attribute_declaration(
395                        prefix.as_deref(),
396                        name.as_ref(),
397                        attribute.prefix.as_deref(),
398                        attribute.name.as_ref(),
399                    )?;
400                    attribute.is_id = xml_id
401                        || declaration
402                            .is_some_and(|info| info.kind == crate::document::DtdAttributeType::Id);
403                    if attribute.is_id {
404                        // xml:id section 4 and XML 1.0 section 3.3.3 require
405                        // ID normalization in the infoset, not just the index.
406                        // https://www.w3.org/TR/2005/REC-xml-id-20050909/#processing
407                        // https://www.w3.org/TR/2008/REC-xml-20081126/#AVNormalize
408                        self.storage.normalize_id(&mut attribute.value)?;
409                        if xml_id {
410                            // xml:id sections 4/6: report constraints without
411                            // suppressing assignment or making parsing fatal.
412                            // https://www.w3.org/TR/2005/REC-xml-id-20050909/#processing
413                            self.storage.charge(attribute.value.as_ref().len())?;
414                            let invalid_value =
415                                !crate::xml::is_xml_ncname(attribute.value.as_ref());
416                            let invalid_type =
417                                declaration.is_some_and(|info| info.has_non_id_declaration);
418                            attribute.xml_id_error = match (invalid_value, invalid_type) {
419                                (true, false) => Some(XmlIdError::InvalidNcName),
420                                (false, true) => Some(XmlIdError::InvalidDeclaredType),
421                                (true, true) => Some(XmlIdError::InvalidNcNameAndDeclaredType),
422                                (false, false) => None,
423                            };
424                        }
425                    }
426                }
427            }
428        }
429        Ok(Document {
430            input: self.input,
431            nodes: self.nodes,
432            #[cfg(feature = "xmldsig")]
433            xpath_nodes,
434        })
435    }
436
437    #[cfg(feature = "xml-backend-roxmltree")]
438    pub(super) fn input(&self) -> &'input str {
439        self.input
440    }
441}
442
443/// Parsed backend-neutral XML document.
444///
445/// The selected parser is used only while constructing this arena. C14N,
446/// XPath, XMLDSig, XMLEnc, and mutation code never branch on the parser.
447pub struct Document<'input, S: Storage = Native> {
448    input: &'input str,
449    nodes: S::Values<NodeData<S>>,
450    #[cfg(feature = "xmldsig")]
451    xpath_nodes: u32,
452}
453
454impl<S: Storage> std::fmt::Debug for Document<'_, S> {
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        f.debug_struct("Document")
457            .field("input", &self.input)
458            .field("nodes", &&self.nodes[..])
459            .finish()
460    }
461}
462
463impl<'input> Document<'input> {
464    /// Parses XML with default backend-neutral options.
465    pub fn parse(input: &'input str) -> Result<Self, ParseError> {
466        Self::parse_with_options(input, ParsingOptions::default())
467    }
468
469    /// Parses XML with the build's default backend.
470    pub fn parse_with_options(
471        input: &'input str,
472        options: ParsingOptions,
473    ) -> Result<Self, ParseError> {
474        Self::parse_with_options_and_backend(input, options, XmlBackend::default())
475    }
476
477    /// Parses XML with an explicitly selected compiled backend.
478    pub fn parse_with_backend(input: &'input str, backend: XmlBackend) -> Result<Self, ParseError> {
479        Self::parse_with_options_and_backend(input, ParsingOptions::default(), backend)
480    }
481
482    /// Parses XML with explicit parser options and backend selection.
483    pub fn parse_with_options_and_backend(
484        input: &'input str,
485        options: ParsingOptions,
486        backend: XmlBackend,
487    ) -> Result<Self, ParseError> {
488        let options = crate::document::preflight_dom_limits(input, options)?;
489        Self::parse_after_limit_preflight_with_backend(input, options, backend)
490    }
491
492    pub(crate) fn parse_after_limit_preflight_with_backend(
493        input: &'input str,
494        options: ParsingOptions,
495        backend: XmlBackend,
496    ) -> Result<Self, ParseError> {
497        let preflight = super::LexicalPreflight::scan(input, options.allow_dtd)?;
498        backend.parse(input, options, &preflight)
499    }
500
501    #[cfg(feature = "xmldsig")]
502    pub(crate) fn parse_after_limit_preflight_in<'w, W: structured_asn1::memory::Work>(
503        input: &'input str,
504        options: ParsingOptions,
505        backend: XmlBackend,
506        storage: super::storage::Accounted<'w, W>,
507    ) -> Result<Document<'input, super::storage::Accounted<'w, W>>, super::AccountedParseError>
508    {
509        let preflight = super::LexicalPreflight::scan_in(input, options.allow_dtd, storage)?;
510        backend.parse_in(input, options, &preflight, storage)
511    }
512}
513
514impl<'input, S: Storage> Document<'input, S> {
515    #[cfg(feature = "xmldsig")]
516    pub(crate) fn node_count(&self) -> usize {
517        self.nodes.len()
518    }
519
520    /// Returns the original UTF-8 source.
521    pub fn input_text(&self) -> &'input str {
522        self.input
523    }
524
525    /// Returns the synthetic document root.
526    pub fn root(&self) -> Node<'_, 'input, S> {
527        self.get_node(NodeId(0))
528            .expect("parsed document has a root")
529    }
530
531    /// Returns the document element.
532    pub fn root_element(&self) -> Node<'_, 'input, S> {
533        self.root()
534            .children()
535            .find(Node::is_element)
536            .expect("well-formed XML has a document element")
537    }
538
539    /// Iterates the document root and all descendants in document order.
540    pub fn descendants(&self) -> Descendants<'_, 'input, S> {
541        self.root().descendants()
542    }
543
544    /// Resolves an arena node ID in this document.
545    pub fn get_node(&self, id: NodeId) -> Option<Node<'_, 'input, S>> {
546        self.nodes
547            .get(id.index())
548            .map(|_| Node { document: self, id })
549    }
550
551    /// Number of XPath nodes: tree nodes with their attribute and namespace nodes.
552    #[cfg(feature = "xmldsig")]
553    pub(crate) fn xpath_node_count(&self) -> u32 {
554        self.xpath_nodes
555    }
556
557    /// Tree node owning a document-order XPath index (itself for tree nodes).
558    #[cfg(feature = "xmldsig")]
559    pub(crate) fn xpath_owner(&self, index: u32) -> Option<Node<'_, 'input, S>> {
560        if index >= self.xpath_nodes {
561            return None;
562        }
563        // The root starts at index zero, so every valid index has an owner.
564        let owner = self.nodes.partition_point(|node| node.xpath_start <= index) - 1;
565        self.get_node(NodeId(
566            u32::try_from(owner).expect("bounded XML node count must fit into u32"),
567        ))
568    }
569}
570
571impl Document<'_> {
572    #[cfg(all(
573        test,
574        feature = "xml-backend-roxmltree",
575        feature = "xml-backend-xmloxide"
576    ))]
577    pub(super) fn ensure_semantically_equivalent(
578        &self,
579        other: &Self,
580        doctype: Option<&Range<usize>>,
581    ) -> Result<(), ParseError> {
582        self.ensure_semantically_equivalent_in(other, doctype, Native)
583    }
584}
585
586#[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
587fn source_ranges_equivalent(
588    left: &Range<usize>,
589    right: &Range<usize>,
590    doctype: Option<&Range<usize>>,
591) -> bool {
592    if left == right {
593        return true;
594    }
595    let Some(doctype) = doctype else {
596        return false;
597    };
598    // Entity-expanded nodes have no single lexical span in the instance:
599    // roxmltree reports declaration bytes while xmloxide reports reference
600    // bytes. Both positions encode the same synthetic entity provenance.
601    range_contains(doctype, left) != range_contains(doctype, right)
602}
603
604#[cfg(feature = "xml-backend-roxmltree")]
605pub(super) fn range_contains(container: &Range<usize>, candidate: &Range<usize>) -> bool {
606    candidate.start >= container.start && candidate.end <= container.end
607}
608
609/// Copyable handle into a backend-neutral document arena.
610pub struct Node<'a, 'input, S: Storage = Native> {
611    document: &'a Document<'input, S>,
612    id: NodeId,
613}
614
615impl<S: Storage> Copy for Node<'_, '_, S> {}
616impl<S: Storage> Clone for Node<'_, '_, S> {
617    fn clone(&self) -> Self {
618        *self
619    }
620}
621impl<S: Storage> std::fmt::Debug for Node<'_, '_, S> {
622    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623        f.debug_struct("Node")
624            .field("document", &self.document)
625            .field("id", &self.id)
626            .finish()
627    }
628}
629impl<S: Storage> PartialEq for Node<'_, '_, S> {
630    fn eq(&self, other: &Self) -> bool {
631        std::ptr::eq(self.document, other.document) && self.id == other.id
632    }
633}
634impl<S: Storage> Eq for Node<'_, '_, S> {}
635impl<S: Storage> Hash for Node<'_, '_, S> {
636    fn hash<H: Hasher>(&self, state: &mut H) {
637        std::ptr::from_ref(self.document).hash(state);
638        self.id.hash(state);
639    }
640}
641
642impl<'a, 'input, S: Storage> Node<'a, 'input, S> {
643    fn data(self) -> &'a NodeData<S> {
644        &self.document.nodes[self.id.index()]
645    }
646    /// Returns this node's stable arena ID.
647    pub fn id(self) -> NodeId {
648        self.id
649    }
650    /// Returns the owning document.
651    pub fn document(self) -> &'a Document<'input, S> {
652        self.document
653    }
654    /// Returns the semantic node kind.
655    pub fn node_type(self) -> NodeType {
656        match self.data().kind {
657            NodeKind::Root => NodeType::Root,
658            NodeKind::Element { .. } => NodeType::Element,
659            NodeKind::Text(_) => NodeType::Text,
660            NodeKind::Comment(_) => NodeType::Comment,
661            NodeKind::PI { .. } => NodeType::PI,
662        }
663    }
664    /// Returns whether this is the synthetic document root.
665    pub fn is_root(&self) -> bool {
666        self.node_type() == NodeType::Root
667    }
668    /// Returns whether this is an element.
669    pub fn is_element(&self) -> bool {
670        self.node_type() == NodeType::Element
671    }
672    /// Returns whether this is text.
673    pub fn is_text(&self) -> bool {
674        self.node_type() == NodeType::Text
675    }
676    /// Returns whether this is a comment.
677    pub fn is_comment(&self) -> bool {
678        self.node_type() == NodeType::Comment
679    }
680    /// Returns whether this is a processing instruction.
681    pub fn is_pi(&self) -> bool {
682        self.node_type() == NodeType::PI
683    }
684    /// Returns the source byte range represented by this node.
685    pub fn range(self) -> Range<usize> {
686        self.data().range.clone()
687    }
688    pub(crate) fn has_actionable_range(self) -> bool {
689        self.data().range_actionable
690    }
691    /// Returns the parent node.
692    pub fn parent(self) -> Option<Self> {
693        self.data().parent.and_then(|id| self.document.get_node(id))
694    }
695    /// Returns the parent only when it is an element.
696    pub fn parent_element(self) -> Option<Self> {
697        self.parent()
698            .and_then(|node| node.is_element().then_some(node))
699    }
700    /// Iterates direct children in document order.
701    pub fn children(self) -> Children<'a, 'input, S> {
702        Children {
703            document: self.document,
704            ids: self.data().children.iter(),
705        }
706    }
707    /// Returns the first direct child node.
708    pub fn first_child(self) -> Option<Self> {
709        self.children().next()
710    }
711    /// Returns the first direct element child.
712    pub fn first_element_child(self) -> Option<Self> {
713        self.children().find(Node::is_element)
714    }
715    /// Returns the last direct element child.
716    pub fn last_element_child(self) -> Option<Self> {
717        self.children().rev().find(Node::is_element)
718    }
719    /// Iterates this node and its descendants in document order.
720    pub fn descendants(self) -> Descendants<'a, 'input, S> {
721        Descendants {
722            document: self.document,
723            ids: self.id.0..self.data().subtree_end,
724        }
725    }
726    /// Document-order XPath index of this tree node.
727    #[cfg(feature = "xmldsig")]
728    pub(crate) fn xpath_index(self) -> u32 {
729        self.data().xpath_start
730    }
731    /// End of the contiguous XPath index range covering this subtree and its axes.
732    #[cfg(feature = "xmldsig")]
733    pub(crate) fn xpath_subtree_end(self) -> u32 {
734        self.document
735            .nodes
736            .get(self.data().subtree_end as usize)
737            .map_or(self.document.xpath_nodes, |next| next.xpath_start)
738    }
739    /// Iterates this node and then its ancestors.
740    pub fn ancestors(self) -> Ancestors<'a, 'input, S> {
741        Ancestors { next: Some(self) }
742    }
743    /// Returns the preceding sibling.
744    pub fn prev_sibling(self) -> Option<Self> {
745        let parent = self.parent()?;
746        let position = parent
747            .data()
748            .children
749            .iter()
750            .position(|id| *id == self.id)?;
751        position
752            .checked_sub(1)
753            .and_then(|index| self.document.get_node(parent.data().children[index]))
754    }
755    /// Returns the following sibling.
756    pub fn next_sibling(self) -> Option<Self> {
757        let parent = self.parent()?;
758        let position = parent
759            .data()
760            .children
761            .iter()
762            .position(|id| *id == self.id)?;
763        parent
764            .data()
765            .children
766            .get(position + 1)
767            .and_then(|id| self.document.get_node(*id))
768    }
769    /// Returns the next sibling that is an element.
770    pub fn next_sibling_element(self) -> Option<Self> {
771        let mut current = self.next_sibling();
772        while let Some(node) = current {
773            if node.is_element() {
774                return Some(node);
775            }
776            current = node.next_sibling();
777        }
778        None
779    }
780    /// Returns the expanded element name, or an empty name for non-elements.
781    pub fn tag_name(self) -> ExpandedName<'a> {
782        match &self.data().kind {
783            NodeKind::Element {
784                name, namespace, ..
785            } => ExpandedName {
786                name,
787                namespace: namespace.as_deref(),
788                match_namespace: true,
789            },
790            _ => ExpandedName {
791                name: "",
792                namespace: None,
793                match_namespace: true,
794            },
795        }
796    }
797    /// Tests an element against a local-name selector or an exact expanded name.
798    ///
799    /// A bare string matches any namespace, mirroring roxmltree. Tuple forms
800    /// match the namespace exactly, including `(None, name)` for an
801    /// unqualified element.
802    pub fn has_tag_name<'n, N>(self, name: N) -> bool
803    where
804        N: Into<ExpandedName<'n>>,
805    {
806        if !self.is_element() {
807            return false;
808        }
809        let name = name.into();
810        self.tag_name().name() == name.name()
811            && (!name.match_namespace || self.tag_name().namespace() == name.namespace())
812    }
813    /// Returns the element's lexical namespace prefix.
814    pub fn prefix(self) -> Option<&'a str> {
815        match &self.data().kind {
816            NodeKind::Element { prefix, .. } => prefix.as_deref(),
817            _ => None,
818        }
819    }
820    /// Iterates non-namespace attributes in source order.
821    pub fn attributes(self) -> Attributes<'a, S> {
822        let values: &[AttributeData<S>] = match &self.data().kind {
823            NodeKind::Element { attributes, .. } => &attributes[..],
824            _ => &[],
825        };
826        Attributes {
827            values: values.iter(),
828        }
829    }
830    /// Looks up an unqualified attribute by local name.
831    pub fn attribute<'n, N>(self, name: N) -> Option<&'a str>
832    where
833        N: Into<ExpandedName<'n>>,
834    {
835        let name = name.into();
836        self.attributes()
837            .find(|attr| attr.name() == name.name() && attr.namespace() == name.namespace())
838            .map(Attribute::value)
839    }
840    /// Iterates in-scope namespace bindings.
841    pub fn namespaces(self) -> Namespaces<'a, S> {
842        let values: &[NamespaceData<S>] = match &self.data().kind {
843            NodeKind::Element { namespaces, .. } => &namespaces[..],
844            _ => &[],
845        };
846        Namespaces {
847            values: values.iter(),
848        }
849    }
850    /// Resolves a namespace prefix in this element's in-scope bindings.
851    pub fn lookup_namespace_uri(self, prefix: Option<&str>) -> Option<&'a str> {
852        if prefix == Some("xml") {
853            return Some(XML_NAMESPACE_URI);
854        }
855        self.namespaces()
856            .find(|ns| ns.name() == prefix)
857            .map(Namespace::uri)
858    }
859    /// Finds an in-scope prefix for a namespace URI.
860    pub fn lookup_prefix(self, uri: &str) -> Option<&'a str> {
861        if uri == XML_NAMESPACE_URI {
862            return Some("xml");
863        }
864        self.namespaces()
865            .filter(|namespace| namespace.uri() == uri)
866            .find_map(Namespace::name)
867    }
868    /// Returns direct character data, or the first direct text child of an element.
869    pub fn text(self) -> Option<&'a str> {
870        match &self.data().kind {
871            NodeKind::Text(value) | NodeKind::Comment(value) => Some(value),
872            NodeKind::Element { .. } => {
873                self.first_child()
874                    .and_then(|child| match &child.data().kind {
875                        NodeKind::Text(value) => Some(value.as_ref()),
876                        _ => None,
877                    })
878            }
879            NodeKind::PI { .. } => None,
880            NodeKind::Root => None,
881        }
882    }
883    /// Returns processing-instruction data for a PI node.
884    pub fn pi(self) -> Option<PI<'a>> {
885        match &self.data().kind {
886            NodeKind::PI { target, value } => Some(PI {
887                target,
888                value: value.as_deref(),
889            }),
890            _ => None,
891        }
892    }
893}
894
895/// Expanded XML name used by element and attribute comparisons.
896#[derive(Clone, Copy, Debug)]
897pub struct ExpandedName<'a> {
898    name: &'a str,
899    namespace: Option<&'a str>,
900    // Selector intent is separate from the represented expanded name so
901    // equality remains a comparison of XML names, not matching syntax.
902    match_namespace: bool,
903}
904impl<'a> ExpandedName<'a> {
905    /// Returns the local name.
906    pub fn name(self) -> &'a str {
907        self.name
908    }
909    /// Returns the namespace URI.
910    pub fn namespace(self) -> Option<&'a str> {
911        self.namespace
912    }
913}
914impl PartialEq for ExpandedName<'_> {
915    fn eq(&self, other: &Self) -> bool {
916        self.name == other.name && self.namespace == other.namespace
917    }
918}
919impl Eq for ExpandedName<'_> {}
920impl<'a> From<&'a str> for ExpandedName<'a> {
921    fn from(name: &'a str) -> Self {
922        Self {
923            name,
924            namespace: None,
925            match_namespace: false,
926        }
927    }
928}
929impl<'a> From<(Option<&'a str>, &'a str)> for ExpandedName<'a> {
930    fn from((namespace, name): (Option<&'a str>, &'a str)) -> Self {
931        Self {
932            name,
933            namespace,
934            match_namespace: true,
935        }
936    }
937}
938impl<'a> From<(&'a str, &'a str)> for ExpandedName<'a> {
939    fn from((namespace, name): (&'a str, &'a str)) -> Self {
940        Self {
941            name,
942            namespace: Some(namespace),
943            match_namespace: true,
944        }
945    }
946}
947
948/// Borrowed semantic XML attribute.
949pub struct Attribute<'a, S: Storage = Native> {
950    data: &'a AttributeData<S>,
951}
952impl<S: Storage> Copy for Attribute<'_, S> {}
953impl<S: Storage> Clone for Attribute<'_, S> {
954    fn clone(&self) -> Self {
955        *self
956    }
957}
958impl<'a, S: Storage> Attribute<'a, S> {
959    /// Report non-fatal xml:id errors without changing ID type assignment.
960    ///
961    /// xml:id 1.0 sections 4 and 6 require assignment even for invalid values;
962    /// applications may inspect this diagnostic before relying on an ID.
963    /// https://www.w3.org/TR/2005/REC-xml-id-20050909/#processing
964    pub fn xml_id_error(self) -> Option<XmlIdError> {
965        self.data.xml_id_error
966    }
967    /// Whether XML processing assigned the ID type (DTD or xml:id).
968    pub fn is_id(self) -> bool {
969        self.data.is_id
970    }
971    /// Returns the local name.
972    pub fn name(self) -> &'a str {
973        &self.data.name
974    }
975    /// Returns the namespace URI.
976    pub fn namespace(self) -> Option<&'a str> {
977        self.data.namespace.as_deref()
978    }
979    /// Returns the expanded value.
980    pub fn value(self) -> &'a str {
981        &self.data.value
982    }
983    /// Returns the lexical namespace prefix.
984    pub fn prefix(self) -> Option<&'a str> {
985        self.data.prefix.as_deref()
986    }
987}
988
989/// Iterator over element attributes.
990pub struct Attributes<'a, S: Storage = Native> {
991    values: std::slice::Iter<'a, AttributeData<S>>,
992}
993impl<'a, S: Storage> Iterator for Attributes<'a, S> {
994    type Item = Attribute<'a, S>;
995    fn next(&mut self) -> Option<Self::Item> {
996        self.values.next().map(|data| Attribute { data })
997    }
998    fn nth(&mut self, n: usize) -> Option<Self::Item> {
999        self.values.nth(n).map(|data| Attribute { data })
1000    }
1001    fn size_hint(&self) -> (usize, Option<usize>) {
1002        self.values.size_hint()
1003    }
1004}
1005impl<S: Storage> ExactSizeIterator for Attributes<'_, S> {}
1006
1007/// Borrowed in-scope namespace binding.
1008pub struct Namespace<'a, S: Storage = Native> {
1009    data: &'a NamespaceData<S>,
1010}
1011impl<S: Storage> Copy for Namespace<'_, S> {}
1012impl<S: Storage> Clone for Namespace<'_, S> {
1013    fn clone(&self) -> Self {
1014        *self
1015    }
1016}
1017impl<'a, S: Storage> Namespace<'a, S> {
1018    /// Returns the prefix, or `None` for the default namespace.
1019    pub fn name(self) -> Option<&'a str> {
1020        self.data.prefix.as_deref()
1021    }
1022    /// Returns the namespace URI.
1023    pub fn uri(self) -> &'a str {
1024        &self.data.uri
1025    }
1026}
1027
1028/// Iterator over in-scope namespace bindings.
1029pub struct Namespaces<'a, S: Storage = Native> {
1030    values: std::slice::Iter<'a, NamespaceData<S>>,
1031}
1032impl<'a, S: Storage> Iterator for Namespaces<'a, S> {
1033    type Item = Namespace<'a, S>;
1034    fn next(&mut self) -> Option<Self::Item> {
1035        self.values.next().map(|data| Namespace { data })
1036    }
1037    fn nth(&mut self, n: usize) -> Option<Self::Item> {
1038        self.values.nth(n).map(|data| Namespace { data })
1039    }
1040    fn size_hint(&self) -> (usize, Option<usize>) {
1041        self.values.size_hint()
1042    }
1043}
1044impl<S: Storage> ExactSizeIterator for Namespaces<'_, S> {}
1045
1046/// Processing-instruction payload.
1047#[derive(Clone, Copy)]
1048pub struct PI<'a> {
1049    /// Processing-instruction target.
1050    pub target: &'a str,
1051    /// Optional processing-instruction data.
1052    pub value: Option<&'a str>,
1053}
1054
1055/// Iterator over direct child nodes.
1056pub struct Children<'a, 'input, S: Storage = Native> {
1057    document: &'a Document<'input, S>,
1058    ids: std::slice::Iter<'a, NodeId>,
1059}
1060impl<'a, 'input, S: Storage> Iterator for Children<'a, 'input, S> {
1061    type Item = Node<'a, 'input, S>;
1062    fn next(&mut self) -> Option<Self::Item> {
1063        self.ids.next().and_then(|id| self.document.get_node(*id))
1064    }
1065    fn size_hint(&self) -> (usize, Option<usize>) {
1066        self.ids.size_hint()
1067    }
1068}
1069impl<S: Storage> ExactSizeIterator for Children<'_, '_, S> {}
1070impl<'a, 'input, S: Storage> DoubleEndedIterator for Children<'a, 'input, S> {
1071    fn next_back(&mut self) -> Option<Self::Item> {
1072        self.ids
1073            .next_back()
1074            .and_then(|id| self.document.get_node(*id))
1075    }
1076}
1077
1078/// Pre-order iterator over a node and its descendants.
1079pub struct Descendants<'a, 'input, S: Storage = Native> {
1080    document: &'a Document<'input, S>,
1081    ids: Range<u32>,
1082}
1083impl<'a, 'input, S: Storage> Iterator for Descendants<'a, 'input, S> {
1084    type Item = Node<'a, 'input, S>;
1085    fn next(&mut self) -> Option<Self::Item> {
1086        self.ids
1087            .next()
1088            .and_then(|id| self.document.get_node(NodeId(id)))
1089    }
1090    fn size_hint(&self) -> (usize, Option<usize>) {
1091        self.ids.size_hint()
1092    }
1093}
1094impl<S: Storage> DoubleEndedIterator for Descendants<'_, '_, S> {
1095    fn next_back(&mut self) -> Option<Self::Item> {
1096        self.ids
1097            .next_back()
1098            .and_then(|id| self.document.get_node(NodeId(id)))
1099    }
1100}
1101impl<S: Storage> ExactSizeIterator for Descendants<'_, '_, S> {}
1102
1103/// Iterator over a node and its ancestors.
1104pub struct Ancestors<'a, 'input, S: Storage = Native> {
1105    next: Option<Node<'a, 'input, S>>,
1106}
1107impl<'a, 'input, S: Storage> Iterator for Ancestors<'a, 'input, S> {
1108    type Item = Node<'a, 'input, S>;
1109    fn next(&mut self) -> Option<Self::Item> {
1110        let current = self.next?;
1111        self.next = current.parent();
1112        Some(current)
1113    }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    #[cfg(feature = "xmldsig")]
1119    fn assert_scoped_projection<S: super::Storage>(document: &super::Document<'_, S>) {
1120        let root = document.root_element();
1121        assert_eq!(root.tag_name().name(), "r");
1122        assert_eq!(root.attribute(("urn:p", "x")), Some("value"));
1123        assert_eq!(root.lookup_namespace_uri(Some("p")), Some("urn:p"));
1124        let id = root
1125            .attributes()
1126            .find(|attribute| attribute.name() == "id")
1127            .expect("xml:id retained");
1128        assert!(id.is_id());
1129        assert_eq!(id.value(), "a b");
1130        assert_eq!(id.xml_id_error(), Some(super::XmlIdError::InvalidNcName));
1131        assert_eq!(root.children().len(), 1);
1132        assert_eq!(root.text(), Some("leftmiddleright"));
1133    }
1134
1135    #[cfg(all(feature = "xmldsig", feature = "xml-backend-roxmltree"))]
1136    #[test]
1137    fn roxmltree_projection_uses_the_actual_scoped_tree_storage() {
1138        // The source parser is deliberately outside this component test's
1139        // account. Every *semantic projection* allocation must be retained by
1140        // the supplied execution without changing IDs, namespaces or text.
1141        use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1142        let xml = "<r xmlns:p='urn:p' xml:id=' a  b ' p:x='value'>left<![CDATA[middle]]>right</r>";
1143        let preflight =
1144            super::super::LexicalPreflight::scan(xml, false).expect("fixed XML preflight");
1145        let raw = crate::parser_roxmltree::Document::parse(xml).expect("fixed backend XML");
1146        let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1147        let document = super::super::roxmltree::project_in(
1148            xml,
1149            &raw,
1150            &preflight,
1151            super::super::storage::Accounted(&memory),
1152        )
1153        .expect("paid projection fits");
1154        assert_scoped_projection(&document);
1155        assert!(memory.used() > 0);
1156        drop(document);
1157        assert_eq!(memory.used(), 0);
1158        let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
1159        assert!(matches!(
1160            super::super::roxmltree::project_in(
1161                xml,
1162                &raw,
1163                &preflight,
1164                super::super::storage::Accounted(&denied)
1165            ),
1166            Err(super::ParseError::SharedExecution(
1167                MemoryError::LimitExceeded
1168            ))
1169        ));
1170        assert_eq!(denied.used(), 0);
1171    }
1172
1173    #[cfg(all(feature = "xmldsig", feature = "xml-backend-roxmltree"))]
1174    #[test]
1175    fn scoped_parser_tree_and_canonicalizer_share_the_same_semantic_kernel() {
1176        // Switching both parser and semantic-tree storage must not require a
1177        // second serializer or alter canonical bytes, axes, IDs or comments.
1178        use crate::c14n::{C14nAlgorithm, C14nMode};
1179        use crate::xmldsig::types::{
1180            NodeSetMaterializationBudget,
1181            projection::{PaidKeys, Projection},
1182        };
1183        use structured_asn1::memory::{Memory, MemoryAccount};
1184        let source = "<r xmlns:p='urn:p' xml:id=' a  b ' p:x='value'>left<![CDATA[middle]]><!--c--><p:leaf/>right</r>";
1185        let expected = super::Document::parse(source).expect("native semantic baseline");
1186        let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
1187        let storage = super::super::storage::Accounted(&memory);
1188        let preflight = super::super::LexicalPreflight::scan_in(source, false, storage)
1189            .expect("funded lexical preflight");
1190        let document = super::super::roxmltree::RoxmltreeBackend::parse_in(
1191            source,
1192            Default::default(),
1193            &preflight,
1194            storage,
1195        )
1196        .expect("funded roxmltree parse");
1197        let nodes = Projection::subtree(
1198            document.root(),
1199            true,
1200            &NodeSetMaterializationBudget::default(),
1201            PaidKeys::new(&memory),
1202        )
1203        .expect("funded node projection");
1204        for mode in [
1205            C14nMode::Inclusive1_0,
1206            C14nMode::Inclusive1_1,
1207            C14nMode::Exclusive1_0,
1208        ] {
1209            for comments in [false, true] {
1210                let algorithm = C14nAlgorithm::new(mode, comments);
1211                let mut canonical = Vec::new();
1212                crate::c14n::canonicalize(&expected, None, &algorithm, &mut canonical)
1213                    .expect("native canonical baseline");
1214                let (actual, _) = crate::c14n::canonicalize_buffer_in(
1215                    &document,
1216                    &nodes,
1217                    (&algorithm, None),
1218                    None,
1219                    1 << 20,
1220                    &Default::default(),
1221                    &crate::c14n::scratch::Accounted(&memory),
1222                )
1223                .expect("funded canonicalization");
1224                assert_eq!(&*actual, &canonical);
1225            }
1226        }
1227        assert!(memory.used() > 0);
1228        drop(nodes);
1229        drop(document);
1230        drop(preflight);
1231        assert_eq!(memory.used(), 0);
1232    }
1233
1234    #[cfg(all(feature = "xmldsig", feature = "xml-backend-xmloxide"))]
1235    #[test]
1236    fn xmloxide_projection_uses_the_actual_scoped_tree_storage() {
1237        // The same tree kernel must retain paid coalescing, normalized xml:id,
1238        // namespace axes and attributes with the alternate selected backend.
1239        use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1240        let xml = "<r xmlns:p='urn:p' xml:id=' a  b ' p:x='value'>left<![CDATA[middle]]>right</r>";
1241        let preflight =
1242            super::super::LexicalPreflight::scan(xml, false).expect("fixed XML preflight");
1243        let raw = crate::parser_xmloxide::parser::parse_str_with_options(
1244            xml,
1245            &super::super::xmloxide::backend_options(),
1246        )
1247        .expect("fixed backend XML");
1248        let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1249        let document = super::super::xmloxide::project_in(
1250            xml,
1251            &raw,
1252            super::ParsingOptions::default(),
1253            &preflight,
1254            super::super::storage::Accounted(&memory),
1255        )
1256        .expect("paid projection fits");
1257        assert_scoped_projection(&document);
1258        assert!(memory.used() > 0);
1259        drop(document);
1260        assert_eq!(memory.used(), 0);
1261        let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
1262        assert!(matches!(
1263            super::super::xmloxide::project_in(
1264                xml,
1265                &raw,
1266                super::ParsingOptions::default(),
1267                &preflight,
1268                super::super::storage::Accounted(&denied)
1269            ),
1270            Err(super::ParseError::SharedExecution(
1271                MemoryError::LimitExceeded
1272            ))
1273        ));
1274        assert_eq!(denied.used(), 0);
1275    }
1276
1277    #[cfg(all(feature = "xmldsig", feature = "xml-backend-xmloxide"))]
1278    #[test]
1279    fn projection_node_admission_precedes_arena_and_payload_growth() {
1280        // A disallowed semantic node must not allocate its attributes or text
1281        // first, nor reserve a larger arena based on the source backend count.
1282        use super::super::storage::Accounted;
1283        use structured_asn1::memory::{Memory, MemoryAccount};
1284        let source = format!("<r a='{}'/>", "x".repeat(65536));
1285        let preflight =
1286            super::super::LexicalPreflight::scan(&source, false).expect("native lexical baseline");
1287        let raw = crate::parser_xmloxide::parser::parse_str_with_options(
1288            &source,
1289            &super::super::xmloxide::backend_options(),
1290        )
1291        .expect("native raw XML baseline");
1292        for (limit, capacity) in [(0, 0), (1, 4096)] {
1293            let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(capacity, 1 << 24)));
1294            let result = super::super::xmloxide::project_in(
1295                &source,
1296                &raw,
1297                super::ParsingOptions {
1298                    nodes_limit: limit,
1299                    allow_dtd: false,
1300                },
1301                &preflight,
1302                Accounted(&memory),
1303            );
1304            assert!(matches!(result, Err(super::ParseError::NodesLimitReached)));
1305            assert_eq!(memory.used(), 0);
1306        }
1307    }
1308
1309    use super::Document;
1310    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1311    use super::{NamespaceData, comparison::namespaces_equal};
1312    use crate::xml::dom::{ParseError, ParsingOptions, XmlBackend};
1313
1314    #[test]
1315    fn runtime_selection_never_falls_back_to_a_compiled_backend() {
1316        // A stable selector can be supplied by application configuration even
1317        // in a thin build. Missing implementations must fail explicitly.
1318        for backend in [
1319            XmlBackend::Xmloxide,
1320            XmlBackend::Roxmltree,
1321            XmlBackend::Differential,
1322        ] {
1323            let result = Document::parse_with_backend("<r/>", backend);
1324            if backend.is_available() {
1325                assert!(result.is_ok(), "{backend:?} should be available");
1326            } else {
1327                assert_eq!(
1328                    result.expect_err("an unavailable backend must not fall back"),
1329                    ParseError::BackendUnavailable { backend }
1330                );
1331            }
1332        }
1333    }
1334
1335    #[test]
1336    fn every_backend_enforces_the_same_active_namespace_ceiling() {
1337        // Backend selection must not change whether an adversarial namespace
1338        // stack is accepted. The 1024th binding is valid; the 1025th is not.
1339        let declarations = (0..1_023)
1340            .map(|index| format!(r#" xmlns:n{index}="urn:namespace:{index}""#))
1341            .collect::<String>();
1342        let accepted = format!(r#"<root{declarations}><child xmlns:p="urn:p"/></root>"#);
1343        let rejected = format!(
1344            r#"<root{declarations} xmlns:overflow="urn:overflow"><child xmlns:p="urn:p"/></root>"#
1345        );
1346
1347        for backend in XmlBackend::available() {
1348            Document::parse_with_backend(&accepted, backend)
1349                .unwrap_or_else(|error| panic!("{backend:?} rejected the ceiling: {error}"));
1350            assert_eq!(
1351                Document::parse_with_backend(&rejected, backend)
1352                    .expect_err("the 1025th active binding must be rejected"),
1353                ParseError::NamespaceBindingLimitReached {
1354                    maximum: 1_024,
1355                    actual: 1_025,
1356                },
1357                "{backend:?} must share the lexical namespace contract"
1358            );
1359        }
1360    }
1361
1362    #[cfg(feature = "xml-backend-differential")]
1363    #[test]
1364    fn compatibility_feature_selects_differential_by_default() {
1365        assert_eq!(XmlBackend::default(), XmlBackend::Differential);
1366    }
1367
1368    #[cfg(all(
1369        feature = "xml-backend-xmloxide",
1370        feature = "xml-backend-roxmltree",
1371        not(feature = "xml-backend-differential")
1372    ))]
1373    #[test]
1374    fn fat_build_defaults_to_xmloxide_without_implicit_double_parsing() {
1375        assert_eq!(XmlBackend::default(), XmlBackend::Xmloxide);
1376    }
1377
1378    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1379    #[test]
1380    fn fat_build_selects_each_semantically_equivalent_runtime_mode() {
1381        // Runtime selection changes only arena construction; downstream XML
1382        // Security semantics and stable source ranges remain identical.
1383        let xml = r#"<r xmlns:p="urn:p"><p:a x="1">value</p:a></r>"#;
1384        let snapshot = |backend| {
1385            let document = Document::parse_with_backend(xml, backend)
1386                .expect("every compiled runtime mode must parse the fixture");
1387            document
1388                .descendants()
1389                .map(|node| {
1390                    (
1391                        node.node_type(),
1392                        node.range(),
1393                        node.text().map(str::to_owned),
1394                    )
1395                })
1396                .collect::<Vec<_>>()
1397        };
1398
1399        let xmloxide = snapshot(XmlBackend::Xmloxide);
1400        assert_eq!(snapshot(XmlBackend::Roxmltree), xmloxide);
1401        assert_eq!(snapshot(XmlBackend::Differential), xmloxide);
1402    }
1403
1404    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1405    #[test]
1406    fn differential_namespace_axis_comparison_preserves_multiplicity() {
1407        // The fail-closed gate must reject unequal axes even if a malformed
1408        // backend projection repeats one binding and hides another.
1409        let binding = |prefix: &str, uri: &str| NamespaceData::<super::Native> {
1410            prefix: Some(prefix.to_owned()),
1411            uri: uri.to_owned(),
1412        };
1413        let a = binding("a", "urn:a");
1414        let b = binding("b", "urn:b");
1415
1416        assert!(
1417            !namespaces_equal(
1418                &[a.clone(), a.clone()],
1419                &[a.clone(), b.clone()],
1420                super::Native,
1421            )
1422            .expect("duplicate axis comparison")
1423        );
1424        assert!(
1425            !namespaces_equal(&[a.clone(), b], &[a.clone(), a.clone()], super::Native,)
1426                .expect("reversed duplicate axis comparison")
1427        );
1428        assert!(
1429            namespaces_equal(&[a.clone(), a.clone()], &[a.clone(), a], super::Native,)
1430                .expect("equal multiplicities")
1431        );
1432    }
1433
1434    #[test]
1435    fn selected_backend_preserves_element_source_ranges() {
1436        // Mutation must splice exact element ranges under either parser.
1437        let xml = "<?p before?><root><empty/><child>value</child></root><!--after-->";
1438        let document = Document::parse(xml).expect("fixture must parse");
1439        let root = document.root_element();
1440        let child = root
1441            .children()
1442            .find(|node| node.has_tag_name("child"))
1443            .expect("fixture must contain the child element");
1444        assert_eq!(
1445            &xml[root.range()],
1446            "<root><empty/><child>value</child></root>"
1447        );
1448        assert_eq!(&xml[child.range()], "<child>value</child>");
1449    }
1450
1451    #[test]
1452    fn selected_backend_materializes_shadowed_namespace_axis() {
1453        // C14N observes the complete in-scope axis, including undeclarations.
1454        let document = Document::parse(
1455            r#"<r xmlns="urn:outer" xmlns:p="urn:p1"><p:a xmlns:p="urn:p2" xmlns=""><p:b/></p:a></r>"#,
1456        ).expect("fixture must parse");
1457        let child = document
1458            .descendants()
1459            .find(|node| node.has_tag_name(("urn:p2", "b")))
1460            .expect("fixture must contain the namespaced descendant");
1461        assert_eq!(child.lookup_namespace_uri(Some("p")), Some("urn:p2"));
1462        assert_eq!(child.lookup_namespace_uri(None), Some(""));
1463        assert_eq!(child.namespaces().count(), 2);
1464    }
1465
1466    #[test]
1467    fn tag_name_matcher_distinguishes_local_from_unqualified_names() {
1468        // A bare string mirrors roxmltree's local-name selector, while an
1469        // explicit optional namespace represents exact expanded-name intent.
1470        let document = Document::parse(r#"<r xmlns:p="urn:p"><item/><p:item/></r>"#)
1471            .expect("namespace matching fixture must parse");
1472        let mut items = document
1473            .root_element()
1474            .children()
1475            .filter(|node| node.is_element());
1476        let unqualified = items.next().expect("unqualified item must exist");
1477        let namespaced = items.next().expect("namespaced item must exist");
1478
1479        assert!(unqualified.has_tag_name("item"));
1480        assert!(namespaced.has_tag_name("item"));
1481        assert!(unqualified.has_tag_name((None, "item")));
1482        assert!(!namespaced.has_tag_name((None, "item")));
1483        assert!(namespaced.has_tag_name((Some("urn:p"), "item")));
1484    }
1485
1486    #[test]
1487    fn namespace_lookup_includes_the_predefined_xml_binding() {
1488        // Namespaces in XML binds `xml` on every element without requiring a
1489        // lexical declaration in the source document.
1490        let document = Document::parse("<root><child xml:lang=\"en\"/></root>")
1491            .expect("predefined namespace fixture must parse");
1492        let child = document
1493            .root_element()
1494            .first_element_child()
1495            .expect("fixture must contain a child");
1496
1497        assert_eq!(
1498            child.lookup_namespace_uri(Some("xml")),
1499            Some("http://www.w3.org/XML/1998/namespace")
1500        );
1501        assert_eq!(
1502            child.lookup_prefix("http://www.w3.org/XML/1998/namespace"),
1503            Some("xml")
1504        );
1505    }
1506
1507    #[test]
1508    fn selected_backend_folds_cdata_and_entities_into_one_text_node() {
1509        // XMLDSig defines character data after entity and CDATA expansion.
1510        let document = Document::parse_with_options(
1511            "<!DOCTYPE r [<!ENTITY value 'two'>]><r>one<![CDATA[+]]>&value;three</r>",
1512            ParsingOptions {
1513                allow_dtd: true,
1514                nodes_limit: 8,
1515            },
1516        )
1517        .expect("bounded internal entity fixture must parse");
1518        let text = document
1519            .root_element()
1520            .children()
1521            .filter(|node| node.is_text())
1522            .collect::<Vec<_>>();
1523        assert_eq!(text.len(), 1);
1524        assert_eq!(text[0].text(), Some("one+twothree"));
1525    }
1526
1527    #[test]
1528    fn selected_backend_maps_builtin_references_into_the_surrounding_text_range() {
1529        // xmloxide expands predefined references into its adjacent text node;
1530        // the lexical sidecar must mirror that boundary rather than invent a
1531        // second semantic text node or lose the original source range.
1532        let xml = "<r>left&amp;right</r>";
1533        let document = Document::parse(xml).expect("fixture must parse");
1534        let text = document
1535            .root_element()
1536            .first_child()
1537            .expect("text child must exist");
1538
1539        assert_eq!(text.text(), Some("left&right"));
1540        assert_eq!(&xml[text.range()], "left&amp;right");
1541    }
1542
1543    #[test]
1544    fn selected_backend_rejects_dtd_before_building_a_tree() {
1545        // The parser option must fail closed before backend-specific handling.
1546        assert_eq!(
1547            Document::parse("<!DOCTYPE r><r/>")
1548                .expect_err("DTD-disabled parsing must reject a document type"),
1549            ParseError::DtdDetected,
1550        );
1551    }
1552
1553    #[test]
1554    fn selected_backend_enforces_semantic_node_limit_after_text_folding() {
1555        // The shared ceiling counts retained nodes rather than lexical events.
1556        let error = Document::parse_with_options(
1557            "<r><a/><b/></r>",
1558            ParsingOptions {
1559                allow_dtd: false,
1560                nodes_limit: 3,
1561            },
1562        )
1563        .expect_err("the semantic node limit must reject the fourth node");
1564        assert_eq!(error, ParseError::NodesLimitReached);
1565    }
1566
1567    #[test]
1568    fn selected_backend_enforces_the_absolute_node_ceiling_before_dom_allocation() {
1569        // Direct DOM callers may request an unbounded parser, but the crate's
1570        // process-safety ceiling must still stop a compact wide document before
1571        // either backend allocates its complete native tree.
1572        let count = crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize;
1573        let xml = format!("<r>{}</r>", "<n/>".repeat(count));
1574
1575        assert_eq!(
1576            Document::parse_with_options(
1577                &xml,
1578                ParsingOptions {
1579                    allow_dtd: false,
1580                    nodes_limit: u32::MAX,
1581                },
1582            )
1583            .expect_err("the absolute retained-node ceiling must remain effective"),
1584            ParseError::NodesLimitReached,
1585        );
1586    }
1587
1588    #[test]
1589    fn selected_backend_enforces_the_absolute_byte_ceiling_before_dom_allocation() {
1590        // Direct semantic-DOM callers have no policy object that can bound the
1591        // source, so the crate ceiling must run before either native backend.
1592        let maximum = crate::hard_limits::XML_DOCUMENT_BYTE_CEILING;
1593        let xml = format!("<r>{}</r>", "x".repeat(maximum));
1594        let expected = ParseError::ByteLimitReached {
1595            maximum,
1596            actual: xml.len(),
1597        };
1598
1599        assert_eq!(
1600            Document::parse(&xml)
1601                .expect_err("default direct parsing must enforce the byte ceiling"),
1602            expected,
1603        );
1604        assert_eq!(
1605            Document::parse_with_options(
1606                &xml,
1607                ParsingOptions {
1608                    allow_dtd: false,
1609                    nodes_limit: u32::MAX,
1610                },
1611            )
1612            .expect_err("custom direct parsing must retain the absolute byte ceiling"),
1613            expected,
1614        );
1615    }
1616
1617    #[test]
1618    fn selected_backend_bounds_direct_entity_expansion_count() {
1619        // A shallow sequence of references bypasses roxmltree's nested-entity
1620        // loop detector. Direct DOM parsing still needs a finite aggregate cap.
1621        let references =
1622            "&value;".repeat(crate::hard_limits::XML_ENTITY_EXPANSION_CEILING as usize + 1);
1623        let xml = format!("<!DOCTYPE r [<!ENTITY value 'x'>]><r>{references}</r>");
1624
1625        assert!(matches!(
1626            Document::parse_with_options(
1627                &xml,
1628                ParsingOptions {
1629                    allow_dtd: true,
1630                    nodes_limit: u32::MAX,
1631                },
1632            ),
1633            Err(ParseError::EntityExpansionLimitReached { maximum, actual })
1634                if maximum == crate::hard_limits::XML_ENTITY_EXPANSION_CEILING
1635                    && actual == maximum + 1
1636        ));
1637    }
1638
1639    #[test]
1640    fn selected_backend_bounds_source_positions_after_semantic_text_folding() {
1641        // Many adjacent CDATA tokens retain one semantic text node, but the
1642        // xmloxide range adapter still needs one source position per token.
1643        // Bound that sidecar independently before constructing a backend DOM.
1644        let segments = crate::hard_limits::XML_SOURCE_POSITION_CEILING + 1;
1645        let xml = format!("<r>{}</r>", "<![CDATA[x]]>".repeat(segments));
1646
1647        assert!(matches!(
1648            Document::parse_with_options(
1649                &xml,
1650                ParsingOptions {
1651                    allow_dtd: false,
1652                    nodes_limit: 3,
1653                },
1654            ),
1655            Err(ParseError::SourcePositionLimitReached { maximum, actual })
1656                if maximum == crate::hard_limits::XML_SOURCE_POSITION_CEILING
1657                    && actual == maximum + 1
1658        ));
1659    }
1660
1661    #[test]
1662    fn unqualified_attribute_lookup_requires_no_namespace() {
1663        // Schema attributes such as Algorithm and URI are unqualified. A
1664        // same-local-name extension attribute must never satisfy that lookup.
1665        let document = Document::parse(
1666            r#"<r xmlns:evil="urn:evil" evil:Algorithm="extension" Algorithm="schema"/>"#,
1667        )
1668        .expect("fixture must parse");
1669        let root = document.root_element();
1670
1671        assert_eq!(root.attribute("Algorithm"), Some("schema"));
1672        assert_eq!(root.attribute(("urn:evil", "Algorithm")), Some("extension"));
1673
1674        let extension_only =
1675            Document::parse(r#"<r xmlns:evil="urn:evil" evil:Algorithm="extension"/>"#)
1676                .expect("fixture must parse");
1677        assert_eq!(extension_only.root_element().attribute("Algorithm"), None);
1678    }
1679
1680    #[test]
1681    fn node_text_preserves_character_data_contract() {
1682        // PI data is not character data, and element text follows the first
1683        // direct child contract rather than searching later descendants.
1684        let document = Document::parse("<r><?p hidden?>visible</r>").expect("fixture must parse");
1685        let root = document.root_element();
1686        let pi = root.first_child().expect("PI must exist");
1687
1688        assert_eq!(pi.text(), None);
1689        assert_eq!(pi.pi().and_then(|value| value.value), Some("hidden"));
1690        assert_eq!(root.text(), None);
1691        assert_eq!(
1692            pi.next_sibling().and_then(|node| node.text()),
1693            Some("visible")
1694        );
1695
1696        let nested =
1697            Document::parse("<r><child>nested</child>later</r>").expect("fixture must parse");
1698        assert_eq!(nested.root_element().text(), None);
1699    }
1700
1701    #[test]
1702    fn selected_backend_rejects_deep_documents_before_dom_parsing() {
1703        // Direct DOM callers do not run operation-policy preflight. The common
1704        // lexical pass must enforce the absolute ceiling before either DOM.
1705        const DEPTH: usize = 20_000;
1706        let mut xml = String::with_capacity(DEPTH * 7);
1707        xml.push_str(&"<n>".repeat(DEPTH));
1708        xml.push_str(&"</n>".repeat(DEPTH));
1709
1710        assert!(matches!(
1711            Document::parse(&xml),
1712            Err(ParseError::DepthLimitReached { maximum, actual })
1713                if maximum == crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING
1714                    && actual == maximum + 1
1715        ));
1716    }
1717
1718    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1719    #[test]
1720    fn differential_comparison_fails_closed_on_semantic_divergence() {
1721        // Differential mode must reject adapter disagreement rather than
1722        // silently selecting one parser's interpretation of attacker input.
1723        let left = Document::parse("<r><a/></r>").expect("fixture must parse");
1724        let mut right = Document::parse("<r><a/></r>").expect("fixture must parse");
1725        right.nodes[2].range.end -= 1;
1726
1727        assert!(matches!(
1728            left.ensure_semantically_equivalent(&right, None),
1729            Err(ParseError::BackendDivergence { .. })
1730        ));
1731    }
1732
1733    #[test]
1734    fn dtd_internal_comments_are_not_document_nodes() {
1735        // A `]` or `>` in an internal-subset comment must not terminate DTD
1736        // range detection and leak that comment into the semantic document.
1737        let document = Document::parse_with_options(
1738            "<!--before--><!DOCTYPE r [<!-- ] > --><!ENTITY value 'ok'>]><r>&value;</r><!--after-->",
1739            ParsingOptions {
1740                allow_dtd: true,
1741                ..ParsingOptions::default()
1742            },
1743        )
1744        .expect("DTD fixture must parse");
1745        let root = document.root();
1746        let comments = root
1747            .descendants()
1748            .filter(|node| node.is_comment())
1749            .filter_map(|node| node.text())
1750            .collect::<Vec<_>>();
1751
1752        assert_eq!(comments, ["before", "after"]);
1753        assert_eq!(
1754            root.descendants()
1755                .find(|node| node.is_text())
1756                .and_then(|node| node.text()),
1757            Some("ok")
1758        );
1759    }
1760
1761    #[test]
1762    fn dtd_range_ignores_doctype_text_in_prolog_nodes() {
1763        // DOCTYPE-like text in ordinary prolog nodes must not redirect DTD
1764        // filtering away from the actual declaration and leak subset nodes.
1765        let document = Document::parse_with_options(
1766            "<!-- <!DOCTYPE fake> --><?probe <!DOCTYPE fake> ?><!DOCTYPE r [<!--hidden-->]><r/>",
1767            ParsingOptions {
1768                allow_dtd: true,
1769                ..ParsingOptions::default()
1770            },
1771        )
1772        .expect("DTD fixture must parse");
1773        let root = document.root();
1774        let comments = root
1775            .children()
1776            .filter(|node| node.is_comment())
1777            .filter_map(|node| node.text())
1778            .collect::<Vec<_>>();
1779        let processing_instructions = root
1780            .children()
1781            .filter(|node| node.is_pi())
1782            .filter_map(|node| node.pi())
1783            .collect::<Vec<_>>();
1784
1785        assert_eq!(comments, [" <!DOCTYPE fake> "]);
1786        assert_eq!(processing_instructions.len(), 1);
1787        assert_eq!(processing_instructions[0].target, "probe");
1788    }
1789
1790    #[cfg(feature = "xml-backend-roxmltree")]
1791    #[test]
1792    fn dtd_range_scanning_accepts_unicode_names() {
1793        // Scanner offsets are bytes; a multibyte XML name must not become an
1794        // invalid UTF-8 slicing boundary while locating the DTD terminator.
1795        let input = "<!DOCTYPE r [<!ENTITY café 'ok'>]><r/>";
1796        let end = input.find("><r/>").expect("fixture has document root") + 1;
1797
1798        let preflight = super::super::LexicalPreflight::scan(input, true)
1799            .expect("Unicode DTD fixture must pass lexical preflight");
1800        assert_eq!(preflight.doctype_range(), Some(&(0..end)));
1801    }
1802}