1use std::cell::Cell;
9use std::collections::HashSet;
10use std::ops::RangeInclusive;
11
12use crate::xml::dom::{Document, Node, NodeId};
13
14const MAX_NODE_SET_ENTRIES: usize = crate::hard_limits::NODE_SET_ENTRY_CEILING;
15const MAX_NODE_SET_OWNED_STRING_BYTES: usize =
16 crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING;
17const MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: usize =
18 crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING;
19
20use crate::c14n::NodeVisibility;
21
22pub enum TransformData<'a> {
31 NodeSet(NodeSet<'a>),
33 Binary(Vec<u8>),
35}
36
37impl std::fmt::Debug for TransformData<'_> {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 Self::NodeSet(_) => f.debug_tuple("NodeSet").field(&"...").finish(),
41 Self::Binary(b) => f.debug_tuple("Binary").field(&b.len()).finish(),
42 }
43 }
44}
45
46impl<'a> TransformData<'a> {
47 pub fn into_node_set(self) -> Result<NodeSet<'a>, TransformError> {
49 match self {
50 Self::NodeSet(ns) => Ok(ns),
51 Self::Binary(_) => Err(TransformError::TypeMismatch {
52 expected: "NodeSet",
53 got: "Binary",
54 }),
55 }
56 }
57
58 pub fn into_binary(self) -> Result<Vec<u8>, TransformError> {
60 match self {
61 Self::Binary(b) => Ok(b),
62 Self::NodeSet(_) => Err(TransformError::TypeMismatch {
63 expected: "Binary",
64 got: "NodeSet",
65 }),
66 }
67 }
68}
69
70pub struct NodeSet<'a> {
79 doc: &'a Document<'a>,
81 nodes: HashSet<XmlNodeKey>,
82 owned_string_bytes: usize,
83 with_comments: bool,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89enum XmlNodeKey {
90 Tree(NodeId),
91 Attribute {
92 owner: NodeId,
93 namespace: Option<String>,
94 local_name: String,
95 },
96 Namespace {
97 owner: NodeId,
98 prefix: String,
99 uri: String,
100 },
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 XmlNodeKey {
178 fn owner_id(&self) -> NodeId {
179 match self {
180 Self::Tree(id) => *id,
181 Self::Attribute { owner, .. } | Self::Namespace { owner, .. } => *owner,
182 }
183 }
184
185 fn owned_string_bytes(&self) -> usize {
186 match self {
187 Self::Tree(_) => 0,
188 Self::Attribute {
189 namespace,
190 local_name,
191 ..
192 } => namespace
193 .as_ref()
194 .map_or(0, String::len)
195 .saturating_add(local_name.len()),
196 Self::Namespace { prefix, uri, .. } => prefix.len().saturating_add(uri.len()),
197 }
198 }
199}
200
201impl<'a> NodeSet<'a> {
202 pub fn entire_document_without_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
212 Self::ensure_subtree_materialization_fits(doc.root(), false)?;
213 Ok(Self::collect_document(doc, false))
214 }
215
216 pub(crate) fn entire_document_without_comments_with_budget(
217 doc: &'a Document<'a>,
218 budget: &NodeSetMaterializationBudget,
219 ) -> Result<Self, TransformError> {
220 Self::charge_subtree_materialization(doc.root(), false, budget)?;
221 Ok(Self::collect_document(doc, false))
222 }
223
224 pub(crate) fn entire_document_without_comments_from_view(
225 view: crate::DocumentView<'a>,
226 budget: Option<&NodeSetMaterializationBudget>,
227 ) -> Result<Self, TransformError> {
228 let set = match budget {
229 Some(budget) => {
230 Self::entire_document_without_comments_with_budget(view.document(), budget)?
231 }
232 None => Self::entire_document_without_comments(view.document())?,
233 };
234 Ok(set)
235 }
236
237 pub fn entire_document_with_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
246 Self::ensure_subtree_materialization_fits(doc.root(), true)?;
247 Ok(Self::collect_document(doc, true))
248 }
249
250 pub(crate) fn entire_document_with_comments_with_budget(
251 doc: &'a Document<'a>,
252 budget: &NodeSetMaterializationBudget,
253 ) -> Result<Self, TransformError> {
254 Self::charge_subtree_materialization(doc.root(), true, budget)?;
255 Ok(Self::collect_document(doc, true))
256 }
257
258 pub(crate) fn entire_document_with_comments_from_view(
259 view: crate::DocumentView<'a>,
260 budget: Option<&NodeSetMaterializationBudget>,
261 ) -> Result<Self, TransformError> {
262 let set = match budget {
263 Some(budget) => {
264 Self::entire_document_with_comments_with_budget(view.document(), budget)?
265 }
266 None => Self::entire_document_with_comments(view.document())?,
267 };
268 Ok(set)
269 }
270
271 pub fn subtree(element: Node<'a, 'a>) -> Result<Self, TransformError> {
280 Self::ensure_subtree_materialization_fits(element, true)?;
281 Ok(Self::collect_subtree(element))
282 }
283
284 pub(crate) fn subtree_without_comments_with_budget(
287 element: Node<'a, 'a>,
288 budget: Option<&NodeSetMaterializationBudget>,
289 ) -> Result<Self, TransformError> {
290 match budget {
291 Some(budget) => Self::charge_subtree_materialization(element, false, budget)?,
292 None => {
293 Self::ensure_subtree_materialization_fits(element, false)?;
294 }
295 }
296 let mut set = Self {
297 doc: element.document(),
298 nodes: HashSet::new(),
299 owned_string_bytes: 0,
300 with_comments: false,
301 };
302 for node in element.descendants().filter(|node| !node.is_comment()) {
303 set.insert_node(node);
304 if node.is_element() {
305 for attribute in node.attributes() {
306 set.insert_attribute(node, attribute.namespace(), attribute.name());
307 }
308 for namespace in node.namespaces() {
309 set.insert_namespace(node, namespace.name().unwrap_or(""), namespace.uri());
310 }
311 }
312 }
313 Ok(set)
314 }
315
316 pub(crate) fn subtree_with_budget(
317 element: Node<'a, 'a>,
318 budget: &NodeSetMaterializationBudget,
319 ) -> Result<Self, TransformError> {
320 Self::charge_subtree_materialization(element, true, budget)?;
321 Ok(Self::collect_subtree(element))
322 }
323
324 pub(crate) fn subtree_from_view(
325 view: crate::DocumentView<'a>,
326 element: Node<'a, 'a>,
327 with_comments: bool,
328 budget: Option<&NodeSetMaterializationBudget>,
329 ) -> Result<Self, TransformError> {
330 if !std::ptr::eq(element.document() as *const _, view.document() as *const _) {
331 return Err(TransformError::CrossDocumentNodeSetInput);
332 }
333 let set = if with_comments {
334 match budget {
335 Some(budget) => Self::subtree_with_budget(element, budget)?,
336 None => Self::subtree(element)?,
337 }
338 } else {
339 Self::subtree_without_comments_with_budget(element, budget)?
340 };
341 Ok(set)
342 }
343
344 fn collect_subtree(element: Node<'a, 'a>) -> Self {
345 let mut set = Self {
346 doc: element.document(),
347 nodes: HashSet::new(),
348 owned_string_bytes: 0,
349 with_comments: true,
350 };
351 set.insert_subtree(element);
352 set
353 }
354
355 pub fn document(&self) -> &'a Document<'a> {
357 self.doc
358 }
359
360 pub fn contains(&self, node: Node<'_, '_>) -> bool {
365 if !std::ptr::eq(node.document() as *const _, self.doc as *const _) {
369 return false;
370 }
371
372 self.nodes.contains(&XmlNodeKey::Tree(node.id()))
373 }
374
375 pub fn exclude_subtree(&mut self, node: Node<'_, '_>) {
379 if !std::ptr::eq(node.document() as *const _, self.doc as *const _) {
381 return;
382 }
383 let excluded_ids = subtree_node_id_range(node);
384 self.nodes
389 .retain(|key| !excluded_ids.contains(&key.owner_id().get()));
390 self.refresh_owned_string_bytes();
391 }
392
393 pub fn with_comments(&self) -> bool {
395 self.with_comments
396 }
397
398 pub(crate) fn empty(doc: &'a Document<'a>) -> Self {
399 Self {
400 doc,
401 nodes: HashSet::new(),
402 owned_string_bytes: 0,
403 with_comments: false,
404 }
405 }
406
407 #[cfg(test)]
408 pub(crate) fn try_entire_document(doc: &'a Document<'a>) -> Result<Self, TransformError> {
409 Self::entire_document_with_comments(doc)
410 }
411
412 pub(crate) fn try_entire_document_with_budget(
413 doc: &'a Document<'a>,
414 budget: &NodeSetMaterializationBudget,
415 ) -> Result<Self, TransformError> {
416 Self::entire_document_with_comments_with_budget(doc, budget)
417 }
418
419 pub(crate) fn len(&self) -> usize {
420 self.nodes.len()
421 }
422
423 pub(crate) fn insert_node(&mut self, node: Node<'_, '_>) {
424 if self.owns(node) {
425 self.with_comments |= node.is_comment();
426 self.nodes.insert(XmlNodeKey::Tree(node.id()));
427 }
428 }
429
430 pub(crate) fn insert_attribute(
431 &mut self,
432 owner: Node<'_, '_>,
433 namespace: Option<&str>,
434 local_name: &str,
435 ) {
436 if self.owns(owner) {
437 let key = XmlNodeKey::Attribute {
438 owner: owner.id(),
439 namespace: namespace.map(str::to_owned),
440 local_name: local_name.to_owned(),
441 };
442 if self.nodes.insert(key) {
443 self.owned_string_bytes = self
444 .owned_string_bytes
445 .saturating_add(namespace.map_or(0, str::len))
446 .saturating_add(local_name.len());
447 }
448 }
449 }
450
451 pub(crate) fn insert_attribute_with_budget(
452 &mut self,
453 owner: Node<'_, '_>,
454 namespace: Option<&str>,
455 local_name: &str,
456 budget: &NodeSetMaterializationBudget,
457 ) -> Result<(), TransformError> {
458 if self.owns(owner) {
459 let owner_id = owner.id();
460 let additional_bytes = namespace.map_or(0, str::len).checked_add(local_name.len());
461 self.insert_projected_key_with_budget(
462 additional_bytes,
463 |key| {
464 matches!(
465 key,
466 XmlNodeKey::Attribute {
467 owner,
468 namespace: stored_namespace,
469 local_name: stored_local_name,
470 } if *owner == owner_id
471 && stored_namespace.as_deref() == namespace
472 && stored_local_name == local_name
473 )
474 },
475 || XmlNodeKey::Attribute {
476 owner: owner_id,
477 namespace: namespace.map(str::to_owned),
478 local_name: local_name.to_owned(),
479 },
480 budget,
481 )?;
482 }
483 Ok(())
484 }
485
486 pub(crate) fn insert_namespace(&mut self, owner: Node<'_, '_>, prefix: &str, uri: &str) {
487 if self.owns(owner) {
488 let key = XmlNodeKey::Namespace {
489 owner: owner.id(),
490 prefix: prefix.to_owned(),
491 uri: uri.to_owned(),
492 };
493 if self.nodes.insert(key) {
494 self.owned_string_bytes = self
495 .owned_string_bytes
496 .saturating_add(prefix.len())
497 .saturating_add(uri.len());
498 }
499 }
500 }
501
502 pub(crate) fn insert_namespace_with_budget(
503 &mut self,
504 owner: Node<'_, '_>,
505 prefix: &str,
506 uri: &str,
507 budget: &NodeSetMaterializationBudget,
508 ) -> Result<(), TransformError> {
509 if self.owns(owner) {
510 let owner_id = owner.id();
511 self.insert_projected_key_with_budget(
512 prefix.len().checked_add(uri.len()),
513 |key| {
514 matches!(
515 key,
516 XmlNodeKey::Namespace {
517 owner,
518 prefix: stored_prefix,
519 uri: stored_uri,
520 } if *owner == owner_id && stored_prefix == prefix && stored_uri == uri
521 )
522 },
523 || XmlNodeKey::Namespace {
524 owner: owner_id,
525 prefix: prefix.to_owned(),
526 uri: uri.to_owned(),
527 },
528 budget,
529 )?;
530 }
531 Ok(())
532 }
533
534 fn insert_projected_key_with_budget<D, F>(
535 &mut self,
536 additional_bytes: Option<usize>,
537 is_duplicate: D,
538 build_key: F,
539 budget: &NodeSetMaterializationBudget,
540 ) -> Result<(), TransformError>
541 where
542 D: Fn(&XmlNodeKey) -> bool,
543 F: FnOnce() -> XmlNodeKey,
544 {
545 let (additional_bytes, preflight_error) = match additional_bytes {
546 None => (
547 0,
548 Some(transform_resource_limit(
549 crate::policy::resource_name::NODE_SET_OWNED_STRING_BYTES,
550 budget.max_owned_string_bytes,
551 usize::MAX,
552 )),
553 ),
554 Some(additional_bytes) => {
555 let total_bytes = self.owned_string_bytes.saturating_add(additional_bytes);
556 let error = if total_bytes > budget.max_owned_string_bytes {
557 Some(transform_resource_limit(
558 crate::policy::resource_name::NODE_SET_OWNED_STRING_BYTES,
559 budget.max_owned_string_bytes,
560 total_bytes,
561 ))
562 } else if self.nodes.len() >= budget.max_entries {
563 Some(transform_resource_limit(
564 crate::policy::resource_name::NODE_SET_ENTRIES,
565 budget.max_entries,
566 self.nodes.len().saturating_add(1),
567 ))
568 } else {
569 let remaining = budget.remaining_owned_string_bytes.get();
570 remaining.checked_sub(additional_bytes).is_none().then(|| {
571 let consumed = budget
572 .max_cumulative_owned_string_bytes
573 .saturating_sub(remaining);
574 transform_resource_limit(
575 crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
576 budget.max_cumulative_owned_string_bytes,
577 consumed.saturating_add(additional_bytes),
578 )
579 })
580 };
581 (additional_bytes, error)
582 }
583 };
584 if let Some(error) = preflight_error {
585 if self.nodes.iter().any(is_duplicate) {
588 return Ok(());
589 }
590 return Err(error);
591 }
592
593 let total_bytes = self.owned_string_bytes.saturating_add(additional_bytes);
594 let key = build_key();
595 if self.nodes.contains(&key) {
596 return Ok(());
597 }
598 budget.charge(additional_bytes)?;
599 let inserted = self.nodes.insert(key);
600 debug_assert!(inserted, "the duplicate key was checked before insertion");
601 if inserted {
602 self.owned_string_bytes = total_bytes;
603 }
604 Ok(())
605 }
606
607 pub(crate) fn insert_subtree(&mut self, root: Node<'_, '_>) {
608 if !self.owns(root) {
609 return;
610 }
611 let mut stack = vec![root];
612 while let Some(node) = stack.pop() {
613 self.insert_node(node);
614 if node.is_element() {
615 for attribute in node.attributes() {
616 self.insert_attribute(node, attribute.namespace(), attribute.name());
617 }
618 for namespace in node.namespaces() {
619 self.insert_namespace(node, namespace.name().unwrap_or(""), namespace.uri());
620 }
621 }
622 stack.extend(node.children());
623 }
624 }
625
626 pub(crate) fn intersect_with(&mut self, other: &Self) {
627 if !std::ptr::eq(self.doc as *const _, other.doc as *const _) {
628 self.nodes.clear();
629 self.owned_string_bytes = 0;
630 self.with_comments = false;
631 return;
632 }
633 self.nodes.retain(|key| other.nodes.contains(key));
634 self.refresh_owned_string_bytes();
635 self.with_comments &= other.with_comments;
636 }
637
638 pub(crate) fn subtract(&mut self, other: &Self) {
639 if std::ptr::eq(self.doc as *const _, other.doc as *const _) {
640 self.nodes.retain(|key| !other.nodes.contains(key));
641 self.refresh_owned_string_bytes();
642 }
643 }
644
645 pub(crate) fn union_with_budget(
646 &mut self,
647 other: &Self,
648 budget: &NodeSetMaterializationBudget,
649 ) -> Result<(), TransformError> {
650 if std::ptr::eq(self.doc as *const _, other.doc as *const _) {
651 for key in &other.nodes {
652 if self.nodes.contains(key) {
653 continue;
654 }
655 let owned_string_bytes = key.owned_string_bytes();
656 let total_bytes = self.owned_string_bytes.saturating_add(owned_string_bytes);
657 if total_bytes > budget.max_owned_string_bytes {
658 return Err(transform_resource_limit(
659 crate::policy::resource_name::NODE_SET_OWNED_STRING_BYTES,
660 budget.max_owned_string_bytes,
661 total_bytes,
662 ));
663 }
664 if self.nodes.len() >= budget.max_entries {
665 return Err(transform_resource_limit(
666 crate::policy::resource_name::NODE_SET_ENTRIES,
667 budget.max_entries,
668 self.nodes.len().saturating_add(1),
669 ));
670 }
671 budget.charge(owned_string_bytes)?;
672 self.nodes.insert(key.clone());
673 self.owned_string_bytes = total_bytes;
674 }
675 self.with_comments |= other.with_comments;
676 }
677 Ok(())
678 }
679
680 fn collect_document(doc: &'a Document<'a>, with_comments: bool) -> Self {
681 let mut set = Self::empty(doc);
682 set.insert_subtree(doc.root());
683 if !with_comments {
684 set.nodes.retain(|key| match key {
685 XmlNodeKey::Tree(id) => !doc.get_node(*id).is_some_and(|node| node.is_comment()),
686 _ => true,
687 });
688 }
689 set.with_comments = with_comments;
690 set
691 }
692
693 fn refresh_owned_string_bytes(&mut self) {
694 self.owned_string_bytes = self.nodes.iter().fold(0_usize, |total, key| {
695 total.saturating_add(key.owned_string_bytes())
696 });
697 }
698
699 pub(crate) fn ensure_subtree_materialization_fits(
700 root: Node<'_, '_>,
701 with_comments: bool,
702 ) -> Result<usize, TransformError> {
703 Ok(Self::subtree_materialization(root, with_comments)?.entries)
704 }
705
706 pub(crate) fn ensure_subtree_materialization_fits_with_budget(
707 root: Node<'_, '_>,
708 with_comments: bool,
709 budget: &NodeSetMaterializationBudget,
710 ) -> Result<usize, TransformError> {
711 Ok(Self::subtree_materialization_with_limits(
712 root,
713 with_comments,
714 budget.max_entries,
715 budget.max_owned_string_bytes,
716 )?
717 .entries)
718 }
719
720 fn charge_subtree_materialization(
721 root: Node<'_, '_>,
722 with_comments: bool,
723 budget: &NodeSetMaterializationBudget,
724 ) -> Result<(), TransformError> {
725 let materialization = Self::subtree_materialization_with_limits(
726 root,
727 with_comments,
728 budget.max_entries,
729 budget.max_owned_string_bytes,
730 )?;
731 budget.charge(materialization.owned_string_bytes)
732 }
733
734 fn subtree_materialization(
735 root: Node<'_, '_>,
736 with_comments: bool,
737 ) -> Result<NodeSetMaterialization, TransformError> {
738 Self::subtree_materialization_with_limits(
739 root,
740 with_comments,
741 MAX_NODE_SET_ENTRIES,
742 MAX_NODE_SET_OWNED_STRING_BYTES,
743 )
744 }
745
746 fn subtree_materialization_with_limits(
747 root: Node<'_, '_>,
748 with_comments: bool,
749 max_entries: usize,
750 max_owned_string_bytes: usize,
751 ) -> Result<NodeSetMaterialization, TransformError> {
752 let mut entries = 0_usize;
753 let mut owned_string_bytes = 0_usize;
754 let mut stack = vec![root];
755 while let Some(node) = stack.pop() {
756 if node.is_comment() && !with_comments {
757 continue;
758 }
759 let projected = if node.is_element() {
760 for attribute in node.attributes() {
761 owned_string_bytes = charge_node_set_string_bytes(
762 owned_string_bytes,
763 attribute.namespace().map_or(0, str::len),
764 max_owned_string_bytes,
765 )?;
766 owned_string_bytes = charge_node_set_string_bytes(
767 owned_string_bytes,
768 attribute.name().len(),
769 max_owned_string_bytes,
770 )?;
771 }
772 for namespace in node.namespaces() {
773 owned_string_bytes = charge_node_set_string_bytes(
774 owned_string_bytes,
775 namespace.name().map_or(0, str::len),
776 max_owned_string_bytes,
777 )?;
778 owned_string_bytes = charge_node_set_string_bytes(
779 owned_string_bytes,
780 namespace.uri().len(),
781 max_owned_string_bytes,
782 )?;
783 }
784 1_usize
785 .checked_add(node.attributes().len())
786 .and_then(|count| count.checked_add(node.namespaces().len()))
787 } else {
788 Some(1)
789 }
790 .ok_or_else(|| {
791 transform_resource_limit(
792 crate::policy::resource_name::NODE_SET_ENTRIES,
793 max_entries,
794 usize::MAX,
795 )
796 })?;
797 entries = entries.checked_add(projected).ok_or_else(|| {
798 transform_resource_limit(
799 crate::policy::resource_name::NODE_SET_ENTRIES,
800 max_entries,
801 usize::MAX,
802 )
803 })?;
804 if entries > max_entries {
805 return Err(transform_resource_limit(
806 crate::policy::resource_name::NODE_SET_ENTRIES,
807 max_entries,
808 entries,
809 ));
810 }
811 stack.extend(node.children());
812 }
813 Ok(NodeSetMaterialization {
814 entries,
815 owned_string_bytes,
816 })
817 }
818
819 fn owns(&self, node: Node<'_, '_>) -> bool {
820 std::ptr::eq(node.document() as *const _, self.doc as *const _)
821 }
822}
823
824struct NodeSetMaterialization {
825 entries: usize,
826 owned_string_bytes: usize,
827}
828
829fn charge_node_set_string_bytes(
830 current: usize,
831 additional: usize,
832 max_bytes: usize,
833) -> Result<usize, TransformError> {
834 charge_resource_bytes(
835 current,
836 additional,
837 max_bytes,
838 crate::policy::resource_name::NODE_SET_OWNED_STRING_BYTES,
839 )
840}
841
842pub(super) fn charge_resource_bytes(
843 current: usize,
844 additional: usize,
845 max_bytes: usize,
846 resource: &'static str,
847) -> Result<usize, TransformError> {
848 let total = current
849 .checked_add(additional)
850 .ok_or_else(|| transform_resource_limit(resource, max_bytes, usize::MAX))?;
851 if total > max_bytes {
852 return Err(transform_resource_limit(resource, max_bytes, total));
853 }
854 Ok(total)
855}
856
857pub(crate) fn subtree_node_id_range(node: Node<'_, '_>) -> RangeInclusive<u32> {
858 let last_id = node
859 .descendants()
860 .next_back()
861 .map_or(node.id(), |descendant| descendant.id());
862 node.id().get()..=last_id.get()
863}
864
865impl NodeVisibility for NodeSet<'_> {
866 fn contains_node(&self, node: Node<'_, '_>) -> bool {
867 self.contains(node)
868 }
869
870 fn contains_attribute(
871 &self,
872 owner: Node<'_, '_>,
873 namespace: Option<&str>,
874 local_name: &str,
875 ) -> bool {
876 self.owns(owner)
877 && self.nodes.contains(&XmlNodeKey::Attribute {
878 owner: owner.id(),
879 namespace: namespace.map(str::to_owned),
880 local_name: local_name.to_owned(),
881 })
882 }
883
884 fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool {
885 self.owns(owner)
886 && self.nodes.contains(&XmlNodeKey::Namespace {
887 owner: owner.id(),
888 prefix: prefix.to_owned(),
889 uri: uri.to_owned(),
890 })
891 }
892}
893
894#[derive(Debug, thiserror::Error)]
896#[non_exhaustive]
897pub enum TransformError {
898 #[error("transform policy violation: {0}")]
900 Policy(#[from] crate::policy::PolicyViolation),
901
902 #[error("type mismatch: expected {expected}, got {got}")]
904 TypeMismatch {
905 expected: &'static str,
907 got: &'static str,
909 },
910
911 #[error("element not found by ID: {0}")]
913 ElementNotFound(String),
914
915 #[error("unsupported URI: {0}")]
917 UnsupportedUri(String),
918
919 #[error("unsupported transform: {0}")]
921 UnsupportedTransform(String),
922
923 #[error("C14N error: {0}")]
925 C14n(#[from] crate::c14n::C14nError),
926
927 #[error("base64 transform decode error: {0}")]
929 Base64(String),
930
931 #[error("XPath transform error: {0}")]
933 XPath(String),
934
935 #[error("OPC relationship transform error: {0}")]
937 Relationship(String),
938
939 #[error("XML transform input parse error: {0}")]
942 XmlParse(String),
943
944 #[error("enveloped-signature transform: invalid Signature node for this document")]
947 CrossDocumentSignatureNode,
948
949 #[error("node-set projection: input node belongs to a different document")]
951 CrossDocumentNodeSetInput,
952}
953
954pub(crate) fn transform_resource_limit(
955 resource: &'static str,
956 maximum: usize,
957 actual: usize,
958) -> TransformError {
959 crate::policy::PolicyViolation::ResourceLimit {
960 resource,
961 maximum,
962 actual,
963 }
964 .into()
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::c14n::{C14nAlgorithm, C14nMode, canonicalize_with_visibility};
971
972 #[test]
973 fn incremental_projection_enforces_aggregate_owned_string_policy() {
974 let document = Document::parse("<root/>").expect("fixed XML must parse");
977 let root = document.root_element();
978 let mut nodes = NodeSet::empty(&document);
979 let budget = NodeSetMaterializationBudget::with_limits(16, 3, 16);
980
981 nodes
982 .insert_attribute_with_budget(root, None, "a", &budget)
983 .expect("the first one-byte attribute name must fit");
984 let error = nodes
985 .insert_attribute_with_budget(root, None, "bbb", &budget)
986 .expect_err("aggregate projected names must exceed three bytes");
987
988 assert!(matches!(
989 error,
990 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
991 resource: "node-set owned string bytes",
992 maximum: 3,
993 actual: 4,
994 })
995 ));
996 }
997
998 #[test]
999 fn projected_attribute_and_namespace_share_one_budget_path() {
1000 let document = Document::parse("<root/>").expect("fixed XML must parse");
1003 let root = document.root_element();
1004 let mut nodes = NodeSet::empty(&document);
1005 let budget = NodeSetMaterializationBudget::with_limits(16, 16, 3);
1006
1007 nodes
1008 .insert_namespace_with_budget(root, "p", "u", &budget)
1009 .expect("two namespace bytes must fit");
1010 nodes
1011 .insert_namespace_with_budget(root, "p", "u", &budget)
1012 .expect("a duplicate namespace must not consume budget twice");
1013 nodes
1014 .insert_attribute_with_budget(root, None, "a", &budget)
1015 .expect("one remaining byte must admit an attribute");
1016 let error = nodes
1017 .insert_attribute_with_budget(root, None, "b", &budget)
1018 .expect_err("distinct projected keys must share cumulative accounting");
1019
1020 assert!(matches!(
1021 error,
1022 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1023 resource: "cumulative node-set owned string bytes",
1024 maximum: 3,
1025 actual: 4,
1026 })
1027 ));
1028 }
1029
1030 #[test]
1031 fn denied_attribute_projection_does_not_construct_owned_key() {
1032 let document = Document::parse("<root/>").expect("fixed XML must parse");
1035 let root = document.root_element();
1036 let mut nodes = NodeSet::empty(&document);
1037 let budget = NodeSetMaterializationBudget::with_limits(16, 0, 0);
1038 let constructed = Cell::new(false);
1039
1040 let error = nodes
1041 .insert_projected_key_with_budget(
1042 Some(1),
1043 |_| false,
1044 || {
1045 constructed.set(true);
1046 XmlNodeKey::Attribute {
1047 owner: root.id(),
1048 namespace: None,
1049 local_name: "a".to_owned(),
1050 }
1051 },
1052 &budget,
1053 )
1054 .expect_err("the borrowed attribute name exceeds the zero-byte budget");
1055
1056 assert!(!constructed.get(), "denied names must not be cloned");
1057 assert!(matches!(
1058 error,
1059 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1060 resource: "node-set owned string bytes",
1061 maximum: 0,
1062 actual: 1,
1063 })
1064 ));
1065 }
1066
1067 #[test]
1068 fn denied_namespace_projection_does_not_construct_owned_key() {
1069 let document = Document::parse("<root/>").expect("fixed XML must parse");
1072 let root = document.root_element();
1073 let mut nodes = NodeSet::empty(&document);
1074 let budget = NodeSetMaterializationBudget::with_limits(16, 16, 0);
1075 let constructed = Cell::new(false);
1076
1077 let error = nodes
1078 .insert_projected_key_with_budget(
1079 Some(2),
1080 |_| false,
1081 || {
1082 constructed.set(true);
1083 XmlNodeKey::Namespace {
1084 owner: root.id(),
1085 prefix: "p".to_owned(),
1086 uri: "u".to_owned(),
1087 }
1088 },
1089 &budget,
1090 )
1091 .expect_err("borrowed namespace strings exceed the zero-byte budget");
1092
1093 assert!(
1094 !constructed.get(),
1095 "denied namespace strings must not be cloned"
1096 );
1097 assert!(matches!(
1098 error,
1099 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1100 resource: "cumulative node-set owned string bytes",
1101 maximum: 0,
1102 actual: 2,
1103 })
1104 ));
1105 }
1106
1107 #[test]
1108 fn document_without_comments_preserves_comment_policy() {
1109 let document = Document::parse("<root><!-- excluded --><child/></root>")
1112 .expect("fixed comment fixture must parse");
1113 let nodes = NodeSet::entire_document_without_comments(&document)
1114 .expect("fixed fixture must fit the node-set materialization budget");
1115 let comment = document
1116 .descendants()
1117 .find(|node| node.is_comment())
1118 .expect("fixed fixture contains one comment");
1119
1120 assert!(!nodes.contains(comment));
1121 assert!(!nodes.with_comments());
1122 }
1123
1124 #[test]
1125 fn document_without_comments_preflights_the_materialized_set() {
1126 let document = Document::parse("<root><!-- one --><child/><!-- two --></root>")
1129 .expect("fixed comment fixture must parse");
1130 let budget = NodeSetMaterializationBudget::with_limits(3, 1, 1);
1131
1132 let nodes = NodeSet::entire_document_without_comments_with_budget(&document, &budget)
1133 .expect("the three materialized tree nodes must fit exactly");
1134
1135 assert_eq!(nodes.nodes.len(), 3);
1136 assert!(nodes.nodes.iter().all(|key| match key {
1137 XmlNodeKey::Tree(id) => !document.get_node(*id).is_some_and(|node| node.is_comment()),
1138 _ => true,
1139 }));
1140 }
1141
1142 #[test]
1143 fn bare_fragment_preflights_the_materialized_set_without_comments() {
1144 let document =
1147 Document::parse("<root><target><!-- one --><child/><!-- two --></target></root>")
1148 .expect("fixed comment fixture must parse");
1149 let target = document
1150 .descendants()
1151 .find(|node| node.has_tag_name("target"))
1152 .expect("fixed fixture contains the selected target");
1153 let budget = NodeSetMaterializationBudget::with_limits(2, 1, 1);
1154
1155 let nodes = NodeSet::subtree_without_comments_with_budget(target, Some(&budget))
1156 .expect("the target and child must fit exactly");
1157
1158 assert_eq!(nodes.nodes.len(), 2);
1159 assert!(!nodes.with_comments());
1160 }
1161
1162 #[test]
1163 fn excluding_disjoint_oversized_subtree_only_scans_input_keys() {
1164 let xml = format!(
1168 "<root><target Id=\"selected\"><child/></target><Signature>{}</Signature></root>",
1169 "<Object/>".repeat(129)
1170 );
1171 let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
1172 let target = document
1173 .descendants()
1174 .find(|node| node.attribute("Id") == Some("selected"))
1175 .expect("fixed fixture contains the selected subtree");
1176 let signature = document
1177 .descendants()
1178 .find(|node| node.has_tag_name("Signature"))
1179 .expect("fixed fixture contains the excluded Signature subtree");
1180 let budget = NodeSetMaterializationBudget::with_limits(3, 8, 8);
1181 let mut nodes = NodeSet::subtree_with_budget(target, &budget)
1182 .expect("small selected subtree must fit the materialization budget");
1183 let entries_before = nodes.nodes.len();
1184
1185 nodes.exclude_subtree(signature);
1186
1187 assert_eq!(nodes.nodes.len(), entries_before);
1188 assert!(nodes.contains(target));
1189 assert!(
1190 nodes.contains(
1191 target
1192 .first_element_child()
1193 .expect("fixed target subtree contains a child")
1194 )
1195 );
1196 }
1197
1198 #[test]
1199 fn materialization_rejects_inherited_namespace_byte_amplification() {
1200 let namespace_uri = "x".repeat(8_192);
1204 let xml = format!(
1205 "<root xmlns:amplified=\"{namespace_uri}\">{}</root>",
1206 "<child/>".repeat(1_025)
1207 );
1208 let document = Document::parse(&xml).expect("fixed namespace fixture must parse");
1209
1210 let error = NodeSet::entire_document_without_comments(&document)
1211 .err()
1212 .expect("amplified namespace bytes must exceed the materialization budget");
1213
1214 assert!(matches!(
1215 error,
1216 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1217 resource: "node-set owned string bytes",
1218 ..
1219 })
1220 ));
1221 }
1222
1223 #[test]
1224 fn subtree_node_id_range_contains_only_the_selected_subtree() {
1225 let document = Document::parse(
1229 "<root><before/><excluded xmlns:gone=\"urn:gone\" a=\"1\"><child/></excluded><after/></root>",
1230 )
1231 .expect("fixed subtree range fixture must parse");
1232 let excluded = document
1233 .descendants()
1234 .find(|node| node.has_tag_name("excluded"))
1235 .expect("fixed fixture contains the excluded subtree");
1236 let range = subtree_node_id_range(excluded);
1237 let before = document
1238 .descendants()
1239 .find(|node| node.has_tag_name("before"))
1240 .expect("fixed fixture contains the preceding sibling");
1241 let child = excluded
1242 .first_element_child()
1243 .expect("fixed fixture contains an excluded child");
1244 let after = document
1245 .descendants()
1246 .find(|node| node.has_tag_name("after"))
1247 .expect("fixed fixture contains the following sibling");
1248
1249 assert!(!range.contains(&before.id().get()));
1250 assert!(range.contains(&excluded.id().get()));
1251 assert!(range.contains(&child.id().get()));
1252 assert!(!range.contains(&after.id().get()));
1253
1254 let mut nodes = NodeSet::entire_document_with_comments(&document)
1255 .expect("fixed fixture must fit the node-set materialization budget");
1256 nodes.exclude_subtree(excluded);
1257
1258 assert!(nodes.contains(before));
1259 assert!(!nodes.contains(excluded));
1260 assert!(!nodes.contains(child));
1261 assert!(!nodes.contains_attribute(excluded, None, "a"));
1262 assert!(!nodes.contains_namespace(excluded, "gone", "urn:gone"));
1263 assert!(nodes.contains(after));
1264 }
1265
1266 #[test]
1267 fn excluding_subtree_removes_trailing_text_and_comments_from_canonical_output() {
1268 let document = Document::parse(
1272 "<root><before/>keep-before<excluded><child/>drop-text<!--drop-comment--></excluded>keep-after<after/></root>",
1273 )
1274 .expect("fixed trailing-node fixture must parse");
1275 let excluded = document
1276 .descendants()
1277 .find(|node| node.has_tag_name("excluded"))
1278 .expect("fixed fixture contains the excluded subtree");
1279 let trailing_text = excluded
1280 .children()
1281 .find(|node| node.is_text())
1282 .expect("fixed fixture contains trailing text");
1283 let trailing_comment = excluded
1284 .children()
1285 .find(|node| node.is_comment())
1286 .expect("fixed fixture contains a trailing comment");
1287 let mut nodes = NodeSet::entire_document_with_comments(&document)
1288 .expect("fixed fixture must fit the node-set materialization budget");
1289
1290 nodes.exclude_subtree(excluded);
1291
1292 assert!(!nodes.contains(trailing_text));
1293 assert!(!nodes.contains(trailing_comment));
1294 let mut output = Vec::new();
1295 canonicalize_with_visibility(
1296 &document,
1297 Some(&nodes),
1298 &C14nAlgorithm::new(C14nMode::Inclusive1_0, true),
1299 &mut output,
1300 )
1301 .expect("the retained node set must canonicalize");
1302 assert_eq!(
1303 output,
1304 b"<root><before></before>keep-beforekeep-after<after></after></root>"
1305 );
1306 }
1307}