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;
41use serialize::{
42 C14nConfig, CanonicalOutputOptions, serialize_canonical_visible_with_position_bounded,
43 serialize_canonical_visible_with_position_with_xml_base_budget,
44};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum C14nMode {
49 Inclusive1_0,
51 Inclusive1_1,
53 Exclusive1_0,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct C14nAlgorithm {
63 mode: C14nMode,
64 with_comments: bool,
65 inclusive_prefixes: HashSet<String>,
68}
69
70impl C14nAlgorithm {
71 pub fn mode(&self) -> C14nMode {
73 self.mode
74 }
75
76 pub fn with_comments(&self) -> bool {
78 self.with_comments
79 }
80
81 pub fn inclusive_prefixes(&self) -> &HashSet<String> {
83 &self.inclusive_prefixes
84 }
85
86 pub fn new(mode: C14nMode, with_comments: bool) -> Self {
88 Self {
89 mode,
90 with_comments,
91 inclusive_prefixes: HashSet::new(),
92 }
93 }
94
95 pub fn from_uri(uri: &str) -> Option<Self> {
97 let (mode, with_comments) = match uri {
98 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" => (C14nMode::Inclusive1_0, false),
99 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" => {
100 (C14nMode::Inclusive1_0, true)
101 }
102 "http://www.w3.org/2006/12/xml-c14n11" => (C14nMode::Inclusive1_1, false),
103 "http://www.w3.org/2006/12/xml-c14n11#WithComments" => (C14nMode::Inclusive1_1, true),
104 "http://www.w3.org/2001/10/xml-exc-c14n#" => (C14nMode::Exclusive1_0, false),
105 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" => (C14nMode::Exclusive1_0, true),
106 _ => return None,
107 };
108 Some(Self {
109 mode,
110 with_comments,
111 inclusive_prefixes: HashSet::new(),
112 })
113 }
114
115 pub fn with_prefix_list(mut self, prefix_list: &str) -> Self {
121 self.inclusive_prefixes = prefix_list
122 .split_whitespace()
123 .map(|p| {
124 if p == "#default" {
125 String::new()
126 } else {
127 p.to_string()
128 }
129 })
130 .collect();
131 self
132 }
133
134 pub fn uri(&self) -> &'static str {
136 match (self.mode, self.with_comments) {
137 (C14nMode::Inclusive1_0, false) => "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
138 (C14nMode::Inclusive1_0, true) => {
139 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
140 }
141 (C14nMode::Inclusive1_1, false) => "http://www.w3.org/2006/12/xml-c14n11",
142 (C14nMode::Inclusive1_1, true) => "http://www.w3.org/2006/12/xml-c14n11#WithComments",
143 (C14nMode::Exclusive1_0, false) => "http://www.w3.org/2001/10/xml-exc-c14n#",
144 (C14nMode::Exclusive1_0, true) => "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
145 }
146 }
147}
148
149#[derive(Debug, thiserror::Error)]
151pub enum C14nError {
152 #[error("XML parse error: {0}")]
154 Parse(String),
155 #[error("invalid node reference")]
157 InvalidNode,
158 #[error("unsupported algorithm: {0}")]
160 UnsupportedAlgorithm(String),
161 #[error("XML Base resolution exceeds maximum of {max} inherited components: got {actual}")]
163 XmlBaseComponentsTooLarge {
164 max: usize,
166 actual: usize,
168 },
169 #[error("XML Base resolution exceeds maximum of {max_bytes} bytes: got at least {actual}")]
171 XmlBaseResolutionTooLarge {
172 max_bytes: usize,
174 actual: usize,
176 },
177 #[error("I/O error: {0}")]
179 Io(#[from] std::io::Error),
180}
181
182#[cfg(any(feature = "xmldsig", test))]
183pub(crate) fn is_output_limit_error(error: &C14nError) -> bool {
184 matches!(
185 error,
186 C14nError::Io(error)
187 if error
188 .get_ref()
189 .is_some_and(|source| source.is::<CanonicalOutputLimitExceeded>())
190 )
191}
192
193pub(crate) trait NodeVisibility {
200 fn contains_node(&self, node: Node<'_, '_>) -> bool;
201
202 fn contains_attribute(
203 &self,
204 owner: Node<'_, '_>,
205 namespace: Option<&str>,
206 local_name: &str,
207 ) -> bool;
208
209 fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool;
210}
211
212struct ClosureVisibility<'a> {
213 predicate: &'a dyn Fn(Node<'_, '_>) -> bool,
214}
215
216impl NodeVisibility for ClosureVisibility<'_> {
217 fn contains_node(&self, node: Node<'_, '_>) -> bool {
218 (self.predicate)(node)
219 }
220
221 fn contains_attribute(
222 &self,
223 owner: Node<'_, '_>,
224 _namespace: Option<&str>,
225 _local_name: &str,
226 ) -> bool {
227 (self.predicate)(owner)
228 }
229
230 fn contains_namespace(&self, owner: Node<'_, '_>, _prefix: &str, _uri: &str) -> bool {
231 (self.predicate)(owner)
232 }
233}
234
235pub fn canonicalize(
243 doc: &Document,
244 node_set: Option<&dyn Fn(Node) -> bool>,
245 algo: &C14nAlgorithm,
246 output: &mut Vec<u8>,
247) -> Result<(), C14nError> {
248 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
249 canonicalize_with_visibility(
250 doc,
251 visibility
252 .as_ref()
253 .map(|visibility| visibility as &dyn NodeVisibility),
254 algo,
255 output,
256 )
257}
258
259#[cfg(any(feature = "xmldsig", test))]
260pub(crate) fn canonicalize_bounded_with_xml_base_budget(
263 doc: &Document,
264 node_set: Option<&dyn Fn(Node) -> bool>,
265 algo: &C14nAlgorithm,
266 max_output_bytes: usize,
267 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
268 output: &mut Vec<u8>,
269) -> Result<(), C14nError> {
270 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
271 canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
272 doc,
273 visibility
274 .as_ref()
275 .map(|visibility| visibility as &dyn NodeVisibility),
276 algo,
277 None,
278 max_output_bytes,
279 xml_base_resolution,
280 output,
281 )?;
282 Ok(())
283}
284
285pub(crate) fn canonicalize_with_visibility(
286 doc: &Document,
287 visibility: Option<&dyn NodeVisibility>,
288 algo: &C14nAlgorithm,
289 output: &mut Vec<u8>,
290) -> Result<(), C14nError> {
291 canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
292 Ok(())
293}
294
295pub(crate) fn canonicalize_with_visibility_and_position(
296 doc: &Document,
297 visibility: Option<&dyn NodeVisibility>,
298 algo: &C14nAlgorithm,
299 tracked_element: Option<NodeId>,
300 output: &mut Vec<u8>,
301) -> Result<Option<usize>, C14nError> {
302 canonicalize_with_visibility_and_position_impl(
303 doc,
304 visibility,
305 algo,
306 tracked_element,
307 None,
308 None,
309 output,
310 )
311}
312
313#[cfg(test)]
314pub(crate) fn canonicalize_with_visibility_and_position_bounded(
315 doc: &Document,
316 visibility: Option<&dyn NodeVisibility>,
317 algo: &C14nAlgorithm,
318 tracked_element: Option<NodeId>,
319 max_output_bytes: usize,
320 output: &mut Vec<u8>,
321) -> Result<Option<usize>, C14nError> {
322 canonicalize_with_visibility_and_position_impl(
323 doc,
324 visibility,
325 algo,
326 tracked_element,
327 Some(max_output_bytes),
328 None,
329 output,
330 )
331}
332
333#[cfg(any(feature = "xmldsig", test))]
334pub(crate) fn canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
335 doc: &Document,
336 visibility: Option<&dyn NodeVisibility>,
337 algo: &C14nAlgorithm,
338 tracked_element: Option<NodeId>,
339 max_output_bytes: usize,
340 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
341 output: &mut Vec<u8>,
342) -> Result<Option<usize>, C14nError> {
343 canonicalize_with_visibility_and_position_impl(
344 doc,
345 visibility,
346 algo,
347 tracked_element,
348 Some(max_output_bytes),
349 Some(xml_base_resolution),
350 output,
351 )
352}
353
354fn canonicalize_with_visibility_and_position_impl(
355 doc: &Document,
356 visibility: Option<&dyn NodeVisibility>,
357 algo: &C14nAlgorithm,
358 tracked_element: Option<NodeId>,
359 max_output_bytes: Option<usize>,
360 xml_base_resolution: Option<&xml_base::XmlBaseResolutionBudget>,
361 output: &mut Vec<u8>,
362) -> Result<Option<usize>, C14nError> {
363 let default_xml_base_resolution = xml_base::XmlBaseResolutionBudget::default();
364 let xml_base_resolution = xml_base_resolution.unwrap_or(&default_xml_base_resolution);
365 match algo.mode {
369 C14nMode::Inclusive1_0 => {
370 let renderer = InclusiveNsRenderer;
371 let config = C14nConfig {
372 inherit_xml_attrs: true,
373 fixup_xml_base: false,
374 };
375 serialize_canonical_visible_with_position_dispatch(
376 doc,
377 visibility,
378 algo.with_comments,
379 &renderer,
380 config,
381 tracked_element,
382 max_output_bytes,
383 xml_base_resolution,
384 output,
385 )
386 }
387 C14nMode::Inclusive1_1 => {
388 let renderer = InclusiveNsRenderer;
389 let config = C14nConfig {
390 inherit_xml_attrs: true,
391 fixup_xml_base: true,
392 };
393 serialize_canonical_visible_with_position_dispatch(
394 doc,
395 visibility,
396 algo.with_comments,
397 &renderer,
398 config,
399 tracked_element,
400 max_output_bytes,
401 xml_base_resolution,
402 output,
403 )
404 }
405 C14nMode::Exclusive1_0 => {
406 let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
407 let config = C14nConfig {
408 inherit_xml_attrs: false,
409 fixup_xml_base: false,
410 };
411 serialize_canonical_visible_with_position_dispatch(
412 doc,
413 visibility,
414 algo.with_comments,
415 &renderer,
416 config,
417 tracked_element,
418 max_output_bytes,
419 xml_base_resolution,
420 output,
421 )
422 }
423 }
424}
425
426#[allow(clippy::too_many_arguments)]
427fn serialize_canonical_visible_with_position_dispatch(
428 doc: &Document,
429 visibility: Option<&dyn NodeVisibility>,
430 with_comments: bool,
431 renderer: &dyn serialize::NsRenderer,
432 config: C14nConfig,
433 tracked_element: Option<NodeId>,
434 max_output_bytes: Option<usize>,
435 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
436 output: &mut Vec<u8>,
437) -> Result<Option<usize>, C14nError> {
438 match max_output_bytes {
439 Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
440 doc,
441 visibility,
442 with_comments,
443 renderer,
444 config,
445 CanonicalOutputOptions::bounded(tracked_element, max_output_bytes, xml_base_resolution),
446 output,
447 ),
448 None => serialize_canonical_visible_with_position_with_xml_base_budget(
449 doc,
450 visibility,
451 with_comments,
452 renderer,
453 config,
454 tracked_element,
455 xml_base_resolution,
456 output,
457 ),
458 }
459}
460
461pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
467 let xml_str =
468 std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
469 let doc = Document::parse(xml_str).map_err(|e| C14nError::Parse(e.to_string()))?;
470 let mut output = Vec::new();
471 canonicalize(&doc, None, algo, &mut output)?;
472 Ok(output)
473}
474
475#[cfg(test)]
476#[allow(clippy::unwrap_used)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn from_uri_roundtrip() {
482 let uris = [
483 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
484 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
485 "http://www.w3.org/2006/12/xml-c14n11",
486 "http://www.w3.org/2006/12/xml-c14n11#WithComments",
487 "http://www.w3.org/2001/10/xml-exc-c14n#",
488 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
489 ];
490 for uri in uris {
491 let algo = C14nAlgorithm::from_uri(uri).expect(uri);
492 assert_eq!(algo.uri(), uri);
493 }
494 }
495
496 #[test]
497 fn unknown_uri_returns_none() {
498 assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
499 }
500
501 #[test]
502 fn prefix_list_parsing() {
503 let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
504 .with_prefix_list("foo bar #default baz");
505 assert!(algo.inclusive_prefixes.contains("foo"));
506 assert!(algo.inclusive_prefixes.contains("bar"));
507 assert!(algo.inclusive_prefixes.contains("baz"));
508 assert!(algo.inclusive_prefixes.contains("")); assert_eq!(algo.inclusive_prefixes.len(), 4);
510 }
511
512 #[test]
513 fn canonicalize_xml_basic() {
514 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
515 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
516 let result = canonicalize_xml(xml, &algo).expect("c14n");
517 assert_eq!(
518 String::from_utf8(result).expect("utf8"),
519 r#"<root a="1" b="2"><empty></empty></root>"#
520 );
521 }
522
523 #[test]
524 fn c14n_1_1_basic() {
525 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
527 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
528 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
529 assert_eq!(
530 String::from_utf8(result).expect("utf8"),
531 r#"<root a="1" b="2"><empty></empty></root>"#
532 );
533 }
534
535 #[test]
536 fn c14n_1_1_with_comments() {
537 let xml = b"<root><!-- comment -->text</root>";
538 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
539 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
540 assert_eq!(
541 String::from_utf8(result).expect("utf8"),
542 "<root><!-- comment -->text</root>"
543 );
544 }
545
546 #[test]
547 fn c14n_1_1_without_comments() {
548 let xml = b"<root><!-- comment -->text</root>";
549 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
550 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
551 assert_eq!(
552 String::from_utf8(result).expect("utf8"),
553 "<root>text</root>"
554 );
555 }
556
557 #[test]
558 fn c14n_1_1_namespaces() {
559 let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
561 let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
562 let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
563 let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
564 let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
565 assert_eq!(result_10, result_11);
567 }
568
569 #[test]
570 fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
571 use roxmltree::Document;
574 use std::collections::HashSet;
575
576 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
577 let doc = Document::parse(xml).expect("parse");
578 let child = doc.root_element().first_element_child().expect("child");
579
580 let mut ids = HashSet::new();
582 let mut stack = vec![child];
583 while let Some(n) = stack.pop() {
584 ids.insert(n.id());
585 for c in n.children() {
586 stack.push(c);
587 }
588 }
589 let pred = move |n: roxmltree::Node| ids.contains(&n.id());
590
591 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
592 let mut out = Vec::new();
593 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
594 let result = String::from_utf8(out).expect("utf8");
595
596 assert!(
597 !result.contains(r#"xml:id="r1""#),
598 "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
599 );
600 }
601
602 #[test]
603 fn c14n_1_0_xml_id_is_inherited_in_subset() {
604 use roxmltree::Document;
607 use std::collections::HashSet;
608
609 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
610 let doc = Document::parse(xml).expect("parse");
611 let child = doc.root_element().first_element_child().expect("child");
612 let ids = child
613 .descendants()
614 .map(|node| node.id())
615 .collect::<HashSet<_>>();
616 let pred = move |node: roxmltree::Node| ids.contains(&node.id());
617
618 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
619 let mut out = Vec::new();
620 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
621
622 assert_eq!(
623 String::from_utf8(out).expect("utf8"),
624 r#"<child xml:id="r1">text</child>"#
625 );
626 }
627
628 #[test]
629 fn bounded_canonicalization_stops_before_exceeding_the_limit() {
630 let xml = format!("<root>{}</root>", "x".repeat(4_096));
634 let document = Document::parse(&xml).expect("fixed XML must parse");
635 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
636 let mut output = Vec::new();
637
638 let error = canonicalize_with_visibility_and_position_bounded(
639 &document,
640 None,
641 &algorithm,
642 None,
643 64,
644 &mut output,
645 )
646 .expect_err("canonicalization must stop at the caller's byte ceiling");
647
648 assert!(is_output_limit_error(&error));
649 assert!(
650 output.len() <= 64,
651 "bounded output grew to {} bytes",
652 output.len()
653 );
654 }
655
656 #[test]
657 fn c14n_1_1_bounds_inherited_xml_base_components() {
658 let mut xml = String::new();
662 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
663 xml.push_str(r#"<n xml:base="segment/">"#);
664 }
665 xml.push_str("<leaf/>");
666 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
667 xml.push_str("</n>");
668 }
669 let document = Document::parse(&xml).expect("fixed XML must parse");
670 let leaf = document
671 .descendants()
672 .find(|node| node.has_tag_name("leaf"))
673 .expect("leaf");
674 let visible = |node: Node<'_, '_>| node == leaf;
675 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
676 let mut output = Vec::new();
677
678 let error = canonicalize_with_visibility(
679 &document,
680 Some(&ClosureVisibility {
681 predicate: &visible,
682 }),
683 &algorithm,
684 &mut output,
685 )
686 .expect_err("C14N must reject an overlong inherited xml:base chain");
687
688 assert!(
689 error.to_string().contains("XML Base"),
690 "unexpected C14N error: {error}"
691 );
692 }
693
694 #[test]
695 fn unbounded_output_preserves_the_callers_xml_base_budget() {
696 let document = Document::parse(
699 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
700 )
701 .unwrap();
702 let leaf = document
703 .descendants()
704 .find(|node| node.has_tag_name("leaf"))
705 .unwrap();
706 let visible = |node: Node<'_, '_>| node == leaf;
707 let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
708 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
709 let mut output = Vec::new();
710
711 let error = canonicalize_with_visibility_and_position_impl(
712 &document,
713 Some(&ClosureVisibility {
714 predicate: &visible,
715 }),
716 &algorithm,
717 None,
718 None,
719 Some(&budget),
720 &mut output,
721 )
722 .expect_err("the caller's component ceiling must survive unbounded dispatch");
723
724 assert!(matches!(
725 error,
726 C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
727 ));
728 }
729}