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 roxmltree::{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> {
504 let xml_str =
505 std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
506 let doc = Document::parse(xml_str).map_err(|e| C14nError::Parse(e.to_string()))?;
507 let mut output = Vec::new();
508 canonicalize(&doc, None, algo, &mut output)?;
509 Ok(output)
510}
511
512#[cfg(test)]
513#[allow(clippy::unwrap_used)]
514mod tests {
515 use super::*;
516
517 #[test]
518 fn from_uri_roundtrip() {
519 let uris = [
520 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
521 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
522 "http://www.w3.org/2006/12/xml-c14n11",
523 "http://www.w3.org/2006/12/xml-c14n11#WithComments",
524 "http://www.w3.org/2001/10/xml-exc-c14n#",
525 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
526 ];
527 for uri in uris {
528 let algo = C14nAlgorithm::from_uri(uri).expect(uri);
529 assert_eq!(algo.uri(), uri);
530 }
531 }
532
533 #[test]
534 fn unknown_uri_returns_none() {
535 assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
536 }
537
538 #[test]
539 fn prefix_list_parsing() {
540 let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
541 .with_prefix_list("foo bar #default baz");
542 assert!(algo.inclusive_prefixes.contains("foo"));
543 assert!(algo.inclusive_prefixes.contains("bar"));
544 assert!(algo.inclusive_prefixes.contains("baz"));
545 assert!(algo.inclusive_prefixes.contains("")); assert_eq!(algo.inclusive_prefixes.len(), 4);
547 }
548
549 #[test]
550 fn canonicalize_xml_basic() {
551 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
552 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
553 let result = canonicalize_xml(xml, &algo).expect("c14n");
554 assert_eq!(
555 String::from_utf8(result).expect("utf8"),
556 r#"<root a="1" b="2"><empty></empty></root>"#
557 );
558 }
559
560 #[test]
561 fn c14n_1_1_basic() {
562 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
564 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
565 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
566 assert_eq!(
567 String::from_utf8(result).expect("utf8"),
568 r#"<root a="1" b="2"><empty></empty></root>"#
569 );
570 }
571
572 #[test]
573 fn c14n_1_1_with_comments() {
574 let xml = b"<root><!-- comment -->text</root>";
575 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
576 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
577 assert_eq!(
578 String::from_utf8(result).expect("utf8"),
579 "<root><!-- comment -->text</root>"
580 );
581 }
582
583 #[test]
584 fn c14n_1_1_without_comments() {
585 let xml = b"<root><!-- comment -->text</root>";
586 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
587 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
588 assert_eq!(
589 String::from_utf8(result).expect("utf8"),
590 "<root>text</root>"
591 );
592 }
593
594 #[test]
595 fn c14n_1_1_namespaces() {
596 let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
598 let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
599 let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
600 let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
601 let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
602 assert_eq!(result_10, result_11);
604 }
605
606 #[test]
607 fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
608 use roxmltree::Document;
611 use std::collections::HashSet;
612
613 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
614 let doc = Document::parse(xml).expect("parse");
615 let child = doc.root_element().first_element_child().expect("child");
616
617 let mut ids = HashSet::new();
619 let mut stack = vec![child];
620 while let Some(n) = stack.pop() {
621 ids.insert(n.id());
622 for c in n.children() {
623 stack.push(c);
624 }
625 }
626 let pred = move |n: roxmltree::Node| ids.contains(&n.id());
627
628 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
629 let mut out = Vec::new();
630 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
631 let result = String::from_utf8(out).expect("utf8");
632
633 assert!(
634 !result.contains(r#"xml:id="r1""#),
635 "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
636 );
637 }
638
639 #[test]
640 fn c14n_1_0_xml_id_is_inherited_in_subset() {
641 use roxmltree::Document;
644 use std::collections::HashSet;
645
646 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
647 let doc = Document::parse(xml).expect("parse");
648 let child = doc.root_element().first_element_child().expect("child");
649 let ids = child
650 .descendants()
651 .map(|node| node.id())
652 .collect::<HashSet<_>>();
653 let pred = move |node: roxmltree::Node| ids.contains(&node.id());
654
655 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
656 let mut out = Vec::new();
657 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
658
659 assert_eq!(
660 String::from_utf8(out).expect("utf8"),
661 r#"<child xml:id="r1">text</child>"#
662 );
663 }
664
665 #[test]
666 fn bounded_canonicalization_stops_before_exceeding_the_limit() {
667 let xml = format!("<root>{}</root>", "x".repeat(4_096));
671 let document = Document::parse(&xml).expect("fixed XML must parse");
672 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
673 let mut output = Vec::new();
674
675 let error = canonicalize_with_visibility_and_position_bounded(
676 &document,
677 None,
678 &algorithm,
679 None,
680 64,
681 &mut output,
682 )
683 .expect_err("canonicalization must stop at the caller's byte ceiling");
684
685 assert!(is_output_limit_error(&error));
686 assert!(
687 output.len() <= 64,
688 "bounded output grew to {} bytes",
689 output.len()
690 );
691 }
692
693 #[test]
694 fn c14n_1_1_bounds_inherited_xml_base_components() {
695 let mut xml = String::new();
699 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
700 xml.push_str(r#"<n xml:base="segment/">"#);
701 }
702 xml.push_str("<leaf/>");
703 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
704 xml.push_str("</n>");
705 }
706 let document = Document::parse(&xml).expect("fixed XML must parse");
707 let leaf = document
708 .descendants()
709 .find(|node| node.has_tag_name("leaf"))
710 .expect("leaf");
711 let visible = |node: Node<'_, '_>| node == leaf;
712 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
713 let mut output = Vec::new();
714
715 let error = canonicalize_with_visibility(
716 &document,
717 Some(&ClosureVisibility {
718 predicate: &visible,
719 }),
720 &algorithm,
721 &mut output,
722 )
723 .expect_err("C14N must reject an overlong inherited xml:base chain");
724
725 assert!(
726 error.to_string().contains("XML Base"),
727 "unexpected C14N error: {error}"
728 );
729 }
730
731 #[test]
732 fn unbounded_output_preserves_the_callers_xml_base_budget() {
733 let document = Document::parse(
736 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
737 )
738 .unwrap();
739 let leaf = document
740 .descendants()
741 .find(|node| node.has_tag_name("leaf"))
742 .unwrap();
743 let visible = |node: Node<'_, '_>| node == leaf;
744 let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
745 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
746 let mut output = Vec::new();
747
748 let error = canonicalize_with_visibility_and_position_impl(
749 &document,
750 Some(&ClosureVisibility {
751 predicate: &visible,
752 }),
753 &algorithm,
754 None,
755 None,
756 Some(&budget),
757 &mut output,
758 )
759 .expect_err("the caller's component ceiling must survive unbounded dispatch");
760
761 assert!(matches!(
762 error,
763 C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
764 ));
765 }
766}