1mod escape;
26#[cfg(feature = "xmldsig")]
27pub(crate) use escape::escape_attr;
28mod ns_common;
29pub(crate) mod ns_exclusive;
30pub(crate) mod ns_inclusive;
31pub(crate) mod prefix;
32pub(crate) mod serialize;
33pub(crate) mod xml_base;
34
35use std::collections::HashSet;
36
37use crate::xml::dom::{Document, Node, NodeId};
38
39use ns_exclusive::ExclusiveNsRenderer;
40use ns_inclusive::InclusiveNsRenderer;
41#[cfg(any(feature = "xmldsig", test))]
42use serialize::CanonicalOutputLimitExceeded;
43#[cfg(any(feature = "xmldsig", test))]
44use serialize::serialize_canonical_visible_with_positions_bounded;
45use serialize::{
46 C14nConfig, CanonicalOutputOptions, serialize_canonical_visible_with_position_bounded,
47 serialize_canonical_visible_with_position_with_xml_base_budget,
48};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum C14nMode {
53 Inclusive1_0,
55 Inclusive1_1,
57 Exclusive1_0,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct C14nAlgorithm {
67 mode: C14nMode,
68 with_comments: bool,
69 inclusive_prefixes: HashSet<String>,
72}
73
74impl C14nAlgorithm {
75 pub fn mode(&self) -> C14nMode {
77 self.mode
78 }
79
80 pub fn with_comments(&self) -> bool {
82 self.with_comments
83 }
84
85 pub fn inclusive_prefixes(&self) -> &HashSet<String> {
87 &self.inclusive_prefixes
88 }
89
90 pub fn new(mode: C14nMode, with_comments: bool) -> Self {
92 Self {
93 mode,
94 with_comments,
95 inclusive_prefixes: HashSet::new(),
96 }
97 }
98
99 pub fn from_uri(uri: &str) -> Option<Self> {
101 let (mode, with_comments) = match uri {
102 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" => (C14nMode::Inclusive1_0, false),
103 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" => {
104 (C14nMode::Inclusive1_0, true)
105 }
106 "http://www.w3.org/2006/12/xml-c14n11" => (C14nMode::Inclusive1_1, false),
107 "http://www.w3.org/2006/12/xml-c14n11#WithComments" => (C14nMode::Inclusive1_1, true),
108 "http://www.w3.org/2001/10/xml-exc-c14n#" => (C14nMode::Exclusive1_0, false),
109 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" => (C14nMode::Exclusive1_0, true),
110 _ => return None,
111 };
112 Some(Self {
113 mode,
114 with_comments,
115 inclusive_prefixes: HashSet::new(),
116 })
117 }
118
119 pub fn with_prefix_list(mut self, prefix_list: &str) -> Self {
125 self.inclusive_prefixes = prefix_list
126 .split_whitespace()
127 .map(|p| {
128 if p == "#default" {
129 String::new()
130 } else {
131 p.to_string()
132 }
133 })
134 .collect();
135 self
136 }
137
138 pub fn uri(&self) -> &'static str {
140 match (self.mode, self.with_comments) {
141 (C14nMode::Inclusive1_0, false) => "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
142 (C14nMode::Inclusive1_0, true) => {
143 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
144 }
145 (C14nMode::Inclusive1_1, false) => "http://www.w3.org/2006/12/xml-c14n11",
146 (C14nMode::Inclusive1_1, true) => "http://www.w3.org/2006/12/xml-c14n11#WithComments",
147 (C14nMode::Exclusive1_0, false) => "http://www.w3.org/2001/10/xml-exc-c14n#",
148 (C14nMode::Exclusive1_0, true) => "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
149 }
150 }
151}
152
153#[derive(Debug, thiserror::Error)]
155pub enum C14nError {
156 #[error("XML parse error: {0}")]
158 Parse(String),
159 #[error("invalid node reference")]
161 InvalidNode,
162 #[error("unsupported algorithm: {0}")]
164 UnsupportedAlgorithm(String),
165 #[error("XML Base resolution exceeds maximum of {max} inherited components: got {actual}")]
167 XmlBaseComponentsTooLarge {
168 max: usize,
170 actual: usize,
172 },
173 #[error("XML Base resolution exceeds maximum of {max_bytes} bytes: got at least {actual}")]
175 XmlBaseResolutionTooLarge {
176 max_bytes: usize,
178 actual: usize,
180 },
181 #[error("I/O error: {0}")]
183 Io(#[from] std::io::Error),
184}
185
186#[cfg(any(feature = "xmldsig", test))]
187pub(crate) fn is_output_limit_error(error: &C14nError) -> bool {
188 matches!(
189 error,
190 C14nError::Io(error)
191 if error
192 .get_ref()
193 .is_some_and(|source| source.is::<CanonicalOutputLimitExceeded>())
194 )
195}
196
197pub(crate) trait NodeVisibility {
204 fn contains_node(&self, node: Node<'_, '_>) -> bool;
205
206 fn contains_attribute(
207 &self,
208 owner: Node<'_, '_>,
209 namespace: Option<&str>,
210 local_name: &str,
211 ) -> bool;
212
213 fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool;
214}
215
216struct ClosureVisibility<'a> {
217 predicate: &'a dyn Fn(Node<'_, '_>) -> bool,
218}
219
220impl NodeVisibility for ClosureVisibility<'_> {
221 fn contains_node(&self, node: Node<'_, '_>) -> bool {
222 (self.predicate)(node)
223 }
224
225 fn contains_attribute(
226 &self,
227 owner: Node<'_, '_>,
228 _namespace: Option<&str>,
229 _local_name: &str,
230 ) -> bool {
231 (self.predicate)(owner)
232 }
233
234 fn contains_namespace(&self, owner: Node<'_, '_>, _prefix: &str, _uri: &str) -> bool {
235 (self.predicate)(owner)
236 }
237}
238
239pub fn canonicalize(
247 doc: &Document,
248 node_set: Option<&dyn Fn(Node) -> bool>,
249 algo: &C14nAlgorithm,
250 output: &mut Vec<u8>,
251) -> Result<(), C14nError> {
252 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
253 canonicalize_with_visibility(
254 doc,
255 visibility
256 .as_ref()
257 .map(|visibility| visibility as &dyn NodeVisibility),
258 algo,
259 output,
260 )
261}
262
263#[cfg(any(feature = "xmldsig", test))]
264pub(crate) fn canonicalize_bounded_with_xml_base_budget(
267 doc: &Document,
268 node_set: Option<&dyn Fn(Node) -> bool>,
269 algo: &C14nAlgorithm,
270 max_output_bytes: usize,
271 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
272 output: &mut Vec<u8>,
273) -> Result<(), C14nError> {
274 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
275 canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
276 doc,
277 visibility
278 .as_ref()
279 .map(|visibility| visibility as &dyn NodeVisibility),
280 algo,
281 None,
282 max_output_bytes,
283 xml_base_resolution,
284 output,
285 )?;
286 Ok(())
287}
288
289pub(crate) fn canonicalize_with_visibility(
290 doc: &Document,
291 visibility: Option<&dyn NodeVisibility>,
292 algo: &C14nAlgorithm,
293 output: &mut Vec<u8>,
294) -> Result<(), C14nError> {
295 canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
296 Ok(())
297}
298
299pub(crate) fn canonicalize_with_visibility_and_position(
300 doc: &Document,
301 visibility: Option<&dyn NodeVisibility>,
302 algo: &C14nAlgorithm,
303 tracked_element: Option<NodeId>,
304 output: &mut Vec<u8>,
305) -> Result<Option<usize>, C14nError> {
306 canonicalize_with_visibility_and_position_impl(
307 doc,
308 visibility,
309 algo,
310 tracked_element,
311 None,
312 None,
313 output,
314 )
315}
316
317#[cfg(test)]
318pub(crate) fn canonicalize_with_visibility_and_position_bounded(
319 doc: &Document,
320 visibility: Option<&dyn NodeVisibility>,
321 algo: &C14nAlgorithm,
322 tracked_element: Option<NodeId>,
323 max_output_bytes: usize,
324 output: &mut Vec<u8>,
325) -> Result<Option<usize>, C14nError> {
326 canonicalize_with_visibility_and_position_impl(
327 doc,
328 visibility,
329 algo,
330 tracked_element,
331 Some(max_output_bytes),
332 None,
333 output,
334 )
335}
336
337#[cfg(any(feature = "xmldsig", test))]
338pub(crate) fn canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
339 doc: &Document,
340 visibility: Option<&dyn NodeVisibility>,
341 algo: &C14nAlgorithm,
342 tracked_element: Option<NodeId>,
343 max_output_bytes: usize,
344 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
345 output: &mut Vec<u8>,
346) -> Result<Option<usize>, C14nError> {
347 canonicalize_with_visibility_and_position_impl(
348 doc,
349 visibility,
350 algo,
351 tracked_element,
352 Some(max_output_bytes),
353 Some(xml_base_resolution),
354 output,
355 )
356}
357
358#[cfg(any(feature = "xmldsig", test))]
359pub(crate) fn canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget(
360 doc: &Document,
361 visibility: Option<&dyn NodeVisibility>,
362 algo: &C14nAlgorithm,
363 tracked_elements: &[NodeId],
364 max_output_bytes: usize,
365 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
366 output: &mut Vec<u8>,
367) -> Result<Vec<(NodeId, usize)>, C14nError> {
368 let config = C14nConfig {
369 inherit_xml_attrs: !matches!(algo.mode, C14nMode::Exclusive1_0),
370 fixup_xml_base: matches!(algo.mode, C14nMode::Inclusive1_1),
371 };
372 let inclusive = InclusiveNsRenderer;
373 let exclusive = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
374 let renderer: &dyn serialize::NsRenderer = match algo.mode {
375 C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
376 C14nMode::Exclusive1_0 => &exclusive,
377 };
378 serialize_canonical_visible_with_positions_bounded(
379 doc,
380 visibility,
381 algo.with_comments,
382 renderer,
383 config,
384 CanonicalOutputOptions::bounded_many(
385 tracked_elements,
386 max_output_bytes,
387 xml_base_resolution,
388 ),
389 output,
390 )
391}
392
393fn canonicalize_with_visibility_and_position_impl(
394 doc: &Document,
395 visibility: Option<&dyn NodeVisibility>,
396 algo: &C14nAlgorithm,
397 tracked_element: Option<NodeId>,
398 max_output_bytes: Option<usize>,
399 xml_base_resolution: Option<&xml_base::XmlBaseResolutionBudget>,
400 output: &mut Vec<u8>,
401) -> Result<Option<usize>, C14nError> {
402 let default_xml_base_resolution = xml_base::XmlBaseResolutionBudget::default();
403 let xml_base_resolution = xml_base_resolution.unwrap_or(&default_xml_base_resolution);
404 match algo.mode {
408 C14nMode::Inclusive1_0 => {
409 let renderer = InclusiveNsRenderer;
410 let config = C14nConfig {
411 inherit_xml_attrs: true,
412 fixup_xml_base: false,
413 };
414 serialize_canonical_visible_with_position_dispatch(
415 doc,
416 visibility,
417 algo.with_comments,
418 &renderer,
419 config,
420 tracked_element,
421 max_output_bytes,
422 xml_base_resolution,
423 output,
424 )
425 }
426 C14nMode::Inclusive1_1 => {
427 let renderer = InclusiveNsRenderer;
428 let config = C14nConfig {
429 inherit_xml_attrs: true,
430 fixup_xml_base: true,
431 };
432 serialize_canonical_visible_with_position_dispatch(
433 doc,
434 visibility,
435 algo.with_comments,
436 &renderer,
437 config,
438 tracked_element,
439 max_output_bytes,
440 xml_base_resolution,
441 output,
442 )
443 }
444 C14nMode::Exclusive1_0 => {
445 let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
446 let config = C14nConfig {
447 inherit_xml_attrs: false,
448 fixup_xml_base: false,
449 };
450 serialize_canonical_visible_with_position_dispatch(
451 doc,
452 visibility,
453 algo.with_comments,
454 &renderer,
455 config,
456 tracked_element,
457 max_output_bytes,
458 xml_base_resolution,
459 output,
460 )
461 }
462 }
463}
464
465#[allow(clippy::too_many_arguments)]
466fn serialize_canonical_visible_with_position_dispatch(
467 doc: &Document,
468 visibility: Option<&dyn NodeVisibility>,
469 with_comments: bool,
470 renderer: &dyn serialize::NsRenderer,
471 config: C14nConfig,
472 tracked_element: Option<NodeId>,
473 max_output_bytes: Option<usize>,
474 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
475 output: &mut Vec<u8>,
476) -> Result<Option<usize>, C14nError> {
477 match max_output_bytes {
478 Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
479 doc,
480 visibility,
481 with_comments,
482 renderer,
483 config,
484 CanonicalOutputOptions::bounded(tracked_element, max_output_bytes, xml_base_resolution),
485 output,
486 ),
487 None => serialize_canonical_visible_with_position_with_xml_base_budget(
488 doc,
489 visibility,
490 with_comments,
491 renderer,
492 config,
493 tracked_element,
494 xml_base_resolution,
495 output,
496 ),
497 }
498}
499
500pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
508 canonicalize_xml_with_backend(xml, algo, crate::XmlBackend::default())
509}
510
511pub fn canonicalize_xml_with_backend(
513 xml: &[u8],
514 algo: &C14nAlgorithm,
515 backend: crate::XmlBackend,
516) -> Result<Vec<u8>, C14nError> {
517 if xml.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
518 return Err(C14nError::Parse(format!(
519 "input exceeds maximum XML document size of {} bytes: got {}",
520 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
521 xml.len()
522 )));
523 }
524 let xml_str =
525 std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
526 let document = crate::document::parse_borrowed_with_settings_and_budget(
527 xml_str,
528 crate::document::DocumentParseSettings::default().with_backend(backend),
529 None,
530 )
531 .map_err(|error| C14nError::Parse(error.to_string()))?;
532 let mut output = Vec::new();
533 canonicalize(&document, None, algo, &mut output)?;
534 Ok(output)
535}
536
537pub fn canonicalize_document(
539 document: &crate::XmlDocument,
540 algo: &C14nAlgorithm,
541) -> Result<Vec<u8>, C14nError> {
542 let mut output = Vec::new();
543 document.with_view(|view| canonicalize(view.document(), None, algo, &mut output))?;
544 Ok(output)
545}
546
547#[cfg(test)]
548#[allow(clippy::unwrap_used)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn from_uri_roundtrip() {
554 let uris = [
555 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
556 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
557 "http://www.w3.org/2006/12/xml-c14n11",
558 "http://www.w3.org/2006/12/xml-c14n11#WithComments",
559 "http://www.w3.org/2001/10/xml-exc-c14n#",
560 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
561 ];
562 for uri in uris {
563 let algo = C14nAlgorithm::from_uri(uri).expect(uri);
564 assert_eq!(algo.uri(), uri);
565 }
566 }
567
568 #[test]
569 fn unknown_uri_returns_none() {
570 assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
571 }
572
573 #[test]
574 fn prefix_list_parsing() {
575 let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
576 .with_prefix_list("foo bar #default baz");
577 assert!(algo.inclusive_prefixes.contains("foo"));
578 assert!(algo.inclusive_prefixes.contains("bar"));
579 assert!(algo.inclusive_prefixes.contains("baz"));
580 assert!(algo.inclusive_prefixes.contains("")); assert_eq!(algo.inclusive_prefixes.len(), 4);
582 }
583
584 #[test]
585 fn canonicalize_xml_basic() {
586 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
587 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
588 let result = canonicalize_xml(xml, &algo).expect("c14n");
589 assert_eq!(
590 String::from_utf8(result).expect("utf8"),
591 r#"<root a="1" b="2"><empty></empty></root>"#
592 );
593 }
594
595 #[test]
596 fn canonicalize_xml_rejects_input_above_the_document_byte_ceiling_before_parsing() {
597 let xml = vec![b' '; crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1];
600 let error = match canonicalize_xml(&xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
601 {
602 Err(error) => error,
603 Ok(_) => panic!("oversized canonicalization input must be rejected"),
604 };
605 assert!(matches!(
606 error,
607 C14nError::Parse(message)
608 if message.contains("exceeds maximum XML document size")
609 ));
610 }
611
612 #[test]
613 fn canonicalize_xml_rejects_input_above_the_document_node_ceiling() {
614 let children = "<n/>".repeat(crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize);
617 let xml = format!("<root>{children}</root>");
618 let error = match canonicalize_xml(
619 xml.as_bytes(),
620 &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
621 ) {
622 Err(error) => error,
623 Ok(_) => panic!("excessive canonicalization nodes must be rejected"),
624 };
625 assert!(matches!(
626 error,
627 C14nError::Parse(message) if message.contains("nodes limit reached")
628 ));
629 }
630
631 #[test]
632 fn canonicalize_xml_rejects_input_above_the_document_depth_ceiling() {
633 let depth = crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING + 1;
636 let xml = format!("{}{}", "<n>".repeat(depth), "</n>".repeat(depth));
637
638 let error = canonicalize_xml(
639 xml.as_bytes(),
640 &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
641 )
642 .expect_err("over-depth canonicalization input must be rejected");
643
644 assert!(matches!(
645 error,
646 C14nError::Parse(message) if message.contains("maximum element depth")
647 ));
648 }
649
650 #[test]
651 fn canonicalize_xml_does_not_enable_dtd_or_external_entity_resolution() {
652 let xml = br#"<!DOCTYPE root [<!ENTITY value 'expanded'>]><root>&value;</root>"#;
655 let error = canonicalize_xml(xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
656 .expect_err("DTD input must remain disabled");
657 assert!(matches!(
658 error,
659 C14nError::Parse(message) if message.contains("DTD detected")
660 ));
661 }
662
663 #[test]
664 fn c14n_1_1_basic() {
665 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
667 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
668 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
669 assert_eq!(
670 String::from_utf8(result).expect("utf8"),
671 r#"<root a="1" b="2"><empty></empty></root>"#
672 );
673 }
674
675 #[test]
676 fn c14n_1_1_with_comments() {
677 let xml = b"<root><!-- comment -->text</root>";
678 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
679 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
680 assert_eq!(
681 String::from_utf8(result).expect("utf8"),
682 "<root><!-- comment -->text</root>"
683 );
684 }
685
686 #[test]
687 fn c14n_1_1_without_comments() {
688 let xml = b"<root><!-- comment -->text</root>";
689 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
690 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
691 assert_eq!(
692 String::from_utf8(result).expect("utf8"),
693 "<root>text</root>"
694 );
695 }
696
697 #[test]
698 fn c14n_1_1_namespaces() {
699 let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
701 let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
702 let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
703 let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
704 let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
705 assert_eq!(result_10, result_11);
707 }
708
709 #[test]
710 fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
711 use crate::xml::dom::Document;
714 use std::collections::HashSet;
715
716 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
717 let doc = Document::parse(xml).expect("parse");
718 let child = doc.root_element().first_element_child().expect("child");
719
720 let mut ids = HashSet::new();
722 let mut stack = vec![child];
723 while let Some(n) = stack.pop() {
724 ids.insert(n.id());
725 for c in n.children() {
726 stack.push(c);
727 }
728 }
729 let pred = move |n: crate::xml::dom::Node| ids.contains(&n.id());
730
731 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
732 let mut out = Vec::new();
733 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
734 let result = String::from_utf8(out).expect("utf8");
735
736 assert!(
737 !result.contains(r#"xml:id="r1""#),
738 "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
739 );
740 }
741
742 #[test]
743 fn c14n_1_0_xml_id_is_inherited_in_subset() {
744 use crate::xml::dom::Document;
747 use std::collections::HashSet;
748
749 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
750 let doc = Document::parse(xml).expect("parse");
751 let child = doc.root_element().first_element_child().expect("child");
752 let ids = child
753 .descendants()
754 .map(|node| node.id())
755 .collect::<HashSet<_>>();
756 let pred = move |node: crate::xml::dom::Node| ids.contains(&node.id());
757
758 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
759 let mut out = Vec::new();
760 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
761
762 assert_eq!(
763 String::from_utf8(out).expect("utf8"),
764 r#"<child xml:id="r1">text</child>"#
765 );
766 }
767
768 #[test]
769 fn bounded_canonicalization_stops_before_exceeding_the_limit() {
770 let xml = format!("<root>{}</root>", "x".repeat(4_096));
774 let document = Document::parse(&xml).expect("fixed XML must parse");
775 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
776 let mut output = Vec::new();
777
778 let error = canonicalize_with_visibility_and_position_bounded(
779 &document,
780 None,
781 &algorithm,
782 None,
783 64,
784 &mut output,
785 )
786 .expect_err("canonicalization must stop at the caller's byte ceiling");
787
788 assert!(is_output_limit_error(&error));
789 assert!(
790 output.len() <= 64,
791 "bounded output grew to {} bytes",
792 output.len()
793 );
794 }
795
796 #[test]
797 fn c14n_1_1_bounds_inherited_xml_base_components() {
798 let mut xml = String::new();
802 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
803 xml.push_str(r#"<n xml:base="segment/">"#);
804 }
805 xml.push_str("<leaf/>");
806 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
807 xml.push_str("</n>");
808 }
809 let document = Document::parse(&xml).expect("fixed XML must parse");
810 let leaf = document
811 .descendants()
812 .find(|node| node.has_tag_name("leaf"))
813 .expect("leaf");
814 let visible = |node: Node<'_, '_>| node == leaf;
815 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
816 let mut output = Vec::new();
817
818 let error = canonicalize_with_visibility(
819 &document,
820 Some(&ClosureVisibility {
821 predicate: &visible,
822 }),
823 &algorithm,
824 &mut output,
825 )
826 .expect_err("C14N must reject an overlong inherited xml:base chain");
827
828 assert!(
829 error.to_string().contains("XML Base"),
830 "unexpected C14N error: {error}"
831 );
832 }
833
834 #[test]
835 fn unbounded_output_preserves_the_callers_xml_base_budget() {
836 let document = Document::parse(
839 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
840 )
841 .unwrap();
842 let leaf = document
843 .descendants()
844 .find(|node| node.has_tag_name("leaf"))
845 .unwrap();
846 let visible = |node: Node<'_, '_>| node == leaf;
847 let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
848 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
849 let mut output = Vec::new();
850
851 let error = canonicalize_with_visibility_and_position_impl(
852 &document,
853 Some(&ClosureVisibility {
854 predicate: &visible,
855 }),
856 &algorithm,
857 None,
858 None,
859 Some(&budget),
860 &mut output,
861 )
862 .expect_err("the caller's component ceiling must survive unbounded dispatch");
863
864 assert!(matches!(
865 error,
866 C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
867 ));
868 }
869}