Skip to main content

xml_sec/xml/dom/
tree.rs

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