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 pub fn next_sibling_element(self) -> Option<Self> {
771 let mut current = self.next_sibling();
772 while let Some(node) = current {
773 if node.is_element() {
774 return Some(node);
775 }
776 current = node.next_sibling();
777 }
778 None
779 }
780 pub fn tag_name(self) -> ExpandedName<'a> {
782 match &self.data().kind {
783 NodeKind::Element {
784 name, namespace, ..
785 } => ExpandedName {
786 name,
787 namespace: namespace.as_deref(),
788 match_namespace: true,
789 },
790 _ => ExpandedName {
791 name: "",
792 namespace: None,
793 match_namespace: true,
794 },
795 }
796 }
797 pub fn has_tag_name<'n, N>(self, name: N) -> bool
803 where
804 N: Into<ExpandedName<'n>>,
805 {
806 if !self.is_element() {
807 return false;
808 }
809 let name = name.into();
810 self.tag_name().name() == name.name()
811 && (!name.match_namespace || self.tag_name().namespace() == name.namespace())
812 }
813 pub fn prefix(self) -> Option<&'a str> {
815 match &self.data().kind {
816 NodeKind::Element { prefix, .. } => prefix.as_deref(),
817 _ => None,
818 }
819 }
820 pub fn attributes(self) -> Attributes<'a, S> {
822 let values: &[AttributeData<S>] = match &self.data().kind {
823 NodeKind::Element { attributes, .. } => &attributes[..],
824 _ => &[],
825 };
826 Attributes {
827 values: values.iter(),
828 }
829 }
830 pub fn attribute<'n, N>(self, name: N) -> Option<&'a str>
832 where
833 N: Into<ExpandedName<'n>>,
834 {
835 let name = name.into();
836 self.attributes()
837 .find(|attr| attr.name() == name.name() && attr.namespace() == name.namespace())
838 .map(Attribute::value)
839 }
840 pub fn namespaces(self) -> Namespaces<'a, S> {
842 let values: &[NamespaceData<S>] = match &self.data().kind {
843 NodeKind::Element { namespaces, .. } => &namespaces[..],
844 _ => &[],
845 };
846 Namespaces {
847 values: values.iter(),
848 }
849 }
850 pub fn lookup_namespace_uri(self, prefix: Option<&str>) -> Option<&'a str> {
852 if prefix == Some("xml") {
853 return Some(XML_NAMESPACE_URI);
854 }
855 self.namespaces()
856 .find(|ns| ns.name() == prefix)
857 .map(Namespace::uri)
858 }
859 pub fn lookup_prefix(self, uri: &str) -> Option<&'a str> {
861 if uri == XML_NAMESPACE_URI {
862 return Some("xml");
863 }
864 self.namespaces()
865 .filter(|namespace| namespace.uri() == uri)
866 .find_map(Namespace::name)
867 }
868 pub fn text(self) -> Option<&'a str> {
870 match &self.data().kind {
871 NodeKind::Text(value) | NodeKind::Comment(value) => Some(value),
872 NodeKind::Element { .. } => {
873 self.first_child()
874 .and_then(|child| match &child.data().kind {
875 NodeKind::Text(value) => Some(value.as_ref()),
876 _ => None,
877 })
878 }
879 NodeKind::PI { .. } => None,
880 NodeKind::Root => None,
881 }
882 }
883 pub fn pi(self) -> Option<PI<'a>> {
885 match &self.data().kind {
886 NodeKind::PI { target, value } => Some(PI {
887 target,
888 value: value.as_deref(),
889 }),
890 _ => None,
891 }
892 }
893}
894
895#[derive(Clone, Copy, Debug)]
897pub struct ExpandedName<'a> {
898 name: &'a str,
899 namespace: Option<&'a str>,
900 match_namespace: bool,
903}
904impl<'a> ExpandedName<'a> {
905 pub fn name(self) -> &'a str {
907 self.name
908 }
909 pub fn namespace(self) -> Option<&'a str> {
911 self.namespace
912 }
913}
914impl PartialEq for ExpandedName<'_> {
915 fn eq(&self, other: &Self) -> bool {
916 self.name == other.name && self.namespace == other.namespace
917 }
918}
919impl Eq for ExpandedName<'_> {}
920impl<'a> From<&'a str> for ExpandedName<'a> {
921 fn from(name: &'a str) -> Self {
922 Self {
923 name,
924 namespace: None,
925 match_namespace: false,
926 }
927 }
928}
929impl<'a> From<(Option<&'a str>, &'a str)> for ExpandedName<'a> {
930 fn from((namespace, name): (Option<&'a str>, &'a str)) -> Self {
931 Self {
932 name,
933 namespace,
934 match_namespace: true,
935 }
936 }
937}
938impl<'a> From<(&'a str, &'a str)> for ExpandedName<'a> {
939 fn from((namespace, name): (&'a str, &'a str)) -> Self {
940 Self {
941 name,
942 namespace: Some(namespace),
943 match_namespace: true,
944 }
945 }
946}
947
948pub struct Attribute<'a, S: Storage = Native> {
950 data: &'a AttributeData<S>,
951}
952impl<S: Storage> Copy for Attribute<'_, S> {}
953impl<S: Storage> Clone for Attribute<'_, S> {
954 fn clone(&self) -> Self {
955 *self
956 }
957}
958impl<'a, S: Storage> Attribute<'a, S> {
959 pub fn xml_id_error(self) -> Option<XmlIdError> {
965 self.data.xml_id_error
966 }
967 pub fn is_id(self) -> bool {
969 self.data.is_id
970 }
971 pub fn name(self) -> &'a str {
973 &self.data.name
974 }
975 pub fn namespace(self) -> Option<&'a str> {
977 self.data.namespace.as_deref()
978 }
979 pub fn value(self) -> &'a str {
981 &self.data.value
982 }
983 pub fn prefix(self) -> Option<&'a str> {
985 self.data.prefix.as_deref()
986 }
987}
988
989pub struct Attributes<'a, S: Storage = Native> {
991 values: std::slice::Iter<'a, AttributeData<S>>,
992}
993impl<'a, S: Storage> Iterator for Attributes<'a, S> {
994 type Item = Attribute<'a, S>;
995 fn next(&mut self) -> Option<Self::Item> {
996 self.values.next().map(|data| Attribute { data })
997 }
998 fn nth(&mut self, n: usize) -> Option<Self::Item> {
999 self.values.nth(n).map(|data| Attribute { data })
1000 }
1001 fn size_hint(&self) -> (usize, Option<usize>) {
1002 self.values.size_hint()
1003 }
1004}
1005impl<S: Storage> ExactSizeIterator for Attributes<'_, S> {}
1006
1007pub struct Namespace<'a, S: Storage = Native> {
1009 data: &'a NamespaceData<S>,
1010}
1011impl<S: Storage> Copy for Namespace<'_, S> {}
1012impl<S: Storage> Clone for Namespace<'_, S> {
1013 fn clone(&self) -> Self {
1014 *self
1015 }
1016}
1017impl<'a, S: Storage> Namespace<'a, S> {
1018 pub fn name(self) -> Option<&'a str> {
1020 self.data.prefix.as_deref()
1021 }
1022 pub fn uri(self) -> &'a str {
1024 &self.data.uri
1025 }
1026}
1027
1028pub struct Namespaces<'a, S: Storage = Native> {
1030 values: std::slice::Iter<'a, NamespaceData<S>>,
1031}
1032impl<'a, S: Storage> Iterator for Namespaces<'a, S> {
1033 type Item = Namespace<'a, S>;
1034 fn next(&mut self) -> Option<Self::Item> {
1035 self.values.next().map(|data| Namespace { data })
1036 }
1037 fn nth(&mut self, n: usize) -> Option<Self::Item> {
1038 self.values.nth(n).map(|data| Namespace { data })
1039 }
1040 fn size_hint(&self) -> (usize, Option<usize>) {
1041 self.values.size_hint()
1042 }
1043}
1044impl<S: Storage> ExactSizeIterator for Namespaces<'_, S> {}
1045
1046#[derive(Clone, Copy)]
1048pub struct PI<'a> {
1049 pub target: &'a str,
1051 pub value: Option<&'a str>,
1053}
1054
1055pub struct Children<'a, 'input, S: Storage = Native> {
1057 document: &'a Document<'input, S>,
1058 ids: std::slice::Iter<'a, NodeId>,
1059}
1060impl<'a, 'input, S: Storage> Iterator for Children<'a, 'input, S> {
1061 type Item = Node<'a, 'input, S>;
1062 fn next(&mut self) -> Option<Self::Item> {
1063 self.ids.next().and_then(|id| self.document.get_node(*id))
1064 }
1065 fn size_hint(&self) -> (usize, Option<usize>) {
1066 self.ids.size_hint()
1067 }
1068}
1069impl<S: Storage> ExactSizeIterator for Children<'_, '_, S> {}
1070impl<'a, 'input, S: Storage> DoubleEndedIterator for Children<'a, 'input, S> {
1071 fn next_back(&mut self) -> Option<Self::Item> {
1072 self.ids
1073 .next_back()
1074 .and_then(|id| self.document.get_node(*id))
1075 }
1076}
1077
1078pub struct Descendants<'a, 'input, S: Storage = Native> {
1080 document: &'a Document<'input, S>,
1081 ids: Range<u32>,
1082}
1083impl<'a, 'input, S: Storage> Iterator for Descendants<'a, 'input, S> {
1084 type Item = Node<'a, 'input, S>;
1085 fn next(&mut self) -> Option<Self::Item> {
1086 self.ids
1087 .next()
1088 .and_then(|id| self.document.get_node(NodeId(id)))
1089 }
1090 fn size_hint(&self) -> (usize, Option<usize>) {
1091 self.ids.size_hint()
1092 }
1093}
1094impl<S: Storage> DoubleEndedIterator for Descendants<'_, '_, S> {
1095 fn next_back(&mut self) -> Option<Self::Item> {
1096 self.ids
1097 .next_back()
1098 .and_then(|id| self.document.get_node(NodeId(id)))
1099 }
1100}
1101impl<S: Storage> ExactSizeIterator for Descendants<'_, '_, S> {}
1102
1103pub struct Ancestors<'a, 'input, S: Storage = Native> {
1105 next: Option<Node<'a, 'input, S>>,
1106}
1107impl<'a, 'input, S: Storage> Iterator for Ancestors<'a, 'input, S> {
1108 type Item = Node<'a, 'input, S>;
1109 fn next(&mut self) -> Option<Self::Item> {
1110 let current = self.next?;
1111 self.next = current.parent();
1112 Some(current)
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 #[cfg(feature = "xmldsig")]
1119 fn assert_scoped_projection<S: super::Storage>(document: &super::Document<'_, S>) {
1120 let root = document.root_element();
1121 assert_eq!(root.tag_name().name(), "r");
1122 assert_eq!(root.attribute(("urn:p", "x")), Some("value"));
1123 assert_eq!(root.lookup_namespace_uri(Some("p")), Some("urn:p"));
1124 let id = root
1125 .attributes()
1126 .find(|attribute| attribute.name() == "id")
1127 .expect("xml:id retained");
1128 assert!(id.is_id());
1129 assert_eq!(id.value(), "a b");
1130 assert_eq!(id.xml_id_error(), Some(super::XmlIdError::InvalidNcName));
1131 assert_eq!(root.children().len(), 1);
1132 assert_eq!(root.text(), Some("leftmiddleright"));
1133 }
1134
1135 #[cfg(all(feature = "xmldsig", feature = "xml-backend-roxmltree"))]
1136 #[test]
1137 fn roxmltree_projection_uses_the_actual_scoped_tree_storage() {
1138 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1142 let xml = "<r xmlns:p='urn:p' xml:id=' a b ' p:x='value'>left<![CDATA[middle]]>right</r>";
1143 let preflight =
1144 super::super::LexicalPreflight::scan(xml, false).expect("fixed XML preflight");
1145 let raw = crate::parser_roxmltree::Document::parse(xml).expect("fixed backend XML");
1146 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1147 let document = super::super::roxmltree::project_in(
1148 xml,
1149 &raw,
1150 &preflight,
1151 super::super::storage::Accounted(&memory),
1152 )
1153 .expect("paid projection fits");
1154 assert_scoped_projection(&document);
1155 assert!(memory.used() > 0);
1156 drop(document);
1157 assert_eq!(memory.used(), 0);
1158 let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
1159 assert!(matches!(
1160 super::super::roxmltree::project_in(
1161 xml,
1162 &raw,
1163 &preflight,
1164 super::super::storage::Accounted(&denied)
1165 ),
1166 Err(super::ParseError::SharedExecution(
1167 MemoryError::LimitExceeded
1168 ))
1169 ));
1170 assert_eq!(denied.used(), 0);
1171 }
1172
1173 #[cfg(all(feature = "xmldsig", feature = "xml-backend-roxmltree"))]
1174 #[test]
1175 fn scoped_parser_tree_and_canonicalizer_share_the_same_semantic_kernel() {
1176 use crate::c14n::{C14nAlgorithm, C14nMode};
1179 use crate::xmldsig::types::{
1180 NodeSetMaterializationBudget,
1181 projection::{PaidKeys, Projection},
1182 };
1183 use structured_asn1::memory::{Memory, MemoryAccount};
1184 let source = "<r xmlns:p='urn:p' xml:id=' a b ' p:x='value'>left<![CDATA[middle]]><!--c--><p:leaf/>right</r>";
1185 let expected = super::Document::parse(source).expect("native semantic baseline");
1186 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
1187 let storage = super::super::storage::Accounted(&memory);
1188 let preflight = super::super::LexicalPreflight::scan_in(source, false, storage)
1189 .expect("funded lexical preflight");
1190 let document = super::super::roxmltree::RoxmltreeBackend::parse_in(
1191 source,
1192 Default::default(),
1193 &preflight,
1194 storage,
1195 )
1196 .expect("funded roxmltree parse");
1197 let nodes = Projection::subtree(
1198 document.root(),
1199 true,
1200 &NodeSetMaterializationBudget::default(),
1201 PaidKeys::new(&memory),
1202 )
1203 .expect("funded node projection");
1204 for mode in [
1205 C14nMode::Inclusive1_0,
1206 C14nMode::Inclusive1_1,
1207 C14nMode::Exclusive1_0,
1208 ] {
1209 for comments in [false, true] {
1210 let algorithm = C14nAlgorithm::new(mode, comments);
1211 let mut canonical = Vec::new();
1212 crate::c14n::canonicalize(&expected, None, &algorithm, &mut canonical)
1213 .expect("native canonical baseline");
1214 let (actual, _) = crate::c14n::canonicalize_buffer_in(
1215 &document,
1216 &nodes,
1217 (&algorithm, None),
1218 None,
1219 1 << 20,
1220 &Default::default(),
1221 &crate::c14n::scratch::Accounted(&memory),
1222 )
1223 .expect("funded canonicalization");
1224 assert_eq!(&*actual, &canonical);
1225 }
1226 }
1227 assert!(memory.used() > 0);
1228 drop(nodes);
1229 drop(document);
1230 drop(preflight);
1231 assert_eq!(memory.used(), 0);
1232 }
1233
1234 #[cfg(all(feature = "xmldsig", feature = "xml-backend-xmloxide"))]
1235 #[test]
1236 fn xmloxide_projection_uses_the_actual_scoped_tree_storage() {
1237 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1240 let xml = "<r xmlns:p='urn:p' xml:id=' a b ' p:x='value'>left<![CDATA[middle]]>right</r>";
1241 let preflight =
1242 super::super::LexicalPreflight::scan(xml, false).expect("fixed XML preflight");
1243 let raw = crate::parser_xmloxide::parser::parse_str_with_options(
1244 xml,
1245 &super::super::xmloxide::backend_options(),
1246 )
1247 .expect("fixed backend XML");
1248 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1249 let document = super::super::xmloxide::project_in(
1250 xml,
1251 &raw,
1252 super::ParsingOptions::default(),
1253 &preflight,
1254 super::super::storage::Accounted(&memory),
1255 )
1256 .expect("paid projection fits");
1257 assert_scoped_projection(&document);
1258 assert!(memory.used() > 0);
1259 drop(document);
1260 assert_eq!(memory.used(), 0);
1261 let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
1262 assert!(matches!(
1263 super::super::xmloxide::project_in(
1264 xml,
1265 &raw,
1266 super::ParsingOptions::default(),
1267 &preflight,
1268 super::super::storage::Accounted(&denied)
1269 ),
1270 Err(super::ParseError::SharedExecution(
1271 MemoryError::LimitExceeded
1272 ))
1273 ));
1274 assert_eq!(denied.used(), 0);
1275 }
1276
1277 #[cfg(all(feature = "xmldsig", feature = "xml-backend-xmloxide"))]
1278 #[test]
1279 fn projection_node_admission_precedes_arena_and_payload_growth() {
1280 use super::super::storage::Accounted;
1283 use structured_asn1::memory::{Memory, MemoryAccount};
1284 let source = format!("<r a='{}'/>", "x".repeat(65536));
1285 let preflight =
1286 super::super::LexicalPreflight::scan(&source, false).expect("native lexical baseline");
1287 let raw = crate::parser_xmloxide::parser::parse_str_with_options(
1288 &source,
1289 &super::super::xmloxide::backend_options(),
1290 )
1291 .expect("native raw XML baseline");
1292 for (limit, capacity) in [(0, 0), (1, 4096)] {
1293 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(capacity, 1 << 24)));
1294 let result = super::super::xmloxide::project_in(
1295 &source,
1296 &raw,
1297 super::ParsingOptions {
1298 nodes_limit: limit,
1299 allow_dtd: false,
1300 },
1301 &preflight,
1302 Accounted(&memory),
1303 );
1304 assert!(matches!(result, Err(super::ParseError::NodesLimitReached)));
1305 assert_eq!(memory.used(), 0);
1306 }
1307 }
1308
1309 use super::Document;
1310 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1311 use super::{NamespaceData, comparison::namespaces_equal};
1312 use crate::xml::dom::{ParseError, ParsingOptions, XmlBackend};
1313
1314 #[test]
1315 fn runtime_selection_never_falls_back_to_a_compiled_backend() {
1316 for backend in [
1319 XmlBackend::Xmloxide,
1320 XmlBackend::Roxmltree,
1321 XmlBackend::Differential,
1322 ] {
1323 let result = Document::parse_with_backend("<r/>", backend);
1324 if backend.is_available() {
1325 assert!(result.is_ok(), "{backend:?} should be available");
1326 } else {
1327 assert_eq!(
1328 result.expect_err("an unavailable backend must not fall back"),
1329 ParseError::BackendUnavailable { backend }
1330 );
1331 }
1332 }
1333 }
1334
1335 #[test]
1336 fn every_backend_enforces_the_same_active_namespace_ceiling() {
1337 let declarations = (0..1_023)
1340 .map(|index| format!(r#" xmlns:n{index}="urn:namespace:{index}""#))
1341 .collect::<String>();
1342 let accepted = format!(r#"<root{declarations}><child xmlns:p="urn:p"/></root>"#);
1343 let rejected = format!(
1344 r#"<root{declarations} xmlns:overflow="urn:overflow"><child xmlns:p="urn:p"/></root>"#
1345 );
1346
1347 for backend in XmlBackend::available() {
1348 Document::parse_with_backend(&accepted, backend)
1349 .unwrap_or_else(|error| panic!("{backend:?} rejected the ceiling: {error}"));
1350 assert_eq!(
1351 Document::parse_with_backend(&rejected, backend)
1352 .expect_err("the 1025th active binding must be rejected"),
1353 ParseError::NamespaceBindingLimitReached {
1354 maximum: 1_024,
1355 actual: 1_025,
1356 },
1357 "{backend:?} must share the lexical namespace contract"
1358 );
1359 }
1360 }
1361
1362 #[cfg(feature = "xml-backend-differential")]
1363 #[test]
1364 fn compatibility_feature_selects_differential_by_default() {
1365 assert_eq!(XmlBackend::default(), XmlBackend::Differential);
1366 }
1367
1368 #[cfg(all(
1369 feature = "xml-backend-xmloxide",
1370 feature = "xml-backend-roxmltree",
1371 not(feature = "xml-backend-differential")
1372 ))]
1373 #[test]
1374 fn fat_build_defaults_to_xmloxide_without_implicit_double_parsing() {
1375 assert_eq!(XmlBackend::default(), XmlBackend::Xmloxide);
1376 }
1377
1378 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1379 #[test]
1380 fn fat_build_selects_each_semantically_equivalent_runtime_mode() {
1381 let xml = r#"<r xmlns:p="urn:p"><p:a x="1">value</p:a></r>"#;
1384 let snapshot = |backend| {
1385 let document = Document::parse_with_backend(xml, backend)
1386 .expect("every compiled runtime mode must parse the fixture");
1387 document
1388 .descendants()
1389 .map(|node| {
1390 (
1391 node.node_type(),
1392 node.range(),
1393 node.text().map(str::to_owned),
1394 )
1395 })
1396 .collect::<Vec<_>>()
1397 };
1398
1399 let xmloxide = snapshot(XmlBackend::Xmloxide);
1400 assert_eq!(snapshot(XmlBackend::Roxmltree), xmloxide);
1401 assert_eq!(snapshot(XmlBackend::Differential), xmloxide);
1402 }
1403
1404 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1405 #[test]
1406 fn differential_namespace_axis_comparison_preserves_multiplicity() {
1407 let binding = |prefix: &str, uri: &str| NamespaceData::<super::Native> {
1410 prefix: Some(prefix.to_owned()),
1411 uri: uri.to_owned(),
1412 };
1413 let a = binding("a", "urn:a");
1414 let b = binding("b", "urn:b");
1415
1416 assert!(
1417 !namespaces_equal(
1418 &[a.clone(), a.clone()],
1419 &[a.clone(), b.clone()],
1420 super::Native,
1421 )
1422 .expect("duplicate axis comparison")
1423 );
1424 assert!(
1425 !namespaces_equal(&[a.clone(), b], &[a.clone(), a.clone()], super::Native,)
1426 .expect("reversed duplicate axis comparison")
1427 );
1428 assert!(
1429 namespaces_equal(&[a.clone(), a.clone()], &[a.clone(), a], super::Native,)
1430 .expect("equal multiplicities")
1431 );
1432 }
1433
1434 #[test]
1435 fn selected_backend_preserves_element_source_ranges() {
1436 let xml = "<?p before?><root><empty/><child>value</child></root><!--after-->";
1438 let document = Document::parse(xml).expect("fixture must parse");
1439 let root = document.root_element();
1440 let child = root
1441 .children()
1442 .find(|node| node.has_tag_name("child"))
1443 .expect("fixture must contain the child element");
1444 assert_eq!(
1445 &xml[root.range()],
1446 "<root><empty/><child>value</child></root>"
1447 );
1448 assert_eq!(&xml[child.range()], "<child>value</child>");
1449 }
1450
1451 #[test]
1452 fn selected_backend_materializes_shadowed_namespace_axis() {
1453 let document = Document::parse(
1455 r#"<r xmlns="urn:outer" xmlns:p="urn:p1"><p:a xmlns:p="urn:p2" xmlns=""><p:b/></p:a></r>"#,
1456 ).expect("fixture must parse");
1457 let child = document
1458 .descendants()
1459 .find(|node| node.has_tag_name(("urn:p2", "b")))
1460 .expect("fixture must contain the namespaced descendant");
1461 assert_eq!(child.lookup_namespace_uri(Some("p")), Some("urn:p2"));
1462 assert_eq!(child.lookup_namespace_uri(None), Some(""));
1463 assert_eq!(child.namespaces().count(), 2);
1464 }
1465
1466 #[test]
1467 fn tag_name_matcher_distinguishes_local_from_unqualified_names() {
1468 let document = Document::parse(r#"<r xmlns:p="urn:p"><item/><p:item/></r>"#)
1471 .expect("namespace matching fixture must parse");
1472 let mut items = document
1473 .root_element()
1474 .children()
1475 .filter(|node| node.is_element());
1476 let unqualified = items.next().expect("unqualified item must exist");
1477 let namespaced = items.next().expect("namespaced item must exist");
1478
1479 assert!(unqualified.has_tag_name("item"));
1480 assert!(namespaced.has_tag_name("item"));
1481 assert!(unqualified.has_tag_name((None, "item")));
1482 assert!(!namespaced.has_tag_name((None, "item")));
1483 assert!(namespaced.has_tag_name((Some("urn:p"), "item")));
1484 }
1485
1486 #[test]
1487 fn namespace_lookup_includes_the_predefined_xml_binding() {
1488 let document = Document::parse("<root><child xml:lang=\"en\"/></root>")
1491 .expect("predefined namespace fixture must parse");
1492 let child = document
1493 .root_element()
1494 .first_element_child()
1495 .expect("fixture must contain a child");
1496
1497 assert_eq!(
1498 child.lookup_namespace_uri(Some("xml")),
1499 Some("http://www.w3.org/XML/1998/namespace")
1500 );
1501 assert_eq!(
1502 child.lookup_prefix("http://www.w3.org/XML/1998/namespace"),
1503 Some("xml")
1504 );
1505 }
1506
1507 #[test]
1508 fn selected_backend_folds_cdata_and_entities_into_one_text_node() {
1509 let document = Document::parse_with_options(
1511 "<!DOCTYPE r [<!ENTITY value 'two'>]><r>one<![CDATA[+]]>&value;three</r>",
1512 ParsingOptions {
1513 allow_dtd: true,
1514 nodes_limit: 8,
1515 },
1516 )
1517 .expect("bounded internal entity fixture must parse");
1518 let text = document
1519 .root_element()
1520 .children()
1521 .filter(|node| node.is_text())
1522 .collect::<Vec<_>>();
1523 assert_eq!(text.len(), 1);
1524 assert_eq!(text[0].text(), Some("one+twothree"));
1525 }
1526
1527 #[test]
1528 fn selected_backend_maps_builtin_references_into_the_surrounding_text_range() {
1529 let xml = "<r>left&right</r>";
1533 let document = Document::parse(xml).expect("fixture must parse");
1534 let text = document
1535 .root_element()
1536 .first_child()
1537 .expect("text child must exist");
1538
1539 assert_eq!(text.text(), Some("left&right"));
1540 assert_eq!(&xml[text.range()], "left&right");
1541 }
1542
1543 #[test]
1544 fn selected_backend_rejects_dtd_before_building_a_tree() {
1545 assert_eq!(
1547 Document::parse("<!DOCTYPE r><r/>")
1548 .expect_err("DTD-disabled parsing must reject a document type"),
1549 ParseError::DtdDetected,
1550 );
1551 }
1552
1553 #[test]
1554 fn selected_backend_enforces_semantic_node_limit_after_text_folding() {
1555 let error = Document::parse_with_options(
1557 "<r><a/><b/></r>",
1558 ParsingOptions {
1559 allow_dtd: false,
1560 nodes_limit: 3,
1561 },
1562 )
1563 .expect_err("the semantic node limit must reject the fourth node");
1564 assert_eq!(error, ParseError::NodesLimitReached);
1565 }
1566
1567 #[test]
1568 fn selected_backend_enforces_the_absolute_node_ceiling_before_dom_allocation() {
1569 let count = crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize;
1573 let xml = format!("<r>{}</r>", "<n/>".repeat(count));
1574
1575 assert_eq!(
1576 Document::parse_with_options(
1577 &xml,
1578 ParsingOptions {
1579 allow_dtd: false,
1580 nodes_limit: u32::MAX,
1581 },
1582 )
1583 .expect_err("the absolute retained-node ceiling must remain effective"),
1584 ParseError::NodesLimitReached,
1585 );
1586 }
1587
1588 #[test]
1589 fn selected_backend_enforces_the_absolute_byte_ceiling_before_dom_allocation() {
1590 let maximum = crate::hard_limits::XML_DOCUMENT_BYTE_CEILING;
1593 let xml = format!("<r>{}</r>", "x".repeat(maximum));
1594 let expected = ParseError::ByteLimitReached {
1595 maximum,
1596 actual: xml.len(),
1597 };
1598
1599 assert_eq!(
1600 Document::parse(&xml)
1601 .expect_err("default direct parsing must enforce the byte ceiling"),
1602 expected,
1603 );
1604 assert_eq!(
1605 Document::parse_with_options(
1606 &xml,
1607 ParsingOptions {
1608 allow_dtd: false,
1609 nodes_limit: u32::MAX,
1610 },
1611 )
1612 .expect_err("custom direct parsing must retain the absolute byte ceiling"),
1613 expected,
1614 );
1615 }
1616
1617 #[test]
1618 fn selected_backend_bounds_direct_entity_expansion_count() {
1619 let references =
1622 "&value;".repeat(crate::hard_limits::XML_ENTITY_EXPANSION_CEILING as usize + 1);
1623 let xml = format!("<!DOCTYPE r [<!ENTITY value 'x'>]><r>{references}</r>");
1624
1625 assert!(matches!(
1626 Document::parse_with_options(
1627 &xml,
1628 ParsingOptions {
1629 allow_dtd: true,
1630 nodes_limit: u32::MAX,
1631 },
1632 ),
1633 Err(ParseError::EntityExpansionLimitReached { maximum, actual })
1634 if maximum == crate::hard_limits::XML_ENTITY_EXPANSION_CEILING
1635 && actual == maximum + 1
1636 ));
1637 }
1638
1639 #[test]
1640 fn selected_backend_bounds_source_positions_after_semantic_text_folding() {
1641 let segments = crate::hard_limits::XML_SOURCE_POSITION_CEILING + 1;
1645 let xml = format!("<r>{}</r>", "<![CDATA[x]]>".repeat(segments));
1646
1647 assert!(matches!(
1648 Document::parse_with_options(
1649 &xml,
1650 ParsingOptions {
1651 allow_dtd: false,
1652 nodes_limit: 3,
1653 },
1654 ),
1655 Err(ParseError::SourcePositionLimitReached { maximum, actual })
1656 if maximum == crate::hard_limits::XML_SOURCE_POSITION_CEILING
1657 && actual == maximum + 1
1658 ));
1659 }
1660
1661 #[test]
1662 fn unqualified_attribute_lookup_requires_no_namespace() {
1663 let document = Document::parse(
1666 r#"<r xmlns:evil="urn:evil" evil:Algorithm="extension" Algorithm="schema"/>"#,
1667 )
1668 .expect("fixture must parse");
1669 let root = document.root_element();
1670
1671 assert_eq!(root.attribute("Algorithm"), Some("schema"));
1672 assert_eq!(root.attribute(("urn:evil", "Algorithm")), Some("extension"));
1673
1674 let extension_only =
1675 Document::parse(r#"<r xmlns:evil="urn:evil" evil:Algorithm="extension"/>"#)
1676 .expect("fixture must parse");
1677 assert_eq!(extension_only.root_element().attribute("Algorithm"), None);
1678 }
1679
1680 #[test]
1681 fn node_text_preserves_character_data_contract() {
1682 let document = Document::parse("<r><?p hidden?>visible</r>").expect("fixture must parse");
1685 let root = document.root_element();
1686 let pi = root.first_child().expect("PI must exist");
1687
1688 assert_eq!(pi.text(), None);
1689 assert_eq!(pi.pi().and_then(|value| value.value), Some("hidden"));
1690 assert_eq!(root.text(), None);
1691 assert_eq!(
1692 pi.next_sibling().and_then(|node| node.text()),
1693 Some("visible")
1694 );
1695
1696 let nested =
1697 Document::parse("<r><child>nested</child>later</r>").expect("fixture must parse");
1698 assert_eq!(nested.root_element().text(), None);
1699 }
1700
1701 #[test]
1702 fn selected_backend_rejects_deep_documents_before_dom_parsing() {
1703 const DEPTH: usize = 20_000;
1706 let mut xml = String::with_capacity(DEPTH * 7);
1707 xml.push_str(&"<n>".repeat(DEPTH));
1708 xml.push_str(&"</n>".repeat(DEPTH));
1709
1710 assert!(matches!(
1711 Document::parse(&xml),
1712 Err(ParseError::DepthLimitReached { maximum, actual })
1713 if maximum == crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING
1714 && actual == maximum + 1
1715 ));
1716 }
1717
1718 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
1719 #[test]
1720 fn differential_comparison_fails_closed_on_semantic_divergence() {
1721 let left = Document::parse("<r><a/></r>").expect("fixture must parse");
1724 let mut right = Document::parse("<r><a/></r>").expect("fixture must parse");
1725 right.nodes[2].range.end -= 1;
1726
1727 assert!(matches!(
1728 left.ensure_semantically_equivalent(&right, None),
1729 Err(ParseError::BackendDivergence { .. })
1730 ));
1731 }
1732
1733 #[test]
1734 fn dtd_internal_comments_are_not_document_nodes() {
1735 let document = Document::parse_with_options(
1738 "<!--before--><!DOCTYPE r [<!-- ] > --><!ENTITY value 'ok'>]><r>&value;</r><!--after-->",
1739 ParsingOptions {
1740 allow_dtd: true,
1741 ..ParsingOptions::default()
1742 },
1743 )
1744 .expect("DTD fixture must parse");
1745 let root = document.root();
1746 let comments = root
1747 .descendants()
1748 .filter(|node| node.is_comment())
1749 .filter_map(|node| node.text())
1750 .collect::<Vec<_>>();
1751
1752 assert_eq!(comments, ["before", "after"]);
1753 assert_eq!(
1754 root.descendants()
1755 .find(|node| node.is_text())
1756 .and_then(|node| node.text()),
1757 Some("ok")
1758 );
1759 }
1760
1761 #[test]
1762 fn dtd_range_ignores_doctype_text_in_prolog_nodes() {
1763 let document = Document::parse_with_options(
1766 "<!-- <!DOCTYPE fake> --><?probe <!DOCTYPE fake> ?><!DOCTYPE r [<!--hidden-->]><r/>",
1767 ParsingOptions {
1768 allow_dtd: true,
1769 ..ParsingOptions::default()
1770 },
1771 )
1772 .expect("DTD fixture must parse");
1773 let root = document.root();
1774 let comments = root
1775 .children()
1776 .filter(|node| node.is_comment())
1777 .filter_map(|node| node.text())
1778 .collect::<Vec<_>>();
1779 let processing_instructions = root
1780 .children()
1781 .filter(|node| node.is_pi())
1782 .filter_map(|node| node.pi())
1783 .collect::<Vec<_>>();
1784
1785 assert_eq!(comments, [" <!DOCTYPE fake> "]);
1786 assert_eq!(processing_instructions.len(), 1);
1787 assert_eq!(processing_instructions[0].target, "probe");
1788 }
1789
1790 #[cfg(feature = "xml-backend-roxmltree")]
1791 #[test]
1792 fn dtd_range_scanning_accepts_unicode_names() {
1793 let input = "<!DOCTYPE r [<!ENTITY café 'ok'>]><r/>";
1796 let end = input.find("><r/>").expect("fixture has document root") + 1;
1797
1798 let preflight = super::super::LexicalPreflight::scan(input, true)
1799 .expect("Unicode DTD fixture must pass lexical preflight");
1800 assert_eq!(preflight.doctype_range(), Some(&(0..end)));
1801 }
1802}