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