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