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