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")]
380pub(crate) fn canonicalize_visible_to_writer_in<
381 W: std::io::Write + ?Sized,
382 S: scratch::Scratch,
383 D: DomStorage,
384>(
385 doc: &Document<'_, D>,
386 visibility: &dyn NodeVisibility<D>,
387 (algo, prefix_list): (&C14nAlgorithm, Option<&str>),
388 max_output_bytes: usize,
389 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
390 output: &mut W,
391 scratch: &S,
392) -> Result<(), C14nError> {
393 canonicalize_writer_with_scratch(
394 doc,
395 Some(visibility),
396 algo,
397 max_output_bytes,
398 xml_base_resolution,
399 output,
400 0,
401 scratch,
402 prefix_list,
403 None,
404 )
405 .map(|_| ())
406}
407
408#[cfg(feature = "xmldsig")]
411pub(crate) fn canonicalize_buffer_in<S: scratch::Scratch, D: DomStorage>(
412 doc: &Document<'_, D>,
413 visibility: &dyn NodeVisibility<D>,
414 (algo, prefix_list): (&C14nAlgorithm, Option<&str>),
415 tracked: Option<NodeId>,
416 max_output_bytes: usize,
417 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
418 scratch: &S,
419) -> Result<(S::Bytes, Option<usize>), C14nError> {
420 struct Buffer<B> {
421 bytes: B,
422 failure: Option<C14nError>,
423 }
424 impl<B: scratch::Bytes> std::io::Write for Buffer<B> {
425 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
426 if self.failure.is_some() {
427 return Err(std::io::ErrorKind::OutOfMemory.into());
428 }
429 if let Err(error) = self.bytes.append(bytes) {
430 self.failure = Some(error);
431 return Err(std::io::ErrorKind::OutOfMemory.into());
432 }
433 Ok(bytes.len())
434 }
435 fn flush(&mut self) -> std::io::Result<()> {
436 Ok(())
437 }
438 }
439 let mut buffer = Buffer {
440 bytes: scratch.bytes(),
441 failure: None,
442 };
443 let result = canonicalize_writer_with_scratch(
444 doc,
445 Some(visibility),
446 algo,
447 max_output_bytes,
448 xml_base_resolution,
449 &mut buffer,
450 0,
451 scratch,
452 prefix_list,
453 tracked,
454 );
455 if let Some(error) = buffer.failure {
456 return Err(error);
457 }
458 Ok((buffer.bytes, result?))
459}
460
461#[cfg(feature = "xmldsig")]
462#[allow(clippy::too_many_arguments)]
463fn canonicalize_writer_with_scratch<
464 W: std::io::Write + ?Sized,
465 S: scratch::Scratch,
466 D: DomStorage,
467>(
468 doc: &Document<'_, D>,
469 visibility: Option<&dyn NodeVisibility<D>>,
470 algo: &C14nAlgorithm,
471 max_output_bytes: usize,
472 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
473 output: &mut W,
474 initial_offset: usize,
475 scratch: &S,
476 prefix_list: Option<&str>,
477 tracked: Option<NodeId>,
478) -> Result<Option<usize>, C14nError> {
479 let inclusive = InclusiveNsRenderer;
480 let exclusive = match prefix_list {
481 Some(list) => ExclusiveNsRenderer::from_prefix_list(list),
482 None => ExclusiveNsRenderer::new(&algo.inclusive_prefixes),
483 };
484 let renderer: &dyn serialize::NsRenderer<S, D> = match algo.mode {
485 C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
486 C14nMode::Exclusive1_0 => &exclusive,
487 };
488 let (position, _) = serialize::serialize_with_scratch(
489 doc,
490 visibility,
491 algo.with_comments,
492 renderer,
493 C14nConfig::for_mode(algo.mode),
494 CanonicalOutputOptions::bounded(tracked, max_output_bytes, xml_base_resolution),
495 output,
496 initial_offset,
497 scratch,
498 )?;
499 Ok(position)
500}
501
502pub(crate) fn canonicalize_with_visibility(
503 doc: &Document,
504 visibility: Option<&dyn NodeVisibility>,
505 algo: &C14nAlgorithm,
506 output: &mut Vec<u8>,
507) -> Result<(), C14nError> {
508 canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
509 Ok(())
510}
511
512pub(crate) fn canonicalize_with_visibility_and_position(
513 doc: &Document,
514 visibility: Option<&dyn NodeVisibility>,
515 algo: &C14nAlgorithm,
516 tracked_element: Option<NodeId>,
517 output: &mut Vec<u8>,
518) -> Result<Option<usize>, C14nError> {
519 canonicalize_with_visibility_and_position_impl(
520 doc,
521 visibility,
522 algo,
523 tracked_element,
524 None,
525 None,
526 output,
527 )
528}
529
530#[cfg(test)]
531pub(crate) fn canonicalize_with_visibility_and_position_bounded(
532 doc: &Document,
533 visibility: Option<&dyn NodeVisibility>,
534 algo: &C14nAlgorithm,
535 tracked_element: Option<NodeId>,
536 max_output_bytes: usize,
537 output: &mut Vec<u8>,
538) -> Result<Option<usize>, C14nError> {
539 canonicalize_with_visibility_and_position_impl(
540 doc,
541 visibility,
542 algo,
543 tracked_element,
544 Some(max_output_bytes),
545 None,
546 output,
547 )
548}
549
550#[cfg(feature = "xmldsig")]
551pub(crate) fn canonicalize_with_visibility_and_position_bounded_with_xml_base_budget<
552 D: DomStorage,
553>(
554 doc: &Document<'_, D>,
555 visibility: Option<&dyn NodeVisibility<D>>,
556 algo: &C14nAlgorithm,
557 tracked_element: Option<NodeId>,
558 max_output_bytes: usize,
559 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
560 output: &mut Vec<u8>,
561) -> Result<Option<usize>, C14nError> {
562 canonicalize_with_visibility_and_position_impl(
563 doc,
564 visibility,
565 algo,
566 tracked_element,
567 Some(max_output_bytes),
568 Some(xml_base_resolution),
569 output,
570 )
571}
572
573#[cfg(feature = "xmldsig")]
574pub(crate) fn canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget<
575 D: DomStorage,
576>(
577 doc: &Document<'_, D>,
578 visibility: Option<&dyn NodeVisibility<D>>,
579 algo: &C14nAlgorithm,
580 tracked_elements: &[NodeId],
581 max_output_bytes: usize,
582 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
583 output: &mut Vec<u8>,
584) -> Result<Vec<(NodeId, usize)>, C14nError> {
585 let config = C14nConfig::for_mode(algo.mode);
586 let inclusive = InclusiveNsRenderer;
587 let exclusive = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
588 let renderer: &dyn serialize::NsRenderer<scratch::Native, D> = match algo.mode {
589 C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
590 C14nMode::Exclusive1_0 => &exclusive,
591 };
592 serialize_canonical_visible_with_positions_bounded(
593 doc,
594 visibility,
595 algo.with_comments,
596 renderer,
597 config,
598 CanonicalOutputOptions::bounded_many(
599 tracked_elements,
600 max_output_bytes,
601 xml_base_resolution,
602 ),
603 output,
604 )
605}
606
607fn canonicalize_with_visibility_and_position_impl<D: DomStorage>(
608 doc: &Document<'_, D>,
609 visibility: Option<&dyn NodeVisibility<D>>,
610 algo: &C14nAlgorithm,
611 tracked_element: Option<NodeId>,
612 max_output_bytes: Option<usize>,
613 xml_base_resolution: Option<&xml_base::XmlBaseResolutionBudget>,
614 output: &mut Vec<u8>,
615) -> Result<Option<usize>, C14nError> {
616 let default_xml_base_resolution = xml_base::XmlBaseResolutionBudget::default();
617 let xml_base_resolution = xml_base_resolution.unwrap_or(&default_xml_base_resolution);
618 match algo.mode {
622 C14nMode::Inclusive1_0 => {
623 let renderer = InclusiveNsRenderer;
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 C14nMode::Inclusive1_1 => {
638 let renderer = InclusiveNsRenderer;
639 let config = C14nConfig::for_mode(algo.mode);
640 serialize_canonical_visible_with_position_dispatch(
641 doc,
642 visibility,
643 algo.with_comments,
644 &renderer,
645 config,
646 tracked_element,
647 max_output_bytes,
648 xml_base_resolution,
649 output,
650 )
651 }
652 C14nMode::Exclusive1_0 => {
653 let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
654 let config = C14nConfig::for_mode(algo.mode);
655 serialize_canonical_visible_with_position_dispatch(
656 doc,
657 visibility,
658 algo.with_comments,
659 &renderer,
660 config,
661 tracked_element,
662 max_output_bytes,
663 xml_base_resolution,
664 output,
665 )
666 }
667 }
668}
669
670#[allow(clippy::too_many_arguments)]
671fn serialize_canonical_visible_with_position_dispatch<D: DomStorage>(
672 doc: &Document<'_, D>,
673 visibility: Option<&dyn NodeVisibility<D>>,
674 with_comments: bool,
675 renderer: &dyn serialize::NsRenderer<scratch::Native, D>,
676 config: C14nConfig,
677 tracked_element: Option<NodeId>,
678 max_output_bytes: Option<usize>,
679 xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
680 output: &mut Vec<u8>,
681) -> Result<Option<usize>, C14nError> {
682 match max_output_bytes {
683 Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
684 doc,
685 visibility,
686 with_comments,
687 renderer,
688 config,
689 CanonicalOutputOptions::bounded(tracked_element, max_output_bytes, xml_base_resolution),
690 output,
691 ),
692 None => serialize_canonical_visible_with_position_with_xml_base_budget(
693 doc,
694 visibility,
695 with_comments,
696 renderer,
697 config,
698 tracked_element,
699 xml_base_resolution,
700 output,
701 ),
702 }
703}
704
705pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
713 canonicalize_xml_with_backend(xml, algo, crate::XmlBackend::default())
714}
715
716pub fn canonicalize_xml_with_backend(
718 xml: &[u8],
719 algo: &C14nAlgorithm,
720 backend: crate::XmlBackend,
721) -> Result<Vec<u8>, C14nError> {
722 if xml.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
723 return Err(C14nError::Parse(format!(
724 "input exceeds maximum XML document size of {} bytes: got {}",
725 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
726 xml.len()
727 )));
728 }
729 let xml_str =
730 std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
731 let document = crate::document::parse_borrowed_with_settings_and_budget(
732 xml_str,
733 crate::document::DocumentParseSettings::default().with_backend(backend),
734 None,
735 )
736 .map_err(|error| C14nError::Parse(error.to_string()))?;
737 let mut output = Vec::new();
738 canonicalize(&document, None, algo, &mut output)?;
739 Ok(output)
740}
741
742pub fn canonicalize_document(
744 document: &crate::XmlDocument,
745 algo: &C14nAlgorithm,
746) -> Result<Vec<u8>, C14nError> {
747 let mut output = Vec::new();
748 document.with_view(|view| canonicalize(view.document(), None, algo, &mut output))?;
749 Ok(output)
750}
751
752#[cfg(test)]
753#[allow(clippy::unwrap_used)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn from_uri_roundtrip() {
759 let uris = [
760 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
761 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
762 "http://www.w3.org/2006/12/xml-c14n11",
763 "http://www.w3.org/2006/12/xml-c14n11#WithComments",
764 "http://www.w3.org/2001/10/xml-exc-c14n#",
765 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
766 ];
767 for uri in uris {
768 let algo = C14nAlgorithm::from_uri(uri).expect(uri);
769 assert_eq!(algo.uri(), uri);
770 }
771 }
772
773 #[test]
774 fn unknown_uri_returns_none() {
775 assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
776 }
777
778 #[test]
779 fn prefix_list_parsing() {
780 let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
781 .with_prefix_list("foo bar #default baz");
782 assert!(algo.inclusive_prefixes.contains("foo"));
783 assert!(algo.inclusive_prefixes.contains("bar"));
784 assert!(algo.inclusive_prefixes.contains("baz"));
785 assert!(algo.inclusive_prefixes.contains("")); assert_eq!(algo.inclusive_prefixes.len(), 4);
787 }
788
789 #[test]
790 fn canonicalize_xml_basic() {
791 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
792 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
793 let result = canonicalize_xml(xml, &algo).expect("c14n");
794 assert_eq!(
795 String::from_utf8(result).expect("utf8"),
796 r#"<root a="1" b="2"><empty></empty></root>"#
797 );
798 }
799
800 #[test]
801 fn canonicalize_xml_rejects_input_above_the_document_byte_ceiling_before_parsing() {
802 let xml = vec![b' '; crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1];
805 let error = match canonicalize_xml(&xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
806 {
807 Err(error) => error,
808 Ok(_) => panic!("oversized canonicalization input must be rejected"),
809 };
810 assert!(matches!(
811 error,
812 C14nError::Parse(message)
813 if message.contains("exceeds maximum XML document size")
814 ));
815 }
816
817 #[test]
818 fn canonicalize_xml_rejects_input_above_the_document_node_ceiling() {
819 let children = "<n/>".repeat(crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize);
822 let xml = format!("<root>{children}</root>");
823 let error = match canonicalize_xml(
824 xml.as_bytes(),
825 &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
826 ) {
827 Err(error) => error,
828 Ok(_) => panic!("excessive canonicalization nodes must be rejected"),
829 };
830 assert!(matches!(
831 error,
832 C14nError::Parse(message) if message.contains("nodes limit reached")
833 ));
834 }
835
836 #[test]
837 fn canonicalize_xml_rejects_input_above_the_document_depth_ceiling() {
838 let depth = crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING + 1;
841 let xml = format!("{}{}", "<n>".repeat(depth), "</n>".repeat(depth));
842
843 let error = canonicalize_xml(
844 xml.as_bytes(),
845 &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
846 )
847 .expect_err("over-depth canonicalization input must be rejected");
848
849 assert!(matches!(
850 error,
851 C14nError::Parse(message) if message.contains("maximum element depth")
852 ));
853 }
854
855 #[test]
856 fn canonicalize_xml_does_not_enable_dtd_or_external_entity_resolution() {
857 let xml = br#"<!DOCTYPE root [<!ENTITY value 'expanded'>]><root>&value;</root>"#;
860 let error = canonicalize_xml(xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
861 .expect_err("DTD input must remain disabled");
862 assert!(matches!(
863 error,
864 C14nError::Parse(message) if message.contains("DTD detected")
865 ));
866 }
867
868 #[test]
869 fn c14n_1_1_basic() {
870 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
872 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
873 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
874 assert_eq!(
875 String::from_utf8(result).expect("utf8"),
876 r#"<root a="1" b="2"><empty></empty></root>"#
877 );
878 }
879
880 #[test]
881 fn c14n_1_1_with_comments() {
882 let xml = b"<root><!-- comment -->text</root>";
883 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
884 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
885 assert_eq!(
886 String::from_utf8(result).expect("utf8"),
887 "<root><!-- comment -->text</root>"
888 );
889 }
890
891 #[test]
892 fn c14n_1_1_without_comments() {
893 let xml = b"<root><!-- comment -->text</root>";
894 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
895 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
896 assert_eq!(
897 String::from_utf8(result).expect("utf8"),
898 "<root>text</root>"
899 );
900 }
901
902 #[test]
903 fn c14n_1_1_namespaces() {
904 let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
906 let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
907 let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
908 let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
909 let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
910 assert_eq!(result_10, result_11);
912 }
913
914 #[test]
915 fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
916 use crate::xml::dom::Document;
919 use std::collections::HashSet;
920
921 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
922 let doc = Document::parse(xml).expect("parse");
923 let child = doc.root_element().first_element_child().expect("child");
924
925 let mut ids = HashSet::new();
927 let mut stack = vec![child];
928 while let Some(n) = stack.pop() {
929 ids.insert(n.id());
930 for c in n.children() {
931 stack.push(c);
932 }
933 }
934 let pred = move |n: crate::xml::dom::Node| ids.contains(&n.id());
935
936 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
937 let mut out = Vec::new();
938 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
939 let result = String::from_utf8(out).expect("utf8");
940
941 assert!(
942 !result.contains(r#"xml:id="r1""#),
943 "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
944 );
945 }
946
947 #[test]
948 fn c14n_1_0_xml_id_is_inherited_in_subset() {
949 use crate::xml::dom::Document;
952 use std::collections::HashSet;
953
954 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
955 let doc = Document::parse(xml).expect("parse");
956 let child = doc.root_element().first_element_child().expect("child");
957 let ids = child
958 .descendants()
959 .map(|node| node.id())
960 .collect::<HashSet<_>>();
961 let pred = move |node: crate::xml::dom::Node| ids.contains(&node.id());
962
963 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
964 let mut out = Vec::new();
965 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
966
967 assert_eq!(
968 String::from_utf8(out).expect("utf8"),
969 r#"<child xml:id="r1">text</child>"#
970 );
971 }
972
973 #[test]
974 fn bounded_canonicalization_stops_before_exceeding_the_limit() {
975 let xml = format!("<root>{}</root>", "x".repeat(4_096));
979 let document = Document::parse(&xml).expect("fixed XML must parse");
980 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
981 let mut output = Vec::new();
982
983 let error = canonicalize_with_visibility_and_position_bounded(
984 &document,
985 None,
986 &algorithm,
987 None,
988 64,
989 &mut output,
990 )
991 .expect_err("canonicalization must stop at the caller's byte ceiling");
992
993 assert!(is_output_limit_error(&error));
994 assert!(
995 output.len() <= 64,
996 "bounded output grew to {} bytes",
997 output.len()
998 );
999 }
1000
1001 #[test]
1002 fn c14n_1_1_bounds_inherited_xml_base_components() {
1003 let mut xml = String::new();
1007 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
1008 xml.push_str(r#"<n xml:base="segment/">"#);
1009 }
1010 xml.push_str("<leaf/>");
1011 for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
1012 xml.push_str("</n>");
1013 }
1014 let document = Document::parse(&xml).expect("fixed XML must parse");
1015 let leaf = document
1016 .descendants()
1017 .find(|node| node.has_tag_name("leaf"))
1018 .expect("leaf");
1019 let visible = |node: Node<'_, '_>| node == leaf;
1020 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
1021 let mut output = Vec::new();
1022
1023 let error = canonicalize_with_visibility(
1024 &document,
1025 Some(&ClosureVisibility {
1026 predicate: &visible,
1027 }),
1028 &algorithm,
1029 &mut output,
1030 )
1031 .expect_err("C14N must reject an overlong inherited xml:base chain");
1032
1033 assert!(
1034 error.to_string().contains("XML Base"),
1035 "unexpected C14N error: {error}"
1036 );
1037 }
1038
1039 #[test]
1040 fn unbounded_output_preserves_the_callers_xml_base_budget() {
1041 let document = Document::parse(
1044 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
1045 )
1046 .unwrap();
1047 let leaf = document
1048 .descendants()
1049 .find(|node| node.has_tag_name("leaf"))
1050 .unwrap();
1051 let visible = |node: Node<'_, '_>| node == leaf;
1052 let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
1053 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
1054 let mut output = Vec::new();
1055
1056 let error = canonicalize_with_visibility_and_position_impl(
1057 &document,
1058 Some(&ClosureVisibility {
1059 predicate: &visible,
1060 }),
1061 &algorithm,
1062 None,
1063 None,
1064 Some(&budget),
1065 &mut output,
1066 )
1067 .expect_err("the caller's component ceiling must survive unbounded dispatch");
1068
1069 assert!(matches!(
1070 error,
1071 C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
1072 ));
1073 }
1074}