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