1#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum NodeType {
39 Root,
41 Element,
43 Text,
45 Comment,
47 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum XmlIdError {
92 InvalidNcName,
94 InvalidDeclaredType,
96 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
106pub(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 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 normalize_id_value(&mut attribute.value);
228 if xml_id {
229 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#[derive(Debug)]
276pub struct Document<'input> {
277 input: &'input str,
278 nodes: Vec<NodeData>,
279}
280
281impl<'input> Document<'input> {
282 pub fn parse(input: &'input str) -> Result<Self, ParseError> {
284 Self::parse_with_options(input, ParsingOptions::default())
285 }
286
287 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 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 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 pub fn input_text(&self) -> &'input str {
321 self.input
322 }
323
324 pub fn root(&self) -> Node<'_, 'input> {
326 self.get_node(NodeId(0))
327 .expect("parsed document has a root")
328 }
329
330 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 pub fn descendants(&self) -> Descendants<'_, 'input> {
340 self.root().descendants()
341 }
342
343 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 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 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#[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 pub fn id(self) -> NodeId {
596 self.id
597 }
598 pub fn document(self) -> &'a Document<'input> {
600 self.document
601 }
602 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 pub fn is_root(&self) -> bool {
614 self.node_type() == NodeType::Root
615 }
616 pub fn is_element(&self) -> bool {
618 self.node_type() == NodeType::Element
619 }
620 pub fn is_text(&self) -> bool {
622 self.node_type() == NodeType::Text
623 }
624 pub fn is_comment(&self) -> bool {
626 self.node_type() == NodeType::Comment
627 }
628 pub fn is_pi(&self) -> bool {
630 self.node_type() == NodeType::PI
631 }
632 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 pub fn parent(self) -> Option<Self> {
641 self.data().parent.and_then(|id| self.document.get_node(id))
642 }
643 pub fn parent_element(self) -> Option<Self> {
645 self.parent()
646 .and_then(|node| node.is_element().then_some(node))
647 }
648 pub fn children(self) -> Children<'a, 'input> {
650 Children {
651 document: self.document,
652 ids: self.data().children.iter(),
653 }
654 }
655 pub fn first_child(self) -> Option<Self> {
657 self.children().next()
658 }
659 pub fn first_element_child(self) -> Option<Self> {
661 self.children().find(Node::is_element)
662 }
663 pub fn last_element_child(self) -> Option<Self> {
665 self.children().rev().find(Node::is_element)
666 }
667 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 pub fn ancestors(self) -> Ancestors<'a, 'input> {
676 Ancestors { next: Some(self) }
677 }
678 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 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 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 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 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 pub fn prefix(self) -> Option<&'a str> {
750 match &self.data().kind {
751 NodeKind::Element { prefix, .. } => prefix.as_deref(),
752 _ => None,
753 }
754 }
755 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 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 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 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 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 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 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#[derive(Clone, Copy, Debug)]
832pub struct ExpandedName<'a> {
833 name: &'a str,
834 namespace: Option<&'a str>,
835 match_namespace: bool,
838}
839impl<'a> ExpandedName<'a> {
840 pub fn name(self) -> &'a str {
842 self.name
843 }
844 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#[derive(Clone, Copy)]
885pub struct Attribute<'a> {
886 data: &'a AttributeData,
887}
888impl<'a> Attribute<'a> {
889 pub fn xml_id_error(self) -> Option<XmlIdError> {
895 self.data.xml_id_error
896 }
897 pub fn is_id(self) -> bool {
899 self.data.is_id
900 }
901 pub fn name(self) -> &'a str {
903 &self.data.name
904 }
905 pub fn namespace(self) -> Option<&'a str> {
907 self.data.namespace.as_deref()
908 }
909 pub fn value(self) -> &'a str {
911 &self.data.value
912 }
913 pub fn prefix(self) -> Option<&'a str> {
915 self.data.prefix.as_deref()
916 }
917}
918
919pub 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#[derive(Clone, Copy)]
936pub struct Namespace<'a> {
937 data: &'a NamespaceData,
938}
939impl<'a> Namespace<'a> {
940 pub fn name(self) -> Option<&'a str> {
942 self.data.prefix.as_deref()
943 }
944 pub fn uri(self) -> &'a str {
946 &self.data.uri
947 }
948}
949
950pub 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#[derive(Clone, Copy)]
967pub struct PI<'a> {
968 pub target: &'a str,
970 pub value: Option<&'a str>,
972}
973
974pub 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
997pub 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
1022pub 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 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 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 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 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 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 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 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 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 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 let xml = "<r>left&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&right");
1265 }
1266
1267 #[test]
1268 fn selected_backend_rejects_dtd_before_building_a_tree() {
1269 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 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 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 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 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 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 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 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 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 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 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 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 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}