1use std::cell::Cell;
9use std::ops::RangeInclusive;
10
11use crate::xml::dom::{Document, Node};
12
13const MAX_NODE_SET_ENTRIES: usize = crate::hard_limits::NODE_SET_ENTRY_CEILING;
14const MAX_NODE_SET_OWNED_STRING_BYTES: usize =
15 crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING;
16const MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: usize =
17 crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING;
18
19use crate::c14n::NodeVisibility;
20
21pub(crate) mod projection;
22
23pub enum TransformData<'a> {
32 NodeSet(NodeSet<'a>),
34 Binary(Vec<u8>),
36 BorrowedBinary(&'a [u8]),
38 Resource {
41 bytes: &'a [u8],
43 uri: &'a str,
45 },
46}
47
48impl std::fmt::Debug for TransformData<'_> {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::NodeSet(_) => f.debug_tuple("NodeSet").field(&"...").finish(),
52 Self::Binary(b) => f.debug_tuple("Binary").field(&b.len()).finish(),
53 Self::BorrowedBinary(b) => f.debug_tuple("BorrowedBinary").field(&b.len()).finish(),
54 Self::Resource { bytes, uri } => f
55 .debug_struct("Resource")
56 .field("bytes", &bytes.len())
57 .field("uri", uri)
58 .finish(),
59 }
60 }
61}
62
63impl<'a> TransformData<'a> {
64 pub fn into_node_set(self) -> Result<NodeSet<'a>, TransformError> {
66 match self {
67 Self::NodeSet(ns) => Ok(ns),
68 Self::Binary(_) | Self::BorrowedBinary(_) | Self::Resource { .. } => {
69 Err(TransformError::TypeMismatch {
70 expected: "NodeSet",
71 got: "Binary",
72 })
73 }
74 }
75 }
76
77 pub fn into_binary(self) -> Result<Vec<u8>, TransformError> {
79 match self {
80 Self::Binary(b) => Ok(b),
81 Self::BorrowedBinary(b) => Ok(b.to_vec()),
82 Self::Resource { bytes, .. } => Ok(bytes.to_vec()),
83 Self::NodeSet(_) => Err(TransformError::TypeMismatch {
84 expected: "Binary",
85 got: "NodeSet",
86 }),
87 }
88 }
89}
90
91pub struct NodeSet<'a> {
100 pub(crate) projection: projection::Projection<'a, projection::NativeKeys>,
101}
102
103pub(crate) struct NodeSetMaterializationBudget {
104 remaining_owned_string_bytes: Cell<usize>,
105 max_entries: usize,
106 max_owned_string_bytes: usize,
107 max_cumulative_owned_string_bytes: usize,
108}
109
110impl Default for NodeSetMaterializationBudget {
111 fn default() -> Self {
112 Self {
113 remaining_owned_string_bytes: Cell::new(MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES),
114 max_entries: MAX_NODE_SET_ENTRIES,
115 max_owned_string_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
116 max_cumulative_owned_string_bytes: MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
117 }
118 }
119}
120
121impl NodeSetMaterializationBudget {
122 pub(crate) fn reserve_workspace(&self, bytes: usize) -> Result<(), TransformError> {
123 if bytes > self.max_owned_string_bytes {
124 return Err(transform_resource_limit(
125 crate::policy::resource_name::NODE_SET_OWNED_STRING_BYTES,
126 self.max_owned_string_bytes,
127 bytes,
128 ));
129 }
130 self.charge(bytes)
131 }
132
133 fn charge(&self, owned_string_bytes: usize) -> Result<(), TransformError> {
134 let remaining_before = self.remaining_owned_string_bytes.get();
135 let Some(remaining) = self
136 .remaining_owned_string_bytes
137 .get()
138 .checked_sub(owned_string_bytes)
139 else {
140 self.remaining_owned_string_bytes.set(0);
141 let consumed = self
142 .max_cumulative_owned_string_bytes
143 .saturating_sub(remaining_before);
144 return Err(transform_resource_limit(
145 crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
146 self.max_cumulative_owned_string_bytes,
147 consumed.saturating_add(owned_string_bytes),
148 ));
149 };
150 self.remaining_owned_string_bytes.set(remaining);
151 Ok(())
152 }
153
154 #[cfg(test)]
155 pub(crate) fn with_limit(limit: usize) -> Self {
156 Self {
157 remaining_owned_string_bytes: Cell::new(limit),
158 max_cumulative_owned_string_bytes: limit,
159 ..Self::default()
160 }
161 }
162
163 pub(crate) fn with_limits(
164 max_entries: usize,
165 max_owned_string_bytes: usize,
166 max_cumulative_owned_string_bytes: usize,
167 ) -> Self {
168 Self {
169 remaining_owned_string_bytes: Cell::new(max_cumulative_owned_string_bytes),
170 max_entries,
171 max_owned_string_bytes,
172 max_cumulative_owned_string_bytes,
173 }
174 }
175}
176
177impl<'a> NodeSet<'a> {
178 pub(crate) fn projected(
179 root: Node<'a, 'a>,
180 comments: bool,
181 budget: Option<&NodeSetMaterializationBudget>,
182 ) -> Result<Self, TransformError> {
183 let default;
184 let budget = match budget {
185 Some(budget) => budget,
186 None => {
187 default = NodeSetMaterializationBudget::default();
188 &default
189 }
190 };
191 Ok(Self {
192 projection: projection::Projection::subtree(
193 root,
194 comments,
195 budget,
196 projection::NativeKeys,
197 )?,
198 })
199 }
200
201 pub fn entire_document_without_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
206 Self::projected(doc.root(), false, None)
207 }
208 pub(crate) fn entire_document_without_comments_with_budget(
209 doc: &'a Document<'a>,
210 budget: &NodeSetMaterializationBudget,
211 ) -> Result<Self, TransformError> {
212 Self::projected(doc.root(), false, Some(budget))
213 }
214 pub(crate) fn entire_document_without_comments_from_view(
215 view: crate::DocumentView<'a>,
216 budget: Option<&NodeSetMaterializationBudget>,
217 ) -> Result<Self, TransformError> {
218 Self::projected(view.document().root(), false, budget)
219 }
220
221 pub fn entire_document_with_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
225 Self::projected(doc.root(), true, None)
226 }
227
228 pub fn subtree(element: Node<'a, 'a>) -> Result<Self, TransformError> {
232 Self::projected(element, true, None)
233 }
234 #[cfg(test)]
235 pub(crate) fn subtree_without_comments_with_budget(
236 element: Node<'a, 'a>,
237 budget: Option<&NodeSetMaterializationBudget>,
238 ) -> Result<Self, TransformError> {
239 Self::projected(element, false, budget)
240 }
241 #[cfg(test)]
242 pub(crate) fn subtree_with_budget(
243 element: Node<'a, 'a>,
244 budget: &NodeSetMaterializationBudget,
245 ) -> Result<Self, TransformError> {
246 Self::projected(element, true, Some(budget))
247 }
248
249 pub fn document(&self) -> &'a Document<'a> {
251 self.projection.document()
252 }
253 pub fn contains(&self, node: Node<'_, '_>) -> bool {
255 self.projection
256 .contains_tree(node)
257 .expect("native projection lookup is infallible")
258 }
259 pub fn exclude_subtree(&mut self, node: Node<'_, '_>) {
261 self.projection
262 .exclude_subtree(node)
263 .expect("native projection retention is infallible");
264 }
265 pub fn with_comments(&self) -> bool {
267 self.projection.with_comments()
268 }
269
270 #[cfg(test)]
271 pub(crate) fn empty(doc: &'a Document<'a>) -> Self {
272 Self {
273 projection: projection::Projection::empty(doc, projection::NativeKeys),
274 }
275 }
276 #[cfg(test)]
277 pub(crate) fn try_entire_document(doc: &'a Document<'a>) -> Result<Self, TransformError> {
278 Self::entire_document_with_comments(doc)
279 }
280 #[cfg(test)]
281 pub(crate) fn len(&self) -> usize {
282 self.projection.len()
283 }
284 #[cfg(test)]
285 pub(crate) fn insert_attribute_with_budget(
286 &mut self,
287 owner: Node<'_, '_>,
288 namespace: Option<&str>,
289 local: &str,
290 budget: &NodeSetMaterializationBudget,
291 ) -> Result<(), TransformError> {
292 self.projection
293 .insert_attribute(owner, namespace, local, Some(budget))
294 }
295 #[cfg(test)]
296 pub(crate) fn insert_namespace_with_budget(
297 &mut self,
298 owner: Node<'_, '_>,
299 prefix: &str,
300 uri: &str,
301 budget: &NodeSetMaterializationBudget,
302 ) -> Result<(), TransformError> {
303 self.projection
304 .insert_namespace(owner, prefix, uri, Some(budget))
305 }
306 pub(crate) fn ensure_subtree_materialization_fits_with_budget(
307 root: Node<'_, '_>,
308 comments: bool,
309 budget: &NodeSetMaterializationBudget,
310 ) -> Result<usize, TransformError> {
311 Ok(projection::measure(
312 root,
313 comments,
314 budget.max_entries,
315 budget.max_owned_string_bytes,
316 &projection::NativeKeys,
317 )?
318 .entries)
319 }
320}
321
322pub(super) fn charge_resource_bytes(
323 current: usize,
324 additional: usize,
325 max_bytes: usize,
326 resource: &'static str,
327) -> Result<usize, TransformError> {
328 let total = current
329 .checked_add(additional)
330 .ok_or_else(|| transform_resource_limit(resource, max_bytes, usize::MAX))?;
331 if total > max_bytes {
332 return Err(transform_resource_limit(resource, max_bytes, total));
333 }
334 Ok(total)
335}
336
337pub(crate) fn subtree_node_id_range<D: crate::xml::dom::storage::Storage>(
338 node: Node<'_, '_, D>,
339) -> RangeInclusive<u32> {
340 let last_id = node
341 .descendants()
342 .next_back()
343 .map_or(node.id(), |descendant| descendant.id());
344 node.id().get()..=last_id.get()
345}
346
347impl NodeSet<'_> {
348 pub(crate) fn contains_attribute(
349 &self,
350 owner: Node<'_, '_>,
351 namespace: Option<&str>,
352 local_name: &str,
353 ) -> bool {
354 self.projection
355 .contains_attribute(owner, namespace, local_name)
356 .expect("native projection lookup is infallible")
357 }
358
359 pub(crate) fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool {
360 self.projection
361 .contains_namespace(owner, prefix, uri)
362 .expect("native projection lookup is infallible")
363 }
364}
365
366impl NodeVisibility for NodeSet<'_> {
367 fn contains_node(&self, node: Node<'_, '_>) -> Result<bool, crate::c14n::C14nError> {
368 Ok(self.contains(node))
369 }
370 fn contains_attribute(
371 &self,
372 owner: Node<'_, '_>,
373 namespace: Option<&str>,
374 local_name: &str,
375 ) -> Result<bool, crate::c14n::C14nError> {
376 Ok(self.contains_attribute(owner, namespace, local_name))
377 }
378 fn contains_namespace(
379 &self,
380 owner: Node<'_, '_>,
381 prefix: &str,
382 uri: &str,
383 ) -> Result<bool, crate::c14n::C14nError> {
384 Ok(self.contains_namespace(owner, prefix, uri))
385 }
386 fn contains_attribute_at(
387 &self,
388 owner: Node<'_, '_>,
389 ordinal: usize,
390 _: crate::xml::dom::Attribute<'_>,
391 ) -> Result<bool, crate::c14n::C14nError> {
392 self.projection.contains_attribute_at(owner, ordinal)
393 }
394 fn contains_namespace_at(
395 &self,
396 owner: Node<'_, '_>,
397 ordinal: usize,
398 _: crate::xml::dom::Namespace<'_>,
399 ) -> Result<bool, crate::c14n::C14nError> {
400 self.projection.contains_namespace_at(owner, ordinal)
401 }
402}
403
404#[derive(Debug, thiserror::Error)]
406#[non_exhaustive]
407pub enum TransformError {
408 #[error("transform policy violation: {0}")]
410 Policy(#[from] crate::policy::PolicyViolation),
411
412 #[error("type mismatch: expected {expected}, got {got}")]
414 TypeMismatch {
415 expected: &'static str,
417 got: &'static str,
419 },
420
421 #[error("element not found by ID: {0}")]
423 ElementNotFound(String),
424
425 #[error("element not found by ID: {0}")]
427 AccountedElementNotFound(structured_asn1::memory::OwnedText),
428
429 #[error("unsupported URI: {0}")]
431 UnsupportedUri(String),
432
433 #[error("unsupported URI: {0}")]
435 AccountedUnsupportedUri(structured_asn1::memory::OwnedText),
436
437 #[error("unsupported URI: invalid same-document fragment")]
439 InvalidFragment,
440
441 #[error("unsupported transform: {0}")]
443 UnsupportedTransform(String),
444
445 #[error("unsupported transform: {0}")]
447 AccountedUnsupportedTransform(structured_asn1::memory::OwnedText),
448
449 #[error("C14N error: {0}")]
451 C14n(#[from] crate::c14n::C14nError),
452
453 #[error("base64 transform decode error: {0}")]
455 Base64(#[source] base64::DecodeSliceError),
456
457 #[error("XPath transform error: {0}")]
459 XPath(String),
460
461 #[error("XPath transform error: {0}")]
463 AccountedXPath(structured_asn1::memory::OwnedText),
464
465 #[error("OPC relationship transform error: {0}")]
467 Relationship(&'static str),
468
469 #[cfg(feature = "xslt")]
471 #[error("XSLT transform error: {0}")]
472 Xslt(String),
473
474 #[cfg(feature = "xslt")]
476 #[error("XSLT transform error: {0}")]
477 AccountedXslt(structured_asn1::memory::OwnedText),
478
479 #[error("XML transform input parse error: {0}")]
482 XmlParse(#[source] crate::document::XmlDocumentError),
483
484 #[error("XML transform encoding error: {0}")]
486 AccountedXmlEncoding(
487 #[source] crate::encoding::XmlEncodingError<structured_asn1::memory::OwnedText>,
488 ),
489
490 #[error("enveloped-signature transform: invalid Signature node for this document")]
493 CrossDocumentSignatureNode,
494
495 #[error("node-set projection: input node belongs to a different document")]
497 CrossDocumentNodeSetInput,
498}
499
500impl From<structured_asn1::memory::MemoryError> for TransformError {
501 fn from(error: structured_asn1::memory::MemoryError) -> Self {
502 Self::C14n(crate::c14n::C14nError::SharedExecution(error))
503 }
504}
505
506pub(crate) fn transform_resource_limit(
507 resource: &'static str,
508 maximum: usize,
509 actual: usize,
510) -> TransformError {
511 crate::policy::PolicyViolation::ResourceLimit {
512 resource,
513 maximum,
514 actual,
515 }
516 .into()
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use crate::c14n::{C14nAlgorithm, C14nMode, canonicalize_with_visibility};
523
524 #[test]
525 fn incremental_projection_enforces_aggregate_owned_string_policy() {
526 let document = Document::parse("<root a='1' bbb='2'/>").expect("fixed XML must parse");
529 let root = document.root_element();
530 let mut nodes = NodeSet::empty(&document);
531 let budget = NodeSetMaterializationBudget::with_limits(16, 3, 16);
532
533 nodes
534 .insert_attribute_with_budget(root, None, "a", &budget)
535 .expect("the first one-byte attribute name must fit");
536 let error = nodes
537 .insert_attribute_with_budget(root, None, "bbb", &budget)
538 .expect_err("aggregate projected names must exceed three bytes");
539
540 assert!(matches!(
541 error,
542 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
543 resource: "node-set owned string bytes",
544 maximum: 3,
545 actual: 4,
546 })
547 ));
548 }
549
550 #[test]
551 fn projected_attribute_and_namespace_share_one_budget_path() {
552 let document =
555 Document::parse("<root xmlns:p='u' a='1' b='2'/>").expect("fixed XML must parse");
556 let root = document.root_element();
557 let mut nodes = NodeSet::empty(&document);
558 let budget = NodeSetMaterializationBudget::with_limits(16, 16, 3);
559
560 nodes
561 .insert_namespace_with_budget(root, "p", "u", &budget)
562 .expect("two namespace bytes must fit");
563 nodes
564 .insert_namespace_with_budget(root, "p", "u", &budget)
565 .expect("a duplicate namespace must not consume budget twice");
566 nodes
567 .insert_attribute_with_budget(root, None, "a", &budget)
568 .expect("one remaining byte must admit an attribute");
569 let error = nodes
570 .insert_attribute_with_budget(root, None, "b", &budget)
571 .expect_err("distinct projected keys must share cumulative accounting");
572
573 assert!(matches!(
574 error,
575 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
576 resource: "cumulative node-set owned string bytes",
577 maximum: 3,
578 actual: 4,
579 })
580 ));
581 }
582
583 #[test]
584 fn denied_attribute_projection_does_not_insert_key() {
585 let document = Document::parse("<root a='1'/>").expect("fixed XML must parse");
588 let root = document.root_element();
589 let mut nodes = NodeSet::empty(&document);
590 let budget = NodeSetMaterializationBudget::with_limits(16, 0, 0);
591 let error = nodes
592 .insert_attribute_with_budget(root, None, "a", &budget)
593 .expect_err("the borrowed attribute name exceeds the zero-byte budget");
594
595 assert_eq!(nodes.len(), 0);
596 assert!(!nodes.contains_attribute(root, None, "a"));
597 assert!(matches!(
598 error,
599 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
600 resource: "node-set owned string bytes",
601 maximum: 0,
602 actual: 1,
603 })
604 ));
605 }
606
607 #[test]
608 fn denied_namespace_projection_does_not_insert_key() {
609 let document = Document::parse("<root xmlns:p='u'/>").expect("fixed XML must parse");
612 let root = document.root_element();
613 let mut nodes = NodeSet::empty(&document);
614 let budget = NodeSetMaterializationBudget::with_limits(16, 16, 0);
615 let error = nodes
616 .insert_namespace_with_budget(root, "p", "u", &budget)
617 .expect_err("borrowed namespace strings exceed the zero-byte budget");
618
619 assert_eq!(nodes.len(), 0);
620 assert!(!nodes.contains_namespace(root, "p", "u"));
621 assert!(matches!(
622 error,
623 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
624 resource: "cumulative node-set owned string bytes",
625 maximum: 0,
626 actual: 2,
627 })
628 ));
629 }
630
631 #[test]
632 fn document_without_comments_preserves_comment_policy() {
633 let document = Document::parse("<root><!-- excluded --><child/></root>")
636 .expect("fixed comment fixture must parse");
637 let nodes = NodeSet::entire_document_without_comments(&document)
638 .expect("fixed fixture must fit the node-set materialization budget");
639 let comment = document
640 .descendants()
641 .find(|node| node.is_comment())
642 .expect("fixed fixture contains one comment");
643
644 assert!(!nodes.contains(comment));
645 assert!(!nodes.with_comments());
646 }
647
648 #[test]
649 fn document_without_comments_preflights_the_materialized_set() {
650 let document = Document::parse("<root><!-- one --><child/><!-- two --></root>")
653 .expect("fixed comment fixture must parse");
654 let budget = NodeSetMaterializationBudget::with_limits(3, 1, 1);
655
656 let nodes = NodeSet::entire_document_without_comments_with_budget(&document, &budget)
657 .expect("the three materialized tree nodes must fit exactly");
658
659 assert_eq!(nodes.len(), 3);
660 assert!(
661 document
662 .descendants()
663 .filter(Node::is_comment)
664 .all(|comment| !nodes.contains(comment))
665 );
666 }
667
668 #[test]
669 fn bare_fragment_preflights_the_materialized_set_without_comments() {
670 let document =
673 Document::parse("<root><target><!-- one --><child/><!-- two --></target></root>")
674 .expect("fixed comment fixture must parse");
675 let target = document
676 .descendants()
677 .find(|node| node.has_tag_name("target"))
678 .expect("fixed fixture contains the selected target");
679 let budget = NodeSetMaterializationBudget::with_limits(2, 1, 1);
680
681 let nodes = NodeSet::subtree_without_comments_with_budget(target, Some(&budget))
682 .expect("the target and child must fit exactly");
683
684 assert_eq!(nodes.len(), 2);
685 assert!(!nodes.with_comments());
686 }
687
688 #[test]
689 fn excluding_disjoint_oversized_subtree_only_scans_input_keys() {
690 let xml = format!(
694 "<root><target Id=\"selected\"><child/></target><Signature>{}</Signature></root>",
695 "<Object/>".repeat(129)
696 );
697 let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
698 let target = document
699 .descendants()
700 .find(|node| node.attribute("Id") == Some("selected"))
701 .expect("fixed fixture contains the selected subtree");
702 let signature = document
703 .descendants()
704 .find(|node| node.has_tag_name("Signature"))
705 .expect("fixed fixture contains the excluded Signature subtree");
706 let budget = NodeSetMaterializationBudget::with_limits(3, 8, 8);
707 let mut nodes = NodeSet::subtree_with_budget(target, &budget)
708 .expect("small selected subtree must fit the materialization budget");
709 let entries_before = nodes.len();
710
711 nodes.exclude_subtree(signature);
712
713 assert_eq!(nodes.len(), entries_before);
714 assert!(nodes.contains(target));
715 assert!(
716 nodes.contains(
717 target
718 .first_element_child()
719 .expect("fixed target subtree contains a child")
720 )
721 );
722 }
723
724 #[test]
725 fn materialization_rejects_inherited_namespace_byte_amplification() {
726 let namespace_uri = "x".repeat(8_192);
730 let xml = format!(
731 "<root xmlns:amplified=\"{namespace_uri}\">{}</root>",
732 "<child/>".repeat(1_025)
733 );
734 let document = Document::parse(&xml).expect("fixed namespace fixture must parse");
735
736 let error = NodeSet::entire_document_without_comments(&document)
737 .err()
738 .expect("amplified namespace bytes must exceed the materialization budget");
739
740 assert!(matches!(
741 error,
742 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
743 resource: "node-set owned string bytes",
744 ..
745 })
746 ));
747 }
748
749 #[test]
750 fn subtree_node_id_range_contains_only_the_selected_subtree() {
751 let document = Document::parse(
755 "<root><before/><excluded xmlns:gone=\"urn:gone\" a=\"1\"><child/></excluded><after/></root>",
756 )
757 .expect("fixed subtree range fixture must parse");
758 let excluded = document
759 .descendants()
760 .find(|node| node.has_tag_name("excluded"))
761 .expect("fixed fixture contains the excluded subtree");
762 let range = subtree_node_id_range(excluded);
763 let before = document
764 .descendants()
765 .find(|node| node.has_tag_name("before"))
766 .expect("fixed fixture contains the preceding sibling");
767 let child = excluded
768 .first_element_child()
769 .expect("fixed fixture contains an excluded child");
770 let after = document
771 .descendants()
772 .find(|node| node.has_tag_name("after"))
773 .expect("fixed fixture contains the following sibling");
774
775 assert!(!range.contains(&before.id().get()));
776 assert!(range.contains(&excluded.id().get()));
777 assert!(range.contains(&child.id().get()));
778 assert!(!range.contains(&after.id().get()));
779
780 let mut nodes = NodeSet::entire_document_with_comments(&document)
781 .expect("fixed fixture must fit the node-set materialization budget");
782 nodes.exclude_subtree(excluded);
783
784 assert!(nodes.contains(before));
785 assert!(!nodes.contains(excluded));
786 assert!(!nodes.contains(child));
787 assert!(!nodes.contains_attribute(excluded, None, "a"));
788 assert!(!nodes.contains_namespace(excluded, "gone", "urn:gone"));
789 assert!(nodes.contains(after));
790 }
791
792 #[test]
793 fn excluding_subtree_removes_trailing_text_and_comments_from_canonical_output() {
794 let document = Document::parse(
798 "<root><before/>keep-before<excluded><child/>drop-text<!--drop-comment--></excluded>keep-after<after/></root>",
799 )
800 .expect("fixed trailing-node fixture must parse");
801 let excluded = document
802 .descendants()
803 .find(|node| node.has_tag_name("excluded"))
804 .expect("fixed fixture contains the excluded subtree");
805 let trailing_text = excluded
806 .children()
807 .find(|node| node.is_text())
808 .expect("fixed fixture contains trailing text");
809 let trailing_comment = excluded
810 .children()
811 .find(|node| node.is_comment())
812 .expect("fixed fixture contains a trailing comment");
813 let mut nodes = NodeSet::entire_document_with_comments(&document)
814 .expect("fixed fixture must fit the node-set materialization budget");
815
816 nodes.exclude_subtree(excluded);
817
818 assert!(!nodes.contains(trailing_text));
819 assert!(!nodes.contains(trailing_comment));
820 let mut output = Vec::new();
821 canonicalize_with_visibility(
822 &document,
823 Some(&nodes),
824 &C14nAlgorithm::new(C14nMode::Inclusive1_0, true),
825 &mut output,
826 )
827 .expect("the retained node set must canonicalize");
828 assert_eq!(
829 output,
830 b"<root><before></before>keep-beforekeep-after<after></after></root>"
831 );
832 }
833}