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