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 following sibling in constant time. The arena stores each
770    /// subtree contiguously in preorder, so the node after this subtree is the
771    /// next sibling exactly when it has the same parent.
772    #[cfg(feature = "xmldsig")]
773    pub(crate) fn following_sibling(self) -> Option<Self> {
774        let parent = self.data().parent?;
775        let next = NodeId(self.data().subtree_end);
776        let data = self.document.nodes.get(next.index())?;
777        (data.parent == Some(parent)).then_some(Self {
778            document: self.document,
779            id: next,
780        })
781    }
782    /// Returns the next sibling that is an element.
783    pub fn next_sibling_element(self) -> Option<Self> {
784        let mut current = self.next_sibling();
785        while let Some(node) = current {
786            if node.is_element() {
787                return Some(node);
788            }
789            current = node.next_sibling();
790        }
791        None
792    }
793    /// Returns the expanded element name, or an empty name for non-elements.
794    pub fn tag_name(self) -> ExpandedName<'a> {
795        match &self.data().kind {
796            NodeKind::Element {
797                name, namespace, ..
798            } => ExpandedName {
799                name,
800                namespace: namespace.as_deref(),
801                match_namespace: true,
802            },
803            _ => ExpandedName {
804                name: "",
805                namespace: None,
806                match_namespace: true,
807            },
808        }
809    }
810    /// Tests an element against a local-name selector or an exact expanded name.
811    ///
812    /// A bare string matches any namespace, mirroring roxmltree. Tuple forms
813    /// match the namespace exactly, including `(None, name)` for an
814    /// unqualified element.
815    pub fn has_tag_name<'n, N>(self, name: N) -> bool
816    where
817        N: Into<ExpandedName<'n>>,
818    {
819        if !self.is_element() {
820            return false;
821        }
822        let name = name.into();
823        self.tag_name().name() == name.name()
824            && (!name.match_namespace || self.tag_name().namespace() == name.namespace())
825    }
826    /// Returns the element's lexical namespace prefix.
827    pub fn prefix(self) -> Option<&'a str> {
828        match &self.data().kind {
829            NodeKind::Element { prefix, .. } => prefix.as_deref(),
830            _ => None,
831        }
832    }
833    /// Iterates non-namespace attributes in source order.
834    pub fn attributes(self) -> Attributes<'a, S> {
835        let values: &[AttributeData<S>] = match &self.data().kind {
836            NodeKind::Element { attributes, .. } => &attributes[..],
837            _ => &[],
838        };
839        Attributes {
840            values: values.iter(),
841        }
842    }
843    /// Looks up an unqualified attribute by local name.
844    pub fn attribute<'n, N>(self, name: N) -> Option<&'a str>
845    where
846        N: Into<ExpandedName<'n>>,
847    {
848        let name = name.into();
849        self.attributes()
850            .find(|attr| attr.name() == name.name() && attr.namespace() == name.namespace())
851            .map(Attribute::value)
852    }
853    /// Iterates in-scope namespace bindings.
854    pub fn namespaces(self) -> Namespaces<'a, S> {
855        let values: &[NamespaceData<S>] = match &self.data().kind {
856            NodeKind::Element { namespaces, .. } => &namespaces[..],
857            _ => &[],
858        };
859        Namespaces {
860            values: values.iter(),
861        }
862    }
863    /// Resolves a namespace prefix in this element's in-scope bindings.
864    pub fn lookup_namespace_uri(self, prefix: Option<&str>) -> Option<&'a str> {
865        if prefix == Some("xml") {
866            return Some(XML_NAMESPACE_URI);
867        }
868        self.namespaces()
869            .find(|ns| ns.name() == prefix)
870            .map(Namespace::uri)
871    }
872    /// Finds an in-scope prefix for a namespace URI.
873    pub fn lookup_prefix(self, uri: &str) -> Option<&'a str> {
874        if uri == XML_NAMESPACE_URI {
875            return Some("xml");
876        }
877        self.namespaces()
878            .filter(|namespace| namespace.uri() == uri)
879            .find_map(Namespace::name)
880    }
881    /// Returns direct character data, or the first direct text child of an element.
882    pub fn text(self) -> Option<&'a str> {
883        match &self.data().kind {
884            NodeKind::Text(value) | NodeKind::Comment(value) => Some(value),
885            NodeKind::Element { .. } => {
886                self.first_child()
887                    .and_then(|child| match &child.data().kind {
888                        NodeKind::Text(value) => Some(value.as_ref()),
889                        _ => None,
890                    })
891            }
892            NodeKind::PI { .. } => None,
893            NodeKind::Root => None,
894        }
895    }
896    /// Returns processing-instruction data for a PI node.
897    pub fn pi(self) -> Option<PI<'a>> {
898        match &self.data().kind {
899            NodeKind::PI { target, value } => Some(PI {
900                target,
901                value: value.as_deref(),
902            }),
903            _ => None,
904        }
905    }
906}
907
908/// Expanded XML name used by element and attribute comparisons.
909#[derive(Clone, Copy, Debug)]
910pub struct ExpandedName<'a> {
911    name: &'a str,
912    namespace: Option<&'a str>,
913    // Selector intent is separate from the represented expanded name so
914    // equality remains a comparison of XML names, not matching syntax.
915    match_namespace: bool,
916}
917impl<'a> ExpandedName<'a> {
918    /// Returns the local name.
919    pub fn name(self) -> &'a str {
920        self.name
921    }
922    /// Returns the namespace URI.
923    pub fn namespace(self) -> Option<&'a str> {
924        self.namespace
925    }
926}
927impl PartialEq for ExpandedName<'_> {
928    fn eq(&self, other: &Self) -> bool {
929        self.name == other.name && self.namespace == other.namespace
930    }
931}
932impl Eq for ExpandedName<'_> {}
933impl<'a> From<&'a str> for ExpandedName<'a> {
934    fn from(name: &'a str) -> Self {
935        Self {
936            name,
937            namespace: None,
938            match_namespace: false,
939        }
940    }
941}
942impl<'a> From<(Option<&'a str>, &'a str)> for ExpandedName<'a> {
943    fn from((namespace, name): (Option<&'a str>, &'a str)) -> Self {
944        Self {
945            name,
946            namespace,
947            match_namespace: true,
948        }
949    }
950}
951impl<'a> From<(&'a str, &'a str)> for ExpandedName<'a> {
952    fn from((namespace, name): (&'a str, &'a str)) -> Self {
953        Self {
954            name,
955            namespace: Some(namespace),
956            match_namespace: true,
957        }
958    }
959}
960
961/// Borrowed semantic XML attribute.
962pub struct Attribute<'a, S: Storage = Native> {
963    data: &'a AttributeData<S>,
964}
965impl<S: Storage> Copy for Attribute<'_, S> {}
966impl<S: Storage> Clone for Attribute<'_, S> {
967    fn clone(&self) -> Self {
968        *self
969    }
970}
971impl<'a, S: Storage> Attribute<'a, S> {
972    /// Report non-fatal xml:id errors without changing ID type assignment.
973    ///
974    /// xml:id 1.0 sections 4 and 6 require assignment even for invalid values;
975    /// applications may inspect this diagnostic before relying on an ID.
976    /// <https://www.w3.org/TR/2005/REC-xml-id-20050909/#processing>
977    pub fn xml_id_error(self) -> Option<XmlIdError> {
978        self.data.xml_id_error
979    }
980    /// Whether XML processing assigned the ID type (DTD or xml:id).
981    pub fn is_id(self) -> bool {
982        self.data.is_id
983    }
984    /// Returns the local name.
985    pub fn name(self) -> &'a str {
986        &self.data.name
987    }
988    /// Returns the namespace URI.
989    pub fn namespace(self) -> Option<&'a str> {
990        self.data.namespace.as_deref()
991    }
992    /// Returns the expanded value.
993    pub fn value(self) -> &'a str {
994        &self.data.value
995    }
996    /// Returns the lexical namespace prefix.
997    pub fn prefix(self) -> Option<&'a str> {
998        self.data.prefix.as_deref()
999    }
1000}
1001
1002/// Iterator over element attributes.
1003pub struct Attributes<'a, S: Storage = Native> {
1004    values: std::slice::Iter<'a, AttributeData<S>>,
1005}
1006impl<'a, S: Storage> Iterator for Attributes<'a, S> {
1007    type Item = Attribute<'a, S>;
1008    fn next(&mut self) -> Option<Self::Item> {
1009        self.values.next().map(|data| Attribute { data })
1010    }
1011    fn nth(&mut self, n: usize) -> Option<Self::Item> {
1012        self.values.nth(n).map(|data| Attribute { data })
1013    }
1014    fn size_hint(&self) -> (usize, Option<usize>) {
1015        self.values.size_hint()
1016    }
1017}
1018impl<S: Storage> ExactSizeIterator for Attributes<'_, S> {}
1019
1020/// Borrowed in-scope namespace binding.
1021pub struct Namespace<'a, S: Storage = Native> {
1022    data: &'a NamespaceData<S>,
1023}
1024impl<S: Storage> Copy for Namespace<'_, S> {}
1025impl<S: Storage> Clone for Namespace<'_, S> {
1026    fn clone(&self) -> Self {
1027        *self
1028    }
1029}
1030impl<'a, S: Storage> Namespace<'a, S> {
1031    /// Returns the prefix, or `None` for the default namespace.
1032    pub fn name(self) -> Option<&'a str> {
1033        self.data.prefix.as_deref()
1034    }
1035    /// Returns the namespace URI.
1036    pub fn uri(self) -> &'a str {
1037        &self.data.uri
1038    }
1039}
1040
1041/// Iterator over in-scope namespace bindings.
1042pub struct Namespaces<'a, S: Storage = Native> {
1043    values: std::slice::Iter<'a, NamespaceData<S>>,
1044}
1045impl<'a, S: Storage> Iterator for Namespaces<'a, S> {
1046    type Item = Namespace<'a, S>;
1047    fn next(&mut self) -> Option<Self::Item> {
1048        self.values.next().map(|data| Namespace { data })
1049    }
1050    fn nth(&mut self, n: usize) -> Option<Self::Item> {
1051        self.values.nth(n).map(|data| Namespace { data })
1052    }
1053    fn size_hint(&self) -> (usize, Option<usize>) {
1054        self.values.size_hint()
1055    }
1056}
1057impl<S: Storage> ExactSizeIterator for Namespaces<'_, S> {}
1058
1059/// Processing-instruction payload.
1060#[derive(Clone, Copy)]
1061pub struct PI<'a> {
1062    /// Processing-instruction target.
1063    pub target: &'a str,
1064    /// Optional processing-instruction data.
1065    pub value: Option<&'a str>,
1066}
1067
1068/// Iterator over direct child nodes.
1069pub struct Children<'a, 'input, S: Storage = Native> {
1070    document: &'a Document<'input, S>,
1071    ids: std::slice::Iter<'a, NodeId>,
1072}
1073impl<'a, 'input, S: Storage> Iterator for Children<'a, 'input, S> {
1074    type Item = Node<'a, 'input, S>;
1075    fn next(&mut self) -> Option<Self::Item> {
1076        self.ids.next().and_then(|id| self.document.get_node(*id))
1077    }
1078    fn size_hint(&self) -> (usize, Option<usize>) {
1079        self.ids.size_hint()
1080    }
1081}
1082impl<S: Storage> ExactSizeIterator for Children<'_, '_, S> {}
1083impl<'a, 'input, S: Storage> DoubleEndedIterator for Children<'a, 'input, S> {
1084    fn next_back(&mut self) -> Option<Self::Item> {
1085        self.ids
1086            .next_back()
1087            .and_then(|id| self.document.get_node(*id))
1088    }
1089}
1090
1091/// Pre-order iterator over a node and its descendants.
1092pub struct Descendants<'a, 'input, S: Storage = Native> {
1093    document: &'a Document<'input, S>,
1094    ids: Range<u32>,
1095}
1096impl<'a, 'input, S: Storage> Iterator for Descendants<'a, 'input, S> {
1097    type Item = Node<'a, 'input, S>;
1098    fn next(&mut self) -> Option<Self::Item> {
1099        self.ids
1100            .next()
1101            .and_then(|id| self.document.get_node(NodeId(id)))
1102    }
1103    fn size_hint(&self) -> (usize, Option<usize>) {
1104        self.ids.size_hint()
1105    }
1106}
1107impl<S: Storage> DoubleEndedIterator for Descendants<'_, '_, S> {
1108    fn next_back(&mut self) -> Option<Self::Item> {
1109        self.ids
1110            .next_back()
1111            .and_then(|id| self.document.get_node(NodeId(id)))
1112    }
1113}
1114impl<S: Storage> ExactSizeIterator for Descendants<'_, '_, S> {}
1115
1116/// Iterator over a node and its ancestors.
1117pub struct Ancestors<'a, 'input, S: Storage = Native> {
1118    next: Option<Node<'a, 'input, S>>,
1119}
1120impl<'a, 'input, S: Storage> Iterator for Ancestors<'a, 'input, S> {
1121    type Item = Node<'a, 'input, S>;
1122    fn next(&mut self) -> Option<Self::Item> {
1123        let current = self.next?;
1124        self.next = current.parent();
1125        Some(current)
1126    }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    #[cfg(feature = "xmldsig")]
1132    fn assert_scoped_projection<S: super::Storage>(document: &super::Document<'_, S>) {
1133        let root = document.root_element();
1134        assert_eq!(root.tag_name().name(), "r");
1135        assert_eq!(root.attribute(("urn:p", "x")), Some("value"));
1136        assert_eq!(root.lookup_namespace_uri(Some("p")), Some("urn:p"));
1137        let id = root
1138            .attributes()
1139            .find(|attribute| attribute.name() == "id")
1140            .expect("xml:id retained");
1141        assert!(id.is_id());
1142        assert_eq!(id.value(), "a b");
1143        assert_eq!(id.xml_id_error(), Some(super::XmlIdError::InvalidNcName));
1144        assert_eq!(root.children().len(), 1);
1145        assert_eq!(root.text(), Some("leftmiddleright"));
1146    }
1147
1148    #[cfg(all(feature = "xmldsig", feature = "xml-backend-roxmltree"))]
1149    #[test]
1150    fn roxmltree_projection_uses_the_actual_scoped_tree_storage() {
1151        // The source parser is deliberately outside this component test's
1152        // account. Every *semantic projection* allocation must be retained by
1153        // the supplied execution without changing IDs, namespaces or text.
1154        use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1155        let xml = "<r xmlns:p='urn:p' xml:id=' a  b ' p:x='value'>left<![CDATA[middle]]>right</r>";
1156        let preflight =
1157            super::super::LexicalPreflight::scan(xml, false).expect("fixed XML preflight");
1158        let raw = crate::parser_roxmltree::Document::parse(xml).expect("fixed backend XML");
1159        let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1160        let document = super::super::roxmltree::project_in(
1161            xml,
1162            &raw,
1163            &preflight,
1164            super::super::storage::Accounted(&memory),
1165        )
1166        .expect("paid projection fits");
1167        assert_scoped_projection(&document);
1168        assert!(memory.used() > 0);
1169        drop(document);
1170        assert_eq!(memory.used(), 0);
1171        let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
1172        assert!(matches!(
1173            super::super::roxmltree::project_in(
1174                xml,
1175                &raw,
1176                &preflight,
1177                super::super::storage::Accounted(&denied)
1178            ),
1179            Err(super::ParseError::SharedExecution(
1180                MemoryError::LimitExceeded
1181            ))
1182        ));
1183        assert_eq!(denied.used(), 0);
1184    }
1185
1186    #[cfg(all(feature = "xmldsig", feature = "xml-backend-roxmltree"))]
1187    #[test]
1188    fn scoped_parser_tree_and_canonicalizer_share_the_same_semantic_kernel() {
1189        // Switching both parser and semantic-tree storage must not require a
1190        // second serializer or alter canonical bytes, axes, IDs or comments.
1191        use crate::c14n::{C14nAlgorithm, C14nMode};
1192        use crate::xmldsig::types::{
1193            NodeSetMaterializationBudget,
1194            projection::{PaidKeys, Projection},
1195        };
1196        use structured_asn1::memory::{Memory, MemoryAccount};
1197        let source = "<r xmlns:p='urn:p' xml:id=' a  b ' p:x='value'>left<![CDATA[middle]]><!--c--><p:leaf/>right</r>";
1198        let expected = super::Document::parse(source).expect("native semantic baseline");
1199        let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
1200        let storage = super::super::storage::Accounted(&memory);
1201        let preflight = super::super::LexicalPreflight::scan_in(source, false, storage)
1202            .expect("funded lexical preflight");
1203        let document = super::super::roxmltree::RoxmltreeBackend::parse_in(
1204            source,
1205            Default::default(),
1206            &preflight,
1207            storage,
1208        )
1209        .expect("funded roxmltree parse");
1210        let nodes = Projection::subtree(
1211            document.root(),
1212            true,
1213            &NodeSetMaterializationBudget::default(),
1214            PaidKeys::new(&memory),
1215        )
1216        .expect("funded node projection");
1217        for mode in [
1218            C14nMode::Inclusive1_0,
1219            C14nMode::Inclusive1_1,
1220            C14nMode::Exclusive1_0,
1221        ] {
1222            for comments in [false, true] {
1223                let algorithm = C14nAlgorithm::new(mode, comments);
1224                let mut canonical = Vec::new();
1225                crate::c14n::canonicalize(&expected, None, &algorithm, &mut canonical)
1226                    .expect("native canonical baseline");
1227                let (actual, _) = crate::c14n::canonicalize_buffer_in(
1228                    &document,
1229                    &nodes,
1230                    (&algorithm, None),
1231                    None,
1232                    1 << 20,
1233                    &Default::default(),
1234                    &crate::c14n::scratch::Accounted(&memory),
1235                )
1236                .expect("funded canonicalization");
1237                assert_eq!(&*actual, &canonical);
1238            }
1239        }
1240        assert!(memory.used() > 0);
1241        drop(nodes);
1242        drop(document);
1243        drop(preflight);
1244        assert_eq!(memory.used(), 0);
1245    }
1246
1247    #[cfg(all(feature = "xmldsig", feature = "xml-backend-xmloxide"))]
1248    #[test]
1249    fn xmloxide_projection_uses_the_actual_scoped_tree_storage() {
1250        // The same tree kernel must retain paid coalescing, normalized xml:id,
1251        // namespace axes and attributes with the alternate selected backend.
1252        use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1253        let xml = "<r xmlns:p='urn:p' xml:id=' a  b ' p:x='value'>left<![CDATA[middle]]>right</r>";
1254        let preflight =
1255            super::super::LexicalPreflight::scan(xml, false).expect("fixed XML preflight");
1256        let raw = crate::parser_xmloxide::parser::parse_str_with_options(
1257            xml,
1258            &super::super::xmloxide::backend_options(),
1259        )
1260        .expect("fixed backend XML");
1261        let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1262        let document = super::super::xmloxide::project_in(
1263            xml,
1264            &raw,
1265            super::ParsingOptions::default(),
1266            &preflight,
1267            super::super::storage::Accounted(&memory),
1268        )
1269        .expect("paid projection fits");
1270        assert_scoped_projection(&document);
1271        assert!(memory.used() > 0);
1272        drop(document);
1273        assert_eq!(memory.used(), 0);
1274        let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
1275        assert!(matches!(
1276            super::super::xmloxide::project_in(
1277                xml,
1278                &raw,
1279                super::ParsingOptions::default(),
1280                &preflight,
1281                super::super::storage::Accounted(&denied)
1282            ),
1283            Err(super::ParseError::SharedExecution(
1284                MemoryError::LimitExceeded
1285            ))
1286        ));
1287        assert_eq!(denied.used(), 0);
1288    }
1289
1290    #[cfg(all(feature = "xmldsig", feature = "xml-backend-xmloxide"))]
1291    #[test]
1292    fn projection_node_admission_precedes_arena_and_payload_growth() {
1293        // A disallowed semantic node must not allocate its attributes or text
1294        // first, nor reserve a larger arena based on the source backend count.
1295        use super::super::storage::Accounted;
1296        use structured_asn1::memory::{Memory, MemoryAccount};
1297        let source = format!("<r a='{}'/>", "x".repeat(65536));
1298        let preflight =
1299            super::super::LexicalPreflight::scan(&source, false).expect("native lexical baseline");
1300        let raw = crate::parser_xmloxide::parser::parse_str_with_options(
1301            &source,
1302            &super::super::xmloxide::backend_options(),
1303        )
1304        .expect("native raw XML baseline");
1305        for (limit, capacity) in [(0, 0), (1, 4096)] {
1306            let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(capacity, 1 << 24)));
1307            let result = super::super::xmloxide::project_in(
1308                &source,
1309                &raw,
1310                super::ParsingOptions {
1311                    nodes_limit: limit,
1312                    allow_dtd: false,
1313                },
1314                &preflight,
1315                Accounted(&memory),
1316            );
1317            assert!(matches!(result, Err(super::ParseError::NodesLimitReached)));
1318            assert_eq!(memory.used(), 0);
1319        }
1320    }
1321
1322    use super::Document;
1323    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1324    use super::{NamespaceData, comparison::namespaces_equal};
1325    use crate::xml::dom::{ParseError, ParsingOptions, XmlBackend};
1326
1327    #[test]
1328    fn runtime_selection_never_falls_back_to_a_compiled_backend() {
1329        // A stable selector can be supplied by application configuration even
1330        // in a thin build. Missing implementations must fail explicitly.
1331        for backend in [
1332            XmlBackend::Xmloxide,
1333            XmlBackend::Roxmltree,
1334            XmlBackend::Differential,
1335        ] {
1336            let result = Document::parse_with_backend("<r/>", backend);
1337            if backend.is_available() {
1338                assert!(result.is_ok(), "{backend:?} should be available");
1339            } else {
1340                assert_eq!(
1341                    result.expect_err("an unavailable backend must not fall back"),
1342                    ParseError::BackendUnavailable { backend }
1343                );
1344            }
1345        }
1346    }
1347
1348    #[test]
1349    fn every_backend_enforces_the_same_active_namespace_ceiling() {
1350        // Backend selection must not change whether an adversarial namespace
1351        // stack is accepted. The 1024th binding is valid; the 1025th is not.
1352        let declarations = (0..1_023)
1353            .map(|index| format!(r#" xmlns:n{index}="urn:namespace:{index}""#))
1354            .collect::<String>();
1355        let accepted = format!(r#"<root{declarations}><child xmlns:p="urn:p"/></root>"#);
1356        let rejected = format!(
1357            r#"<root{declarations} xmlns:overflow="urn:overflow"><child xmlns:p="urn:p"/></root>"#
1358        );
1359
1360        for backend in XmlBackend::available() {
1361            Document::parse_with_backend(&accepted, backend)
1362                .unwrap_or_else(|error| panic!("{backend:?} rejected the ceiling: {error}"));
1363            assert_eq!(
1364                Document::parse_with_backend(&rejected, backend)
1365                    .expect_err("the 1025th active binding must be rejected"),
1366                ParseError::NamespaceBindingLimitReached {
1367                    maximum: 1_024,
1368                    actual: 1_025,
1369                },
1370                "{backend:?} must share the lexical namespace contract"
1371            );
1372        }
1373    }
1374
1375    #[cfg(feature = "xml-backend-differential")]
1376    #[test]
1377    fn compatibility_feature_selects_differential_by_default() {
1378        assert_eq!(XmlBackend::default(), XmlBackend::Differential);
1379    }
1380
1381    #[cfg(all(
1382        feature = "xml-backend-xmloxide",
1383        feature = "xml-backend-roxmltree",
1384        not(feature = "xml-backend-differential")
1385    ))]
1386    #[test]
1387    fn fat_build_defaults_to_xmloxide_without_implicit_double_parsing() {
1388        assert_eq!(XmlBackend::default(), XmlBackend::Xmloxide);
1389    }
1390
1391    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1392    #[test]
1393    fn fat_build_selects_each_semantically_equivalent_runtime_mode() {
1394        // Runtime selection changes only arena construction; downstream XML
1395        // Security semantics and stable source ranges remain identical.
1396        let xml = r#"<r xmlns:p="urn:p"><p:a x="1">value</p:a></r>"#;
1397        let snapshot = |backend| {
1398            let document = Document::parse_with_backend(xml, backend)
1399                .expect("every compiled runtime mode must parse the fixture");
1400            document
1401                .descendants()
1402                .map(|node| {
1403                    (
1404                        node.node_type(),
1405                        node.range(),
1406                        node.text().map(str::to_owned),
1407                    )
1408                })
1409                .collect::<Vec<_>>()
1410        };
1411
1412        let xmloxide = snapshot(XmlBackend::Xmloxide);
1413        assert_eq!(snapshot(XmlBackend::Roxmltree), xmloxide);
1414        assert_eq!(snapshot(XmlBackend::Differential), xmloxide);
1415    }
1416
1417    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1418    #[test]
1419    fn differential_namespace_axis_comparison_preserves_multiplicity() {
1420        // The fail-closed gate must reject unequal axes even if a malformed
1421        // backend projection repeats one binding and hides another.
1422        let binding = |prefix: &str, uri: &str| NamespaceData::<super::Native> {
1423            prefix: Some(prefix.to_owned()),
1424            uri: uri.to_owned(),
1425        };
1426        let a = binding("a", "urn:a");
1427        let b = binding("b", "urn:b");
1428
1429        assert!(
1430            !namespaces_equal(
1431                &[a.clone(), a.clone()],
1432                &[a.clone(), b.clone()],
1433                super::Native,
1434            )
1435            .expect("duplicate axis comparison")
1436        );
1437        assert!(
1438            !namespaces_equal(&[a.clone(), b], &[a.clone(), a.clone()], super::Native,)
1439                .expect("reversed duplicate axis comparison")
1440        );
1441        assert!(
1442            namespaces_equal(&[a.clone(), a.clone()], &[a.clone(), a], super::Native,)
1443                .expect("equal multiplicities")
1444        );
1445    }
1446
1447    #[test]
1448    fn selected_backend_preserves_element_source_ranges() {
1449        // Mutation must splice exact element ranges under either parser.
1450        let xml = "<?p before?><root><empty/><child>value</child></root><!--after-->";
1451        let document = Document::parse(xml).expect("fixture must parse");
1452        let root = document.root_element();
1453        let child = root
1454            .children()
1455            .find(|node| node.has_tag_name("child"))
1456            .expect("fixture must contain the child element");
1457        assert_eq!(
1458            &xml[root.range()],
1459            "<root><empty/><child>value</child></root>"
1460        );
1461        assert_eq!(&xml[child.range()], "<child>value</child>");
1462    }
1463
1464    #[test]
1465    fn selected_backend_materializes_shadowed_namespace_axis() {
1466        // C14N observes the complete in-scope axis, including undeclarations.
1467        let document = Document::parse(
1468            r#"<r xmlns="urn:outer" xmlns:p="urn:p1"><p:a xmlns:p="urn:p2" xmlns=""><p:b/></p:a></r>"#,
1469        ).expect("fixture must parse");
1470        let child = document
1471            .descendants()
1472            .find(|node| node.has_tag_name(("urn:p2", "b")))
1473            .expect("fixture must contain the namespaced descendant");
1474        assert_eq!(child.lookup_namespace_uri(Some("p")), Some("urn:p2"));
1475        assert_eq!(child.lookup_namespace_uri(None), Some(""));
1476        assert_eq!(child.namespaces().count(), 2);
1477    }
1478
1479    #[test]
1480    fn tag_name_matcher_distinguishes_local_from_unqualified_names() {
1481        // A bare string mirrors roxmltree's local-name selector, while an
1482        // explicit optional namespace represents exact expanded-name intent.
1483        let document = Document::parse(r#"<r xmlns:p="urn:p"><item/><p:item/></r>"#)
1484            .expect("namespace matching fixture must parse");
1485        let mut items = document
1486            .root_element()
1487            .children()
1488            .filter(|node| node.is_element());
1489        let unqualified = items.next().expect("unqualified item must exist");
1490        let namespaced = items.next().expect("namespaced item must exist");
1491
1492        assert!(unqualified.has_tag_name("item"));
1493        assert!(namespaced.has_tag_name("item"));
1494        assert!(unqualified.has_tag_name((None, "item")));
1495        assert!(!namespaced.has_tag_name((None, "item")));
1496        assert!(namespaced.has_tag_name((Some("urn:p"), "item")));
1497    }
1498
1499    #[test]
1500    fn namespace_lookup_includes_the_predefined_xml_binding() {
1501        // Namespaces in XML binds `xml` on every element without requiring a
1502        // lexical declaration in the source document.
1503        let document = Document::parse("<root><child xml:lang=\"en\"/></root>")
1504            .expect("predefined namespace fixture must parse");
1505        let child = document
1506            .root_element()
1507            .first_element_child()
1508            .expect("fixture must contain a child");
1509
1510        assert_eq!(
1511            child.lookup_namespace_uri(Some("xml")),
1512            Some("http://www.w3.org/XML/1998/namespace")
1513        );
1514        assert_eq!(
1515            child.lookup_prefix("http://www.w3.org/XML/1998/namespace"),
1516            Some("xml")
1517        );
1518    }
1519
1520    #[test]
1521    fn selected_backend_folds_cdata_and_entities_into_one_text_node() {
1522        // XMLDSig defines character data after entity and CDATA expansion.
1523        let document = Document::parse_with_options(
1524            "<!DOCTYPE r [<!ENTITY value 'two'>]><r>one<![CDATA[+]]>&value;three</r>",
1525            ParsingOptions {
1526                allow_dtd: true,
1527                nodes_limit: 8,
1528            },
1529        )
1530        .expect("bounded internal entity fixture must parse");
1531        let text = document
1532            .root_element()
1533            .children()
1534            .filter(|node| node.is_text())
1535            .collect::<Vec<_>>();
1536        assert_eq!(text.len(), 1);
1537        assert_eq!(text[0].text(), Some("one+twothree"));
1538    }
1539
1540    #[test]
1541    fn selected_backend_maps_builtin_references_into_the_surrounding_text_range() {
1542        // xmloxide expands predefined references into its adjacent text node;
1543        // the lexical sidecar must mirror that boundary rather than invent a
1544        // second semantic text node or lose the original source range.
1545        let xml = "<r>left&amp;right</r>";
1546        let document = Document::parse(xml).expect("fixture must parse");
1547        let text = document
1548            .root_element()
1549            .first_child()
1550            .expect("text child must exist");
1551
1552        assert_eq!(text.text(), Some("left&right"));
1553        assert_eq!(&xml[text.range()], "left&amp;right");
1554    }
1555
1556    #[test]
1557    fn selected_backend_rejects_dtd_before_building_a_tree() {
1558        // The parser option must fail closed before backend-specific handling.
1559        assert_eq!(
1560            Document::parse("<!DOCTYPE r><r/>")
1561                .expect_err("DTD-disabled parsing must reject a document type"),
1562            ParseError::DtdDetected,
1563        );
1564    }
1565
1566    #[test]
1567    fn selected_backend_enforces_semantic_node_limit_after_text_folding() {
1568        // The shared ceiling counts retained nodes rather than lexical events.
1569        let error = Document::parse_with_options(
1570            "<r><a/><b/></r>",
1571            ParsingOptions {
1572                allow_dtd: false,
1573                nodes_limit: 3,
1574            },
1575        )
1576        .expect_err("the semantic node limit must reject the fourth node");
1577        assert_eq!(error, ParseError::NodesLimitReached);
1578    }
1579
1580    #[test]
1581    fn selected_backend_enforces_the_absolute_node_ceiling_before_dom_allocation() {
1582        // Direct DOM callers may request an unbounded parser, but the crate's
1583        // process-safety ceiling must still stop a compact wide document before
1584        // either backend allocates its complete native tree.
1585        let count = crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize;
1586        let xml = format!("<r>{}</r>", "<n/>".repeat(count));
1587
1588        assert_eq!(
1589            Document::parse_with_options(
1590                &xml,
1591                ParsingOptions {
1592                    allow_dtd: false,
1593                    nodes_limit: u32::MAX,
1594                },
1595            )
1596            .expect_err("the absolute retained-node ceiling must remain effective"),
1597            ParseError::NodesLimitReached,
1598        );
1599    }
1600
1601    #[test]
1602    fn selected_backend_enforces_the_absolute_byte_ceiling_before_dom_allocation() {
1603        // Direct semantic-DOM callers have no policy object that can bound the
1604        // source, so the crate ceiling must run before either native backend.
1605        let maximum = crate::hard_limits::XML_DOCUMENT_BYTE_CEILING;
1606        let xml = format!("<r>{}</r>", "x".repeat(maximum));
1607        let expected = ParseError::ByteLimitReached {
1608            maximum,
1609            actual: xml.len(),
1610        };
1611
1612        assert_eq!(
1613            Document::parse(&xml)
1614                .expect_err("default direct parsing must enforce the byte ceiling"),
1615            expected,
1616        );
1617        assert_eq!(
1618            Document::parse_with_options(
1619                &xml,
1620                ParsingOptions {
1621                    allow_dtd: false,
1622                    nodes_limit: u32::MAX,
1623                },
1624            )
1625            .expect_err("custom direct parsing must retain the absolute byte ceiling"),
1626            expected,
1627        );
1628    }
1629
1630    #[test]
1631    fn selected_backend_bounds_direct_entity_expansion_count() {
1632        // A shallow sequence of references bypasses roxmltree's nested-entity
1633        // loop detector. Direct DOM parsing still needs a finite aggregate cap.
1634        let references =
1635            "&value;".repeat(crate::hard_limits::XML_ENTITY_EXPANSION_CEILING as usize + 1);
1636        let xml = format!("<!DOCTYPE r [<!ENTITY value 'x'>]><r>{references}</r>");
1637
1638        assert!(matches!(
1639            Document::parse_with_options(
1640                &xml,
1641                ParsingOptions {
1642                    allow_dtd: true,
1643                    nodes_limit: u32::MAX,
1644                },
1645            ),
1646            Err(ParseError::EntityExpansionLimitReached { maximum, actual })
1647                if maximum == crate::hard_limits::XML_ENTITY_EXPANSION_CEILING
1648                    && actual == maximum + 1
1649        ));
1650    }
1651
1652    #[test]
1653    fn selected_backend_bounds_source_positions_after_semantic_text_folding() {
1654        // Many adjacent CDATA tokens retain one semantic text node, but the
1655        // xmloxide range adapter still needs one source position per token.
1656        // Bound that sidecar independently before constructing a backend DOM.
1657        let segments = crate::hard_limits::XML_SOURCE_POSITION_CEILING + 1;
1658        let xml = format!("<r>{}</r>", "<![CDATA[x]]>".repeat(segments));
1659
1660        assert!(matches!(
1661            Document::parse_with_options(
1662                &xml,
1663                ParsingOptions {
1664                    allow_dtd: false,
1665                    nodes_limit: 3,
1666                },
1667            ),
1668            Err(ParseError::SourcePositionLimitReached { maximum, actual })
1669                if maximum == crate::hard_limits::XML_SOURCE_POSITION_CEILING
1670                    && actual == maximum + 1
1671        ));
1672    }
1673
1674    #[test]
1675    fn unqualified_attribute_lookup_requires_no_namespace() {
1676        // Schema attributes such as Algorithm and URI are unqualified. A
1677        // same-local-name extension attribute must never satisfy that lookup.
1678        let document = Document::parse(
1679            r#"<r xmlns:evil="urn:evil" evil:Algorithm="extension" Algorithm="schema"/>"#,
1680        )
1681        .expect("fixture must parse");
1682        let root = document.root_element();
1683
1684        assert_eq!(root.attribute("Algorithm"), Some("schema"));
1685        assert_eq!(root.attribute(("urn:evil", "Algorithm")), Some("extension"));
1686
1687        let extension_only =
1688            Document::parse(r#"<r xmlns:evil="urn:evil" evil:Algorithm="extension"/>"#)
1689                .expect("fixture must parse");
1690        assert_eq!(extension_only.root_element().attribute("Algorithm"), None);
1691    }
1692
1693    #[test]
1694    fn node_text_preserves_character_data_contract() {
1695        // PI data is not character data, and element text follows the first
1696        // direct child contract rather than searching later descendants.
1697        let document = Document::parse("<r><?p hidden?>visible</r>").expect("fixture must parse");
1698        let root = document.root_element();
1699        let pi = root.first_child().expect("PI must exist");
1700
1701        assert_eq!(pi.text(), None);
1702        assert_eq!(pi.pi().and_then(|value| value.value), Some("hidden"));
1703        assert_eq!(root.text(), None);
1704        assert_eq!(
1705            pi.next_sibling().and_then(|node| node.text()),
1706            Some("visible")
1707        );
1708
1709        let nested =
1710            Document::parse("<r><child>nested</child>later</r>").expect("fixture must parse");
1711        assert_eq!(nested.root_element().text(), None);
1712    }
1713
1714    #[test]
1715    fn selected_backend_rejects_deep_documents_before_dom_parsing() {
1716        // Direct DOM callers do not run operation-policy preflight. The common
1717        // lexical pass must enforce the absolute ceiling before either DOM.
1718        const DEPTH: usize = 20_000;
1719        let mut xml = String::with_capacity(DEPTH * 7);
1720        xml.push_str(&"<n>".repeat(DEPTH));
1721        xml.push_str(&"</n>".repeat(DEPTH));
1722
1723        assert!(matches!(
1724            Document::parse(&xml),
1725            Err(ParseError::DepthLimitReached { maximum, actual })
1726                if maximum == crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING
1727                    && actual == maximum + 1
1728        ));
1729    }
1730
1731    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1732    #[test]
1733    fn differential_comparison_fails_closed_on_semantic_divergence() {
1734        // Differential mode must reject adapter disagreement rather than
1735        // silently selecting one parser's interpretation of attacker input.
1736        let left = Document::parse("<r><a/></r>").expect("fixture must parse");
1737        let mut right = Document::parse("<r><a/></r>").expect("fixture must parse");
1738        right.nodes[2].range.end -= 1;
1739
1740        assert!(matches!(
1741            left.ensure_semantically_equivalent(&right, None),
1742            Err(ParseError::BackendDivergence { .. })
1743        ));
1744    }
1745
1746    #[test]
1747    fn dtd_internal_comments_are_not_document_nodes() {
1748        // A `]` or `>` in an internal-subset comment must not terminate DTD
1749        // range detection and leak that comment into the semantic document.
1750        let document = Document::parse_with_options(
1751            "<!--before--><!DOCTYPE r [<!-- ] > --><!ENTITY value 'ok'>]><r>&value;</r><!--after-->",
1752            ParsingOptions {
1753                allow_dtd: true,
1754                ..ParsingOptions::default()
1755            },
1756        )
1757        .expect("DTD fixture must parse");
1758        let root = document.root();
1759        let comments = root
1760            .descendants()
1761            .filter(|node| node.is_comment())
1762            .filter_map(|node| node.text())
1763            .collect::<Vec<_>>();
1764
1765        assert_eq!(comments, ["before", "after"]);
1766        assert_eq!(
1767            root.descendants()
1768                .find(|node| node.is_text())
1769                .and_then(|node| node.text()),
1770            Some("ok")
1771        );
1772    }
1773
1774    #[test]
1775    fn dtd_range_ignores_doctype_text_in_prolog_nodes() {
1776        // DOCTYPE-like text in ordinary prolog nodes must not redirect DTD
1777        // filtering away from the actual declaration and leak subset nodes.
1778        let document = Document::parse_with_options(
1779            "<!-- <!DOCTYPE fake> --><?probe <!DOCTYPE fake> ?><!DOCTYPE r [<!--hidden-->]><r/>",
1780            ParsingOptions {
1781                allow_dtd: true,
1782                ..ParsingOptions::default()
1783            },
1784        )
1785        .expect("DTD fixture must parse");
1786        let root = document.root();
1787        let comments = root
1788            .children()
1789            .filter(|node| node.is_comment())
1790            .filter_map(|node| node.text())
1791            .collect::<Vec<_>>();
1792        let processing_instructions = root
1793            .children()
1794            .filter(|node| node.is_pi())
1795            .filter_map(|node| node.pi())
1796            .collect::<Vec<_>>();
1797
1798        assert_eq!(comments, [" <!DOCTYPE fake> "]);
1799        assert_eq!(processing_instructions.len(), 1);
1800        assert_eq!(processing_instructions[0].target, "probe");
1801    }
1802
1803    #[cfg(feature = "xml-backend-roxmltree")]
1804    #[test]
1805    fn dtd_range_scanning_accepts_unicode_names() {
1806        // Scanner offsets are bytes; a multibyte XML name must not become an
1807        // invalid UTF-8 slicing boundary while locating the DTD terminator.
1808        let input = "<!DOCTYPE r [<!ENTITY café 'ok'>]><r/>";
1809        let end = input.find("><r/>").expect("fixture has document root") + 1;
1810
1811        let preflight = super::super::LexicalPreflight::scan(input, true)
1812            .expect("Unicode DTD fixture must pass lexical preflight");
1813        assert_eq!(preflight.doctype_range(), Some(&(0..end)));
1814    }
1815}