1use std::cell::Cell;
9use std::collections::HashSet;
10use std::ops::RangeInclusive;
11
12use roxmltree::{Document, Node, NodeId};
13
14const MAX_NODE_SET_ENTRIES: usize = 65_536;
15const MAX_NODE_SET_OWNED_STRING_BYTES: usize = 8 * 1024 * 1024;
16const MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: usize = 64 * 1024 * 1024;
17
18use crate::c14n::NodeVisibility;
19
20pub enum TransformData<'a> {
29 NodeSet(NodeSet<'a>),
31 Binary(Vec<u8>),
33}
34
35impl std::fmt::Debug for TransformData<'_> {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::NodeSet(_) => f.debug_tuple("NodeSet").field(&"...").finish(),
39 Self::Binary(b) => f.debug_tuple("Binary").field(&b.len()).finish(),
40 }
41 }
42}
43
44impl<'a> TransformData<'a> {
45 pub fn into_node_set(self) -> Result<NodeSet<'a>, TransformError> {
47 match self {
48 Self::NodeSet(ns) => Ok(ns),
49 Self::Binary(_) => Err(TransformError::TypeMismatch {
50 expected: "NodeSet",
51 got: "Binary",
52 }),
53 }
54 }
55
56 pub fn into_binary(self) -> Result<Vec<u8>, TransformError> {
58 match self {
59 Self::Binary(b) => Ok(b),
60 Self::NodeSet(_) => Err(TransformError::TypeMismatch {
61 expected: "Binary",
62 got: "NodeSet",
63 }),
64 }
65 }
66}
67
68pub struct NodeSet<'a> {
77 doc: &'a Document<'a>,
79 nodes: HashSet<XmlNodeKey>,
80 with_comments: bool,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86enum XmlNodeKey {
87 Tree(NodeId),
88 Attribute {
89 owner: NodeId,
90 namespace: Option<String>,
91 local_name: String,
92 },
93 Namespace {
94 owner: NodeId,
95 prefix: String,
96 uri: String,
97 },
98}
99
100pub(crate) struct NodeSetMaterializationBudget {
101 remaining_owned_string_bytes: Cell<usize>,
102}
103
104impl Default for NodeSetMaterializationBudget {
105 fn default() -> Self {
106 Self {
107 remaining_owned_string_bytes: Cell::new(MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES),
108 }
109 }
110}
111
112impl NodeSetMaterializationBudget {
113 fn charge(&self, owned_string_bytes: usize) -> Result<(), TransformError> {
114 let Some(remaining) = self
115 .remaining_owned_string_bytes
116 .get()
117 .checked_sub(owned_string_bytes)
118 else {
119 self.remaining_owned_string_bytes.set(0);
120 return Err(TransformError::NodeSetCumulativeStringsTooLarge {
121 max_bytes: MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
122 });
123 };
124 self.remaining_owned_string_bytes.set(remaining);
125 Ok(())
126 }
127
128 #[cfg(test)]
129 pub(crate) fn with_limit(limit: usize) -> Self {
130 Self {
131 remaining_owned_string_bytes: Cell::new(limit),
132 }
133 }
134}
135
136impl XmlNodeKey {
137 fn owner_id(&self) -> NodeId {
138 match self {
139 Self::Tree(id) => *id,
140 Self::Attribute { owner, .. } | Self::Namespace { owner, .. } => *owner,
141 }
142 }
143}
144
145impl<'a> NodeSet<'a> {
146 pub fn entire_document_without_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
157 Self::ensure_subtree_materialization_fits(doc.root())?;
158 Ok(Self::collect_document(doc, false))
159 }
160
161 pub(crate) fn entire_document_without_comments_with_budget(
162 doc: &'a Document<'a>,
163 budget: &NodeSetMaterializationBudget,
164 ) -> Result<Self, TransformError> {
165 Self::charge_subtree_materialization(doc.root(), budget)?;
166 Ok(Self::collect_document(doc, false))
167 }
168
169 pub fn entire_document_with_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
179 Self::ensure_subtree_materialization_fits(doc.root())?;
180 Ok(Self::collect_document(doc, true))
181 }
182
183 pub(crate) fn entire_document_with_comments_with_budget(
184 doc: &'a Document<'a>,
185 budget: &NodeSetMaterializationBudget,
186 ) -> Result<Self, TransformError> {
187 Self::charge_subtree_materialization(doc.root(), budget)?;
188 Ok(Self::collect_document(doc, true))
189 }
190
191 pub fn subtree(element: Node<'a, 'a>) -> Result<Self, TransformError> {
201 Self::ensure_subtree_materialization_fits(element)?;
202 Ok(Self::collect_subtree(element))
203 }
204
205 pub(crate) fn subtree_without_comments_with_budget(
208 element: Node<'a, 'a>,
209 budget: Option<&NodeSetMaterializationBudget>,
210 ) -> Result<Self, TransformError> {
211 match budget {
212 Some(budget) => Self::charge_subtree_materialization(element, budget)?,
213 None => {
214 Self::ensure_subtree_materialization_fits(element)?;
215 }
216 }
217 let mut set = Self {
218 doc: element.document(),
219 nodes: HashSet::new(),
220 with_comments: false,
221 };
222 for node in element.descendants().filter(|node| !node.is_comment()) {
223 set.insert_node(node);
224 if node.is_element() {
225 for attribute in node.attributes() {
226 set.insert_attribute(node, attribute.namespace(), attribute.name());
227 }
228 for namespace in node.namespaces() {
229 set.insert_namespace(node, namespace.name().unwrap_or(""), namespace.uri());
230 }
231 }
232 }
233 Ok(set)
234 }
235
236 pub(crate) fn subtree_with_budget(
237 element: Node<'a, 'a>,
238 budget: &NodeSetMaterializationBudget,
239 ) -> Result<Self, TransformError> {
240 Self::charge_subtree_materialization(element, budget)?;
241 Ok(Self::collect_subtree(element))
242 }
243
244 fn collect_subtree(element: Node<'a, 'a>) -> Self {
245 let mut set = Self {
246 doc: element.document(),
247 nodes: HashSet::new(),
248 with_comments: true,
249 };
250 set.insert_subtree(element);
251 set
252 }
253
254 pub fn document(&self) -> &'a Document<'a> {
256 self.doc
257 }
258
259 pub fn contains(&self, node: Node<'_, '_>) -> bool {
264 if !std::ptr::eq(node.document() as *const _, self.doc as *const _) {
268 return false;
269 }
270
271 self.nodes.contains(&XmlNodeKey::Tree(node.id()))
272 }
273
274 pub fn exclude_subtree(&mut self, node: Node<'_, '_>) {
278 if !std::ptr::eq(node.document() as *const _, self.doc as *const _) {
280 return;
281 }
282 let excluded_ids = subtree_node_id_range(node);
283 self.nodes
288 .retain(|key| !excluded_ids.contains(&key.owner_id().get()));
289 }
290
291 pub fn with_comments(&self) -> bool {
293 self.with_comments
294 }
295
296 pub(crate) fn empty(doc: &'a Document<'a>) -> Self {
297 Self {
298 doc,
299 nodes: HashSet::new(),
300 with_comments: false,
301 }
302 }
303
304 #[cfg(test)]
305 pub(crate) fn try_entire_document(doc: &'a Document<'a>) -> Result<Self, TransformError> {
306 Self::entire_document_with_comments(doc)
307 }
308
309 pub(crate) fn try_entire_document_with_budget(
310 doc: &'a Document<'a>,
311 budget: &NodeSetMaterializationBudget,
312 ) -> Result<Self, TransformError> {
313 Self::entire_document_with_comments_with_budget(doc, budget)
314 }
315
316 pub(crate) fn len(&self) -> usize {
317 self.nodes.len()
318 }
319
320 pub(crate) fn insert_node(&mut self, node: Node<'_, '_>) {
321 if self.owns(node) {
322 self.with_comments |= node.is_comment();
323 self.nodes.insert(XmlNodeKey::Tree(node.id()));
324 }
325 }
326
327 pub(crate) fn insert_attribute(
328 &mut self,
329 owner: Node<'_, '_>,
330 namespace: Option<&str>,
331 local_name: &str,
332 ) {
333 if self.owns(owner) {
334 self.nodes.insert(XmlNodeKey::Attribute {
335 owner: owner.id(),
336 namespace: namespace.map(str::to_owned),
337 local_name: local_name.to_owned(),
338 });
339 }
340 }
341
342 pub(crate) fn insert_attribute_with_budget(
343 &mut self,
344 owner: Node<'_, '_>,
345 namespace: Option<&str>,
346 local_name: &str,
347 budget: &NodeSetMaterializationBudget,
348 ) -> Result<(), TransformError> {
349 if self.owns(owner) {
350 let owned_string_bytes = namespace
351 .map_or(0, str::len)
352 .checked_add(local_name.len())
353 .ok_or(TransformError::NodeSetStringsTooLarge {
354 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
355 })?;
356 budget.charge(owned_string_bytes)?;
357 self.insert_attribute(owner, namespace, local_name);
358 }
359 Ok(())
360 }
361
362 pub(crate) fn insert_namespace(&mut self, owner: Node<'_, '_>, prefix: &str, uri: &str) {
363 if self.owns(owner) {
364 self.nodes.insert(XmlNodeKey::Namespace {
365 owner: owner.id(),
366 prefix: prefix.to_owned(),
367 uri: uri.to_owned(),
368 });
369 }
370 }
371
372 pub(crate) fn insert_namespace_with_budget(
373 &mut self,
374 owner: Node<'_, '_>,
375 prefix: &str,
376 uri: &str,
377 budget: &NodeSetMaterializationBudget,
378 ) -> Result<(), TransformError> {
379 if self.owns(owner) {
380 let owned_string_bytes = prefix.len().checked_add(uri.len()).ok_or(
381 TransformError::NodeSetStringsTooLarge {
382 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
383 },
384 )?;
385 budget.charge(owned_string_bytes)?;
386 self.insert_namespace(owner, prefix, uri);
387 }
388 Ok(())
389 }
390
391 pub(crate) fn insert_subtree(&mut self, root: Node<'_, '_>) {
392 if !self.owns(root) {
393 return;
394 }
395 let mut stack = vec![root];
396 while let Some(node) = stack.pop() {
397 self.insert_node(node);
398 if node.is_element() {
399 for attribute in node.attributes() {
400 self.insert_attribute(node, attribute.namespace(), attribute.name());
401 }
402 for namespace in node.namespaces() {
403 self.insert_namespace(node, namespace.name().unwrap_or(""), namespace.uri());
404 }
405 }
406 stack.extend(node.children());
407 }
408 }
409
410 pub(crate) fn intersect_with(&mut self, other: &Self) {
411 if !std::ptr::eq(self.doc as *const _, other.doc as *const _) {
412 self.nodes.clear();
413 self.with_comments = false;
414 return;
415 }
416 self.nodes.retain(|key| other.nodes.contains(key));
417 self.with_comments &= other.with_comments;
418 }
419
420 pub(crate) fn subtract(&mut self, other: &Self) {
421 if std::ptr::eq(self.doc as *const _, other.doc as *const _) {
422 self.nodes.retain(|key| !other.nodes.contains(key));
423 }
424 }
425
426 pub(crate) fn union_with_budget(
427 &mut self,
428 other: &Self,
429 budget: &NodeSetMaterializationBudget,
430 ) -> Result<(), TransformError> {
431 if std::ptr::eq(self.doc as *const _, other.doc as *const _) {
432 for key in &other.nodes {
433 if self.nodes.contains(key) {
434 continue;
435 }
436 let owned_string_bytes = match key {
437 XmlNodeKey::Tree(_) => 0,
438 XmlNodeKey::Attribute {
439 namespace,
440 local_name,
441 ..
442 } => namespace.as_ref().map_or(0, String::len) + local_name.len(),
443 XmlNodeKey::Namespace { prefix, uri, .. } => prefix.len() + uri.len(),
444 };
445 budget.charge(owned_string_bytes)?;
446 self.nodes.insert(key.clone());
447 }
448 self.with_comments |= other.with_comments;
449 }
450 Ok(())
451 }
452
453 fn collect_document(doc: &'a Document<'a>, with_comments: bool) -> Self {
454 let mut set = Self::empty(doc);
455 set.insert_subtree(doc.root());
456 if !with_comments {
457 set.nodes.retain(|key| match key {
458 XmlNodeKey::Tree(id) => !doc.get_node(*id).is_some_and(|node| node.is_comment()),
459 _ => true,
460 });
461 }
462 set.with_comments = with_comments;
463 set
464 }
465
466 pub(crate) fn ensure_subtree_materialization_fits(
467 root: Node<'_, '_>,
468 ) -> Result<usize, TransformError> {
469 Ok(Self::subtree_materialization(root)?.entries)
470 }
471
472 fn charge_subtree_materialization(
473 root: Node<'_, '_>,
474 budget: &NodeSetMaterializationBudget,
475 ) -> Result<(), TransformError> {
476 let materialization = Self::subtree_materialization(root)?;
477 budget.charge(materialization.owned_string_bytes)
478 }
479
480 fn subtree_materialization(
481 root: Node<'_, '_>,
482 ) -> Result<NodeSetMaterialization, TransformError> {
483 let mut entries = 0_usize;
484 let mut owned_string_bytes = 0_usize;
485 let mut stack = vec![root];
486 while let Some(node) = stack.pop() {
487 let projected = if node.is_element() {
488 for attribute in node.attributes() {
489 owned_string_bytes = charge_node_set_string_bytes(
490 owned_string_bytes,
491 attribute.namespace().map_or(0, str::len),
492 )?;
493 owned_string_bytes =
494 charge_node_set_string_bytes(owned_string_bytes, attribute.name().len())?;
495 }
496 for namespace in node.namespaces() {
497 owned_string_bytes = charge_node_set_string_bytes(
498 owned_string_bytes,
499 namespace.name().map_or(0, str::len),
500 )?;
501 owned_string_bytes =
502 charge_node_set_string_bytes(owned_string_bytes, namespace.uri().len())?;
503 }
504 1_usize
505 .checked_add(node.attributes().len())
506 .and_then(|count| count.checked_add(node.namespaces().len()))
507 } else {
508 Some(1)
509 }
510 .ok_or(TransformError::NodeSetTooLarge {
511 max: MAX_NODE_SET_ENTRIES,
512 })?;
513 entries = entries
514 .checked_add(projected)
515 .ok_or(TransformError::NodeSetTooLarge {
516 max: MAX_NODE_SET_ENTRIES,
517 })?;
518 if entries > MAX_NODE_SET_ENTRIES {
519 return Err(TransformError::NodeSetTooLarge {
520 max: MAX_NODE_SET_ENTRIES,
521 });
522 }
523 stack.extend(node.children());
524 }
525 Ok(NodeSetMaterialization {
526 entries,
527 owned_string_bytes,
528 })
529 }
530
531 fn owns(&self, node: Node<'_, '_>) -> bool {
532 std::ptr::eq(node.document() as *const _, self.doc as *const _)
533 }
534}
535
536struct NodeSetMaterialization {
537 entries: usize,
538 owned_string_bytes: usize,
539}
540
541fn charge_node_set_string_bytes(
542 current: usize,
543 additional: usize,
544) -> Result<usize, TransformError> {
545 let total = current
546 .checked_add(additional)
547 .ok_or(TransformError::NodeSetStringsTooLarge {
548 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
549 })?;
550 if total > MAX_NODE_SET_OWNED_STRING_BYTES {
551 return Err(TransformError::NodeSetStringsTooLarge {
552 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
553 });
554 }
555 Ok(total)
556}
557
558fn subtree_node_id_range(node: Node<'_, '_>) -> RangeInclusive<u32> {
559 let last_id = node
560 .descendants()
561 .next_back()
562 .map_or(node.id(), |descendant| descendant.id());
563 node.id().get()..=last_id.get()
564}
565
566impl NodeVisibility for NodeSet<'_> {
567 fn contains_node(&self, node: Node<'_, '_>) -> bool {
568 self.contains(node)
569 }
570
571 fn contains_attribute(
572 &self,
573 owner: Node<'_, '_>,
574 namespace: Option<&str>,
575 local_name: &str,
576 ) -> bool {
577 self.owns(owner)
578 && self.nodes.contains(&XmlNodeKey::Attribute {
579 owner: owner.id(),
580 namespace: namespace.map(str::to_owned),
581 local_name: local_name.to_owned(),
582 })
583 }
584
585 fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool {
586 self.owns(owner)
587 && self.nodes.contains(&XmlNodeKey::Namespace {
588 owner: owner.id(),
589 prefix: prefix.to_owned(),
590 uri: uri.to_owned(),
591 })
592 }
593}
594
595#[derive(Debug, thiserror::Error)]
597#[non_exhaustive]
598pub enum TransformError {
599 #[error("type mismatch: expected {expected}, got {got}")]
601 TypeMismatch {
602 expected: &'static str,
604 got: &'static str,
606 },
607
608 #[error("element not found by ID: {0}")]
610 ElementNotFound(String),
611
612 #[error("unsupported URI: {0}")]
614 UnsupportedUri(String),
615
616 #[error("unsupported transform: {0}")]
618 UnsupportedTransform(String),
619
620 #[error("transform chain exceeds maximum length of {max}")]
622 TooManyTransforms {
623 max: usize,
625 },
626
627 #[error("node-set materialization exceeds maximum of {max} entries")]
629 NodeSetTooLarge {
630 max: usize,
632 },
633
634 #[error("node-set materialization exceeds maximum of {max_bytes} owned string bytes")]
636 NodeSetStringsTooLarge {
637 max_bytes: usize,
639 },
640
641 #[error(
643 "node-set materialization exceeds signature-wide maximum of {max_bytes} cumulative owned string bytes"
644 )]
645 NodeSetCumulativeStringsTooLarge {
646 max_bytes: usize,
648 },
649
650 #[error("node-set filtering exceeds signature-wide maximum of {max_entries} entry visits")]
652 NodeSetFilterWorkTooLarge {
653 max_entries: usize,
655 },
656
657 #[error(
659 "XPath mirrors exceed signature-wide maximum of {max_bytes} cumulative copied string bytes"
660 )]
661 XPathMirrorTooLarge {
662 max_bytes: usize,
664 },
665
666 #[error(
668 "XPath transform exceeds signature-wide maximum of {max_bytes} string-processing work bytes"
669 )]
670 XPathStringWorkTooLarge {
671 max_bytes: usize,
673 },
674
675 #[error("C14N error: {0}")]
677 C14n(#[from] crate::c14n::C14nError),
678
679 #[error("cumulative canonical output exceeds signature-wide maximum of {max_bytes} bytes")]
681 C14nOutputTooLarge {
682 max_bytes: usize,
684 },
685
686 #[error("base64 transform decode error: {0}")]
688 Base64(String),
689
690 #[error("cumulative base64 transform input exceeds maximum of {max_bytes} bytes")]
692 Base64InputTooLarge {
693 max_bytes: usize,
695 },
696
697 #[error("base64 transform output exceeds maximum of {max_bytes} bytes")]
699 Base64OutputTooLarge {
700 max_bytes: usize,
702 },
703
704 #[error("XPath transform error: {0}")]
706 XPath(String),
707
708 #[error("XML transform input parse error: {0}")]
711 XmlParse(String),
712
713 #[error("XML transform input exceeds the configured node limit")]
715 XmlNodeLimit,
716
717 #[error("XML Base resolution exceeds maximum of {max} inherited components: got {actual}")]
719 XmlBaseComponentsTooLarge {
720 max: usize,
722 actual: usize,
724 },
725
726 #[error("XML Base resolution exceeds cumulative maximum of {max_bytes} bytes: got {actual}")]
728 XmlBaseResolutionTooLarge {
729 max_bytes: usize,
731 actual: usize,
733 },
734
735 #[error("external resource bytes exceed maximum of {max_bytes}: got {actual}")]
737 ExternalResourceTooLarge {
738 max_bytes: usize,
740 actual: usize,
742 },
743
744 #[error("aggregate external resource bytes exceed maximum of {max_bytes}: got {actual}")]
746 ExternalResourceTotalTooLarge {
747 max_bytes: usize,
749 actual: usize,
751 },
752
753 #[error("enveloped-signature transform: invalid Signature node for this document")]
756 CrossDocumentSignatureNode,
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762 use crate::c14n::{C14nAlgorithm, C14nMode, canonicalize_with_visibility};
763
764 #[test]
765 fn document_without_comments_preserves_comment_policy() {
766 let document = Document::parse("<root><!-- excluded --><child/></root>")
769 .expect("fixed comment fixture must parse");
770 let nodes = NodeSet::entire_document_without_comments(&document)
771 .expect("fixed fixture must fit the node-set materialization budget");
772 let comment = document
773 .descendants()
774 .find(|node| node.is_comment())
775 .expect("fixed fixture contains one comment");
776
777 assert!(!nodes.contains(comment));
778 assert!(!nodes.with_comments());
779 }
780
781 #[test]
782 fn excluding_disjoint_oversized_subtree_only_scans_input_keys() {
783 let xml = format!(
787 "<root><target Id=\"selected\"><child/></target><Signature>{}</Signature></root>",
788 "<Object/>".repeat(MAX_NODE_SET_ENTRIES + 1)
789 );
790 let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
791 let target = document
792 .descendants()
793 .find(|node| node.attribute("Id") == Some("selected"))
794 .expect("fixed fixture contains the selected subtree");
795 let signature = document
796 .descendants()
797 .find(|node| node.has_tag_name("Signature"))
798 .expect("fixed fixture contains the excluded Signature subtree");
799 let mut nodes = NodeSet::subtree(target)
800 .expect("small selected subtree must fit the materialization budget");
801 let entries_before = nodes.nodes.len();
802
803 nodes.exclude_subtree(signature);
804
805 assert_eq!(nodes.nodes.len(), entries_before);
806 assert!(nodes.contains(target));
807 assert!(
808 nodes.contains(
809 target
810 .first_element_child()
811 .expect("fixed target subtree contains a child")
812 )
813 );
814 }
815
816 #[test]
817 fn materialization_rejects_inherited_namespace_byte_amplification() {
818 let namespace_uri = "x".repeat(8_192);
822 let xml = format!(
823 "<root xmlns:amplified=\"{namespace_uri}\">{}</root>",
824 "<child/>".repeat(1_025)
825 );
826 let document = Document::parse(&xml).expect("fixed namespace fixture must parse");
827
828 let error = NodeSet::entire_document_without_comments(&document)
829 .err()
830 .expect("amplified namespace bytes must exceed the materialization budget");
831
832 assert!(matches!(
833 error,
834 TransformError::NodeSetStringsTooLarge { .. }
835 ));
836 }
837
838 #[test]
839 fn subtree_node_id_range_contains_only_the_selected_subtree() {
840 let document = Document::parse(
844 "<root><before/><excluded xmlns:gone=\"urn:gone\" a=\"1\"><child/></excluded><after/></root>",
845 )
846 .expect("fixed subtree range fixture must parse");
847 let excluded = document
848 .descendants()
849 .find(|node| node.has_tag_name("excluded"))
850 .expect("fixed fixture contains the excluded subtree");
851 let range = subtree_node_id_range(excluded);
852 let before = document
853 .descendants()
854 .find(|node| node.has_tag_name("before"))
855 .expect("fixed fixture contains the preceding sibling");
856 let child = excluded
857 .first_element_child()
858 .expect("fixed fixture contains an excluded child");
859 let after = document
860 .descendants()
861 .find(|node| node.has_tag_name("after"))
862 .expect("fixed fixture contains the following sibling");
863
864 assert!(!range.contains(&before.id().get()));
865 assert!(range.contains(&excluded.id().get()));
866 assert!(range.contains(&child.id().get()));
867 assert!(!range.contains(&after.id().get()));
868
869 let mut nodes = NodeSet::entire_document_with_comments(&document)
870 .expect("fixed fixture must fit the node-set materialization budget");
871 nodes.exclude_subtree(excluded);
872
873 assert!(nodes.contains(before));
874 assert!(!nodes.contains(excluded));
875 assert!(!nodes.contains(child));
876 assert!(!nodes.contains_attribute(excluded, None, "a"));
877 assert!(!nodes.contains_namespace(excluded, "gone", "urn:gone"));
878 assert!(nodes.contains(after));
879 }
880
881 #[test]
882 fn excluding_subtree_removes_trailing_text_and_comments_from_canonical_output() {
883 let document = Document::parse(
887 "<root><before/>keep-before<excluded><child/>drop-text<!--drop-comment--></excluded>keep-after<after/></root>",
888 )
889 .expect("fixed trailing-node fixture must parse");
890 let excluded = document
891 .descendants()
892 .find(|node| node.has_tag_name("excluded"))
893 .expect("fixed fixture contains the excluded subtree");
894 let trailing_text = excluded
895 .children()
896 .find(|node| node.is_text())
897 .expect("fixed fixture contains trailing text");
898 let trailing_comment = excluded
899 .children()
900 .find(|node| node.is_comment())
901 .expect("fixed fixture contains a trailing comment");
902 let mut nodes = NodeSet::entire_document_with_comments(&document)
903 .expect("fixed fixture must fit the node-set materialization budget");
904
905 nodes.exclude_subtree(excluded);
906
907 assert!(!nodes.contains(trailing_text));
908 assert!(!nodes.contains(trailing_comment));
909 let mut output = Vec::new();
910 canonicalize_with_visibility(
911 &document,
912 Some(&nodes),
913 &C14nAlgorithm::new(C14nMode::Inclusive1_0, true),
914 &mut output,
915 )
916 .expect("the retained node set must canonicalize");
917 assert_eq!(
918 output,
919 b"<root><before></before>keep-beforekeep-after<after></after></root>"
920 );
921 }
922}