1mod escape;
26pub(crate) mod scratch;
27#[cfg(feature = "xmldsig")]
28pub(crate) use escape::escape_attr;
29mod ns_common;
30pub(crate) mod ns_exclusive;
31pub(crate) mod ns_inclusive;
32pub(crate) mod prefix;
33pub(crate) mod serialize;
34pub(crate) mod xml_base;
35
36use std::collections::HashSet;
37
38use crate::xml::dom::storage::{Native as NativeDom, Storage as DomStorage};
39use crate::xml::dom::{Document, Node, NodeId};
40
41use ns_exclusive::ExclusiveNsRenderer;
42use ns_inclusive::InclusiveNsRenderer;
43#[cfg(feature = "xmldsig")]
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::inclusive_tokens(prefix_list)
126 .map(|p| {
127 if p == "#default" {
128 String::new()
129 } else {
130 p.to_string()
131 }
132 })
133 .collect();
134 self
135 }
136
137 pub fn uri(&self) -> &'static str {
139 match (self.mode, self.with_comments) {
140 (C14nMode::Inclusive1_0, false) => "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
141 (C14nMode::Inclusive1_0, true) => {
142 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
143 }
144 (C14nMode::Inclusive1_1, false) => "http://www.w3.org/2006/12/xml-c14n11",
145 (C14nMode::Inclusive1_1, true) => "http://www.w3.org/2006/12/xml-c14n11#WithComments",
146 (C14nMode::Exclusive1_0, false) => "http://www.w3.org/2001/10/xml-exc-c14n#",
147 (C14nMode::Exclusive1_0, true) => "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
148 }
149 }
150}
151
152#[derive(Debug, thiserror::Error)]
154pub enum C14nError {
155 #[cfg(feature = "xmldsig")]
157 #[error("canonicalization shared execution: {0}")]
158 SharedExecution(#[from] structured_asn1::memory::MemoryError),
159 #[error("XML parse error: {0}")]
161 Parse(String),
162 #[error("invalid node reference")]
164 InvalidNode,
165 #[error("unsupported algorithm: {0}")]
167 UnsupportedAlgorithm(String),
168 #[error("XML Base resolution exceeds maximum of {max} inherited components: got {actual}")]
170 XmlBaseComponentsTooLarge {
171 max: usize,
173 actual: usize,
175 },
176 #[error("XML Base resolution exceeds maximum of {max_bytes} bytes: got at least {actual}")]
178 XmlBaseResolutionTooLarge {
179 max_bytes: usize,
181 actual: usize,
183 },
184 #[error("canonical output exceeds maximum of {max_bytes} bytes")]
186 OutputLimitExceeded {
187 max_bytes: usize,
189 },
190 #[error("I/O error: {0}")]
192 Io(#[from] std::io::Error),
193}
194
195#[cfg(any(feature = "xmldsig", test))]
196pub(crate) fn is_output_limit_error(error: &C14nError) -> bool {
197 matches!(error, C14nError::OutputLimitExceeded { .. })
198}
199
200pub(crate) trait NodeVisibility<D: DomStorage = NativeDom> {
207 fn contains_node(&self, node: Node<'_, '_, D>) -> Result<bool, C14nError>;
208
209 fn contains_attribute(
210 &self,
211 owner: Node<'_, '_, D>,
212 namespace: Option<&str>,
213 local_name: &str,
214 ) -> Result<bool, C14nError>;
215
216 fn contains_namespace(
217 &self,
218 owner: Node<'_, '_, D>,
219 prefix: &str,
220 uri: &str,
221 ) -> Result<bool, C14nError>;
222
223 fn contains_attribute_at(
224 &self,
225 owner: Node<'_, '_, D>,
226 _ordinal: usize,
227 attribute: crate::xml::dom::Attribute<'_, D>,
228 ) -> Result<bool, C14nError> {
229 self.contains_attribute(owner, attribute.namespace(), attribute.name())
230 }
231 fn contains_namespace_at(
232 &self,
233 owner: Node<'_, '_, D>,
234 _ordinal: usize,
235 namespace: crate::xml::dom::Namespace<'_, D>,
236 ) -> Result<bool, C14nError> {
237 self.contains_namespace(owner, namespace.name().unwrap_or(""), namespace.uri())
238 }
239}
240
241struct ClosureVisibility<'a> {
242 predicate: &'a dyn Fn(Node<'_, '_>) -> bool,
243}
244
245impl NodeVisibility for ClosureVisibility<'_> {
246 fn contains_node(&self, node: Node<'_, '_>) -> Result<bool, C14nError> {
247 Ok((self.predicate)(node))
248 }
249
250 fn contains_attribute(
251 &self,
252 owner: Node<'_, '_>,
253 _namespace: Option<&str>,
254 _local_name: &str,
255 ) -> Result<bool, C14nError> {
256 Ok((self.predicate)(owner))
257 }
258
259 fn contains_namespace(
260 &self,
261 owner: Node<'_, '_>,
262 _prefix: &str,
263 _uri: &str,
264 ) -> Result<bool, C14nError> {
265 Ok((self.predicate)(owner))
266 }
267}
268
269pub fn canonicalize(
277 doc: &Document,
278 node_set: Option<&dyn Fn(Node) -> bool>,
279 algo: &C14nAlgorithm,
280 output: &mut Vec<u8>,
281) -> Result<(), C14nError> {
282 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
283 canonicalize_with_visibility(
284 doc,
285 visibility
286 .as_ref()
287 .map(|visibility| visibility as &dyn NodeVisibility),
288 algo,
289 output,
290 )
291}
292
293#[cfg(feature = "xmldsig")]
296pub(crate) fn canonicalize_bounded_with_xml_base_budget(
297 doc: &Document,
298 node_set: Option<&dyn Fn(Node) -> bool>,
299 algo: &C14nAlgorithm,
300 max_output_bytes: usize,
301 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
302 output: &mut Vec<u8>,
303) -> Result<(), C14nError> {
304 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
305 let initial_offset = output.len();
306 canonicalize_visible_to_writer_bounded(
307 doc,
308 visibility
309 .as_ref()
310 .map(|visibility| visibility as &dyn NodeVisibility),
311 algo,
312 max_output_bytes,
313 xml_base_resolution,
314 output,
315 initial_offset,
316 )?;
317 Ok(())
318}
319
320#[cfg(feature = "xmldsig")]
323pub(crate) fn canonicalize_visible_to_writer_bounded<W: std::io::Write + ?Sized>(
324 doc: &Document,
325 visibility: Option<&dyn NodeVisibility>,
326 algo: &C14nAlgorithm,
327 max_output_bytes: usize,
328 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
329 output: &mut W,
330 initial_offset: usize,
331) -> Result<(), C14nError> {
332 canonicalize_writer_with_scratch(
333 doc,
334 visibility,
335 algo,
336 max_output_bytes,
337 xml_base_resolution,
338 output,
339 initial_offset,
340 &scratch::Native,
341 None,
342 None,
343 )
344 .map(|_| ())
345}
346
347#[cfg(feature = "xmldsig")]
348#[allow(clippy::too_many_arguments)]
349pub(crate) fn canonicalize_visible_to_writer_accounted<
350 W: std::io::Write + ?Sized,
351 E: structured_asn1::memory::Work,
352 D: DomStorage,
353>(
354 doc: &Document<'_, D>,
355 visibility: Option<&dyn NodeVisibility<D>>,
356 algo: &C14nAlgorithm,
357 max_output_bytes: usize,
358 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
359 output: &mut W,
360 work: &E,
361 prefix_list: Option<&str>,
362) -> Result<(), C14nError> {
363 canonicalize_writer_with_scratch(
364 doc,
365 visibility,
366 algo,
367 max_output_bytes,
368 xml_base_resolution,
369 output,
370 0,
371 &scratch::Accounted(work),
372 prefix_list,
373 None,
374 )
375 .map(|_| ())
376}
377
378#[cfg(feature = "xmldsig")]
381pub(crate) fn canonicalize_buffer_in<S: scratch::Scratch, D: DomStorage>(
382 doc: &Document<'_, D>,
383 visibility: &dyn NodeVisibility<D>,
384 (algo, prefix_list): (&C14nAlgorithm, Option<&str>),
385 tracked: Option<NodeId>,
386 max_output_bytes: usize,
387 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
388 scratch: &S,
389) -> Result<(S::Bytes, Option<usize>), C14nError> {
390 struct Buffer<B> {
391 bytes: B,
392 failure: Option<C14nError>,
393 }
394 impl<B: scratch::Bytes> std::io::Write for Buffer<B> {
395 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
396 if self.failure.is_some() {
397 return Err(std::io::ErrorKind::OutOfMemory.into());
398 }
399 if let Err(error) = self.bytes.append(bytes) {
400 self.failure = Some(error);
401 return Err(std::io::ErrorKind::OutOfMemory.into());
402 }
403 Ok(bytes.len())
404 }
405 fn flush(&mut self) -> std::io::Result<()> {
406 Ok(())
407 }
408 }
409 let mut buffer = Buffer {
410 bytes: scratch.bytes(),
411 failure: None,
412 };
413 let result = canonicalize_writer_with_scratch(
414 doc,
415 Some(visibility),
416 algo,
417 max_output_bytes,
418 xml_base_resolution,
419 &mut buffer,
420 0,
421 scratch,
422 prefix_list,
423 tracked,
424 );
425 if let Some(error) = buffer.failure {
426 return Err(error);
427 }
428 Ok((buffer.bytes, result?))
429}
430
431#[cfg(feature = "xmldsig")]
432#[allow(clippy::too_many_arguments)]
433fn canonicalize_writer_with_scratch<
434 W: std::io::Write + ?Sized,
435 S: scratch::Scratch,
436 D: DomStorage,
437>(
438 doc: &Document<'_, D>,
439 visibility: Option<&dyn NodeVisibility<D>>,
440 algo: &C14nAlgorithm,
441 max_output_bytes: usize,
442 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
443 output: &mut W,
444 initial_offset: usize,
445 scratch: &S,
446 prefix_list: Option<&str>,
447 tracked: Option<NodeId>,
448) -> Result<Option<usize>, C14nError> {
449 let inclusive = InclusiveNsRenderer;
450 let exclusive = match prefix_list {
451 Some(list) => ExclusiveNsRenderer::from_prefix_list(list),
452 None => ExclusiveNsRenderer::new(&algo.inclusive_prefixes),
453 };
454 let renderer: &dyn serialize::NsRenderer<S, D> = match algo.mode {
455 C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
456 C14nMode::Exclusive1_0 => &exclusive,
457 };
458 let (position, _) = serialize::serialize_with_scratch(
459 doc,
460 visibility,
461 algo.with_comments,
462 renderer,
463 C14nConfig::for_mode(algo.mode),
464 CanonicalOutputOptions::bounded(tracked, max_output_bytes, xml_base_resolution),
465 output,
466 initial_offset,
467 scratch,
468 )?;
469 Ok(position)
470}
471
472pub(crate) fn canonicalize_with_visibility(
473 doc: &Document,
474 visibility: Option<&dyn NodeVisibility>,
475 algo: &C14nAlgorithm,
476 output: &mut Vec<u8>,
477) -> Result<(), C14nError> {
478 canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
479 Ok(())
480}
481
482pub(crate) fn canonicalize_with_visibility_and_position(
483 doc: &Document,
484 visibility: Option<&dyn NodeVisibility>,
485 algo: &C14nAlgorithm,
486 tracked_element: Option<NodeId>,
487 output: &mut Vec<u8>,
488) -> Result<Option<usize>, C14nError> {
489 canonicalize_with_visibility_and_position_impl(
490 doc,
491 visibility,
492 algo,
493 tracked_element,
494 None,
495 None,
496 output,
497 )
498}
499
500#[cfg(test)]
501pub(crate) fn canonicalize_with_visibility_and_position_bounded(
502 doc: &Document,
503 visibility: Option<&dyn NodeVisibility>,
504 algo: &C14nAlgorithm,
505 tracked_element: Option<NodeId>,
506 max_output_bytes: usize,
507 output: &mut Vec<u8>,
508) -> Result<Option<usize>, C14nError> {
509 canonicalize_with_visibility_and_position_impl(
510 doc,
511 visibility,
512 algo,
513 tracked_element,
514 Some(max_output_bytes),
515 None,
516 output,
517 )
518}
519
520#[cfg(feature = "xmldsig")]
521pub(crate) fn canonicalize_with_visibility_and_position_bounded_with_xml_base_budget<
522 D: DomStorage,
523>(
524 doc: &Document<'_, D>,
525 visibility: Option<&dyn NodeVisibility<D>>,
526 algo: &C14nAlgorithm,
527 tracked_element: Option<NodeId>,
528 max_output_bytes: usize,
529 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
530 output: &mut Vec<u8>,
531) -> Result<Option<usize>, C14nError> {
532 canonicalize_with_visibility_and_position_impl(
533 doc,
534 visibility,
535 algo,
536 tracked_element,
537 Some(max_output_bytes),
538 Some(xml_base_resolution),
539 output,
540 )
541}
542
543#[cfg(feature = "xmldsig")]
544pub(crate) fn canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget<
545 D: DomStorage,
546>(
547 doc: &Document<'_, D>,
548 visibility: Option<&dyn NodeVisibility<D>>,
549 algo: &C14nAlgorithm,
550 tracked_elements: &[NodeId],
551 max_output_bytes: usize,
552 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
553 output: &mut Vec<u8>,
554) -> Result<Vec<(NodeId, usize)>, C14nError> {
555 let config = C14nConfig::for_mode(algo.mode);
556 let inclusive = InclusiveNsRenderer;
557 let exclusive = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
558 let renderer: &dyn serialize::NsRenderer<scratch::Native, D> = match algo.mode {
559 C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
560 C14nMode::Exclusive1_0 => &exclusive,
561 };
562 serialize_canonical_visible_with_positions_bounded(
563 doc,
564 visibility,
565 algo.with_comments,
566 renderer,
567 config,
568 CanonicalOutputOptions::bounded_many(
569 tracked_elements,
570 max_output_bytes,
571 xml_base_resolution,
572 ),
573 output,
574 )
575}
576
577fn canonicalize_with_visibility_and_position_impl<D: DomStorage>(
578 doc: &Document<'_, D>,
579 visibility: Option<&dyn NodeVisibility<D>>,
580 algo: &C14nAlgorithm,
581 tracked_element: Option<NodeId>,
582 max_output_bytes: Option<usize>,
583 xml_base_resolution: Option<&xml_base::XmlBaseResolutionBudget>,
584 output: &mut Vec<u8>,
585) -> Result<Option<usize>, C14nError> {
586 let default_xml_base_resolution = xml_base::XmlBaseResolutionBudget::default();
587 let xml_base_resolution = xml_base_resolution.unwrap_or(&default_xml_base_resolution);
588 match algo.mode {
592 C14nMode::Inclusive1_0 => {
593 let renderer = InclusiveNsRenderer;
594 let config = C14nConfig::for_mode(algo.mode);
595 serialize_canonical_visible_with_position_dispatch(
596 doc,
597 visibility,
598 algo.with_comments,
599 &renderer,
600 config,
601 tracked_element,
602 max_output_bytes,
603 xml_base_resolution,
604 output,
605 )
606 }
607 C14nMode::Inclusive1_1 => {
608 let renderer = InclusiveNsRenderer;
609 let config = C14nConfig::for_mode(algo.mode);
610 serialize_canonical_visible_with_position_dispatch(
611 doc,
612 visibility,
613 algo.with_comments,
614 &renderer,
615 config,
616 tracked_element,
617 max_output_bytes,
618 xml_base_resolution,
619 output,
620 )
621 }
622 C14nMode::Exclusive1_0 => {
623 let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
624 let config = C14nConfig::for_mode(algo.mode);
625 serialize_canonical_visible_with_position_dispatch(
626 doc,
627 visibility,
628 algo.with_comments,
629 &renderer,
630 config,
631 tracked_element,
632 max_output_bytes,
633 xml_base_resolution,
634 output,
635 )
636 }
637 }
638}
639
640#[allow(clippy::too_many_arguments)]
641fn serialize_canonical_visible_with_position_dispatch<D: DomStorage>(
642 doc: &Document<'_, D>,
643 visibility: Option<&dyn NodeVisibility<D>>,
644 with_comments: bool,
645 renderer: &dyn serialize::NsRenderer<scratch::Native, D>,
646 config: C14nConfig,
647 tracked_element: Option<NodeId>,
648 max_output_bytes: Option<usize>,
649 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
650 output: &mut Vec<u8>,
651) -> Result<Option<usize>, C14nError> {
652 match max_output_bytes {
653 Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
654 doc,
655 visibility,
656 with_comments,
657 renderer,
658 config,
659 CanonicalOutputOptions::bounded(tracked_element, max_output_bytes, xml_base_resolution),
660 output,
661 ),
662 None => serialize_canonical_visible_with_position_with_xml_base_budget(
663 doc,
664 visibility,
665 with_comments,
666 renderer,
667 config,
668 tracked_element,
669 xml_base_resolution,
670 output,
671 ),
672 }
673}
674
675pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
683 canonicalize_xml_with_backend(xml, algo, crate::XmlBackend::default())
684}
685
686pub fn canonicalize_xml_with_backend(
688 xml: &[u8],
689 algo: &C14nAlgorithm,
690 backend: crate::XmlBackend,
691) -> Result<Vec<u8>, C14nError> {
692 if xml.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
693 return Err(C14nError::Parse(format!(
694 "input exceeds maximum XML document size of {} bytes: got {}",
695 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
696 xml.len()
697 )));
698 }
699 let xml_str =
700 std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
701 let document = crate::document::parse_borrowed_with_settings_and_budget(
702 xml_str,
703 crate::document::DocumentParseSettings::default().with_backend(backend),
704 None,
705 )
706 .map_err(|error| C14nError::Parse(error.to_string()))?;
707 let mut output = Vec::new();
708 canonicalize(&document, None, algo, &mut output)?;
709 Ok(output)
710}
711
712pub fn canonicalize_document(
714 document: &crate::XmlDocument,
715 algo: &C14nAlgorithm,
716) -> Result<Vec<u8>, C14nError> {
717 let mut output = Vec::new();
718 document.with_view(|view| canonicalize(view.document(), None, algo, &mut output))?;
719 Ok(output)
720}
721
722#[cfg(test)]
723#[allow(clippy::unwrap_used)]
724mod tests {
725 use super::*;
726
727 #[test]
728 fn from_uri_roundtrip() {
729 let uris = [
730 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
731 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
732 "http://www.w3.org/2006/12/xml-c14n11",
733 "http://www.w3.org/2006/12/xml-c14n11#WithComments",
734 "http://www.w3.org/2001/10/xml-exc-c14n#",
735 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
736 ];
737 for uri in uris {
738 let algo = C14nAlgorithm::from_uri(uri).expect(uri);
739 assert_eq!(algo.uri(), uri);
740 }
741 }
742
743 #[test]
744 fn unknown_uri_returns_none() {
745 assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
746 }
747
748 #[test]
749 fn prefix_list_parsing() {
750 let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
751 .with_prefix_list("foo bar #default baz");
752 assert!(algo.inclusive_prefixes.contains("foo"));
753 assert!(algo.inclusive_prefixes.contains("bar"));
754 assert!(algo.inclusive_prefixes.contains("baz"));
755 assert!(algo.inclusive_prefixes.contains("")); assert_eq!(algo.inclusive_prefixes.len(), 4);
757 }
758
759 #[test]
760 fn canonicalize_xml_basic() {
761 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
762 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
763 let result = canonicalize_xml(xml, &algo).expect("c14n");
764 assert_eq!(
765 String::from_utf8(result).expect("utf8"),
766 r#"<root a="1" b="2"><empty></empty></root>"#
767 );
768 }
769
770 #[test]
771 fn canonicalize_xml_rejects_input_above_the_document_byte_ceiling_before_parsing() {
772 let xml = vec![b' '; crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1];
775 let error = match canonicalize_xml(&xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
776 {
777 Err(error) => error,
778 Ok(_) => panic!("oversized canonicalization input must be rejected"),
779 };
780 assert!(matches!(
781 error,
782 C14nError::Parse(message)
783 if message.contains("exceeds maximum XML document size")
784 ));
785 }
786
787 #[test]
788 fn canonicalize_xml_rejects_input_above_the_document_node_ceiling() {
789 let children = "<n/>".repeat(crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize);
792 let xml = format!("<root>{children}</root>");
793 let error = match canonicalize_xml(
794 xml.as_bytes(),
795 &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
796 ) {
797 Err(error) => error,
798 Ok(_) => panic!("excessive canonicalization nodes must be rejected"),
799 };
800 assert!(matches!(
801 error,
802 C14nError::Parse(message) if message.contains("nodes limit reached")
803 ));
804 }
805
806 #[test]
807 fn canonicalize_xml_rejects_input_above_the_document_depth_ceiling() {
808 let depth = crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING + 1;
811 let xml = format!("{}{}", "<n>".repeat(depth), "</n>".repeat(depth));
812
813 let error = canonicalize_xml(
814 xml.as_bytes(),
815 &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
816 )
817 .expect_err("over-depth canonicalization input must be rejected");
818
819 assert!(matches!(
820 error,
821 C14nError::Parse(message) if message.contains("maximum element depth")
822 ));
823 }
824
825 #[test]
826 fn canonicalize_xml_does_not_enable_dtd_or_external_entity_resolution() {
827 let xml = br#"<!DOCTYPE root [<!ENTITY value 'expanded'>]><root>&value;</root>"#;
830 let error = canonicalize_xml(xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
831 .expect_err("DTD input must remain disabled");
832 assert!(matches!(
833 error,
834 C14nError::Parse(message) if message.contains("DTD detected")
835 ));
836 }
837
838 #[test]
839 fn c14n_1_1_basic() {
840 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
842 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
843 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
844 assert_eq!(
845 String::from_utf8(result).expect("utf8"),
846 r#"<root a="1" b="2"><empty></empty></root>"#
847 );
848 }
849
850 #[test]
851 fn c14n_1_1_with_comments() {
852 let xml = b"<root><!-- comment -->text</root>";
853 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
854 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
855 assert_eq!(
856 String::from_utf8(result).expect("utf8"),
857 "<root><!-- comment -->text</root>"
858 );
859 }
860
861 #[test]
862 fn c14n_1_1_without_comments() {
863 let xml = b"<root><!-- comment -->text</root>";
864 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
865 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
866 assert_eq!(
867 String::from_utf8(result).expect("utf8"),
868 "<root>text</root>"
869 );
870 }
871
872 #[test]
873 fn c14n_1_1_namespaces() {
874 let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
876 let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
877 let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
878 let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
879 let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
880 assert_eq!(result_10, result_11);
882 }
883
884 #[test]
885 fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
886 use crate::xml::dom::Document;
889 use std::collections::HashSet;
890
891 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
892 let doc = Document::parse(xml).expect("parse");
893 let child = doc.root_element().first_element_child().expect("child");
894
895 let mut ids = HashSet::new();
897 let mut stack = vec![child];
898 while let Some(n) = stack.pop() {
899 ids.insert(n.id());
900 for c in n.children() {
901 stack.push(c);
902 }
903 }
904 let pred = move |n: crate::xml::dom::Node| ids.contains(&n.id());
905
906 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
907 let mut out = Vec::new();
908 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
909 let result = String::from_utf8(out).expect("utf8");
910
911 assert!(
912 !result.contains(r#"xml:id="r1""#),
913 "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
914 );
915 }
916
917 #[test]
918 fn c14n_1_0_xml_id_is_inherited_in_subset() {
919 use crate::xml::dom::Document;
922 use std::collections::HashSet;
923
924 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
925 let doc = Document::parse(xml).expect("parse");
926 let child = doc.root_element().first_element_child().expect("child");
927 let ids = child
928 .descendants()
929 .map(|node| node.id())
930 .collect::<HashSet<_>>();
931 let pred = move |node: crate::xml::dom::Node| ids.contains(&node.id());
932
933 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
934 let mut out = Vec::new();
935 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
936
937 assert_eq!(
938 String::from_utf8(out).expect("utf8"),
939 r#"<child xml:id="r1">text</child>"#
940 );
941 }
942
943 #[test]
944 fn bounded_canonicalization_stops_before_exceeding_the_limit() {
945 let xml = format!("<root>{}</root>", "x".repeat(4_096));
949 let document = Document::parse(&xml).expect("fixed XML must parse");
950 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
951 let mut output = Vec::new();
952
953 let error = canonicalize_with_visibility_and_position_bounded(
954 &document,
955 None,
956 &algorithm,
957 None,
958 64,
959 &mut output,
960 )
961 .expect_err("canonicalization must stop at the caller's byte ceiling");
962
963 assert!(is_output_limit_error(&error));
964 assert!(
965 output.len() <= 64,
966 "bounded output grew to {} bytes",
967 output.len()
968 );
969 }
970
971 #[test]
972 fn c14n_1_1_bounds_inherited_xml_base_components() {
973 let mut xml = String::new();
977 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
978 xml.push_str(r#"<n xml:base="segment/">"#);
979 }
980 xml.push_str("<leaf/>");
981 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
982 xml.push_str("</n>");
983 }
984 let document = Document::parse(&xml).expect("fixed XML must parse");
985 let leaf = document
986 .descendants()
987 .find(|node| node.has_tag_name("leaf"))
988 .expect("leaf");
989 let visible = |node: Node<'_, '_>| node == leaf;
990 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
991 let mut output = Vec::new();
992
993 let error = canonicalize_with_visibility(
994 &document,
995 Some(&ClosureVisibility {
996 predicate: &visible,
997 }),
998 &algorithm,
999 &mut output,
1000 )
1001 .expect_err("C14N must reject an overlong inherited xml:base chain");
1002
1003 assert!(
1004 error.to_string().contains("XML Base"),
1005 "unexpected C14N error: {error}"
1006 );
1007 }
1008
1009 #[test]
1010 fn unbounded_output_preserves_the_callers_xml_base_budget() {
1011 let document = Document::parse(
1014 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
1015 )
1016 .unwrap();
1017 let leaf = document
1018 .descendants()
1019 .find(|node| node.has_tag_name("leaf"))
1020 .unwrap();
1021 let visible = |node: Node<'_, '_>| node == leaf;
1022 let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
1023 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
1024 let mut output = Vec::new();
1025
1026 let error = canonicalize_with_visibility_and_position_impl(
1027 &document,
1028 Some(&ClosureVisibility {
1029 predicate: &visible,
1030 }),
1031 &algorithm,
1032 None,
1033 None,
1034 Some(&budget),
1035 &mut output,
1036 )
1037 .expect_err("the caller's component ceiling must survive unbounded dispatch");
1038
1039 assert!(matches!(
1040 error,
1041 C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
1042 ));
1043 }
1044}