1mod escape;
26mod ns_common;
27pub(crate) mod ns_exclusive;
28pub(crate) mod ns_inclusive;
29pub(crate) mod prefix;
30pub(crate) mod serialize;
31mod 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,
43 serialize_canonical_visible_with_position_bounded,
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("I/O error: {0}")]
163 Io(#[from] std::io::Error),
164}
165
166#[cfg(any(feature = "xmldsig", test))]
167pub(crate) fn is_output_limit_error(error: &C14nError) -> bool {
168 matches!(
169 error,
170 C14nError::Io(error)
171 if error
172 .get_ref()
173 .is_some_and(|source| source.is::<CanonicalOutputLimitExceeded>())
174 )
175}
176
177pub(crate) trait NodeVisibility {
184 fn contains_node(&self, node: Node<'_, '_>) -> bool;
185
186 fn contains_attribute(
187 &self,
188 owner: Node<'_, '_>,
189 namespace: Option<&str>,
190 local_name: &str,
191 ) -> bool;
192
193 fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool;
194}
195
196struct ClosureVisibility<'a> {
197 predicate: &'a dyn Fn(Node<'_, '_>) -> bool,
198}
199
200impl NodeVisibility for ClosureVisibility<'_> {
201 fn contains_node(&self, node: Node<'_, '_>) -> bool {
202 (self.predicate)(node)
203 }
204
205 fn contains_attribute(
206 &self,
207 owner: Node<'_, '_>,
208 _namespace: Option<&str>,
209 _local_name: &str,
210 ) -> bool {
211 (self.predicate)(owner)
212 }
213
214 fn contains_namespace(&self, owner: Node<'_, '_>, _prefix: &str, _uri: &str) -> bool {
215 (self.predicate)(owner)
216 }
217}
218
219pub fn canonicalize(
227 doc: &Document,
228 node_set: Option<&dyn Fn(Node) -> bool>,
229 algo: &C14nAlgorithm,
230 output: &mut Vec<u8>,
231) -> Result<(), C14nError> {
232 let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
233 canonicalize_with_visibility(
234 doc,
235 visibility
236 .as_ref()
237 .map(|visibility| visibility as &dyn NodeVisibility),
238 algo,
239 output,
240 )
241}
242
243pub(crate) fn canonicalize_with_visibility(
244 doc: &Document,
245 visibility: Option<&dyn NodeVisibility>,
246 algo: &C14nAlgorithm,
247 output: &mut Vec<u8>,
248) -> Result<(), C14nError> {
249 canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
250 Ok(())
251}
252
253pub(crate) fn canonicalize_with_visibility_and_position(
254 doc: &Document,
255 visibility: Option<&dyn NodeVisibility>,
256 algo: &C14nAlgorithm,
257 tracked_element: Option<NodeId>,
258 output: &mut Vec<u8>,
259) -> Result<Option<usize>, C14nError> {
260 canonicalize_with_visibility_and_position_impl(
261 doc,
262 visibility,
263 algo,
264 tracked_element,
265 None,
266 output,
267 )
268}
269
270#[cfg(any(feature = "xmldsig", test))]
271pub(crate) fn canonicalize_with_visibility_and_position_bounded(
272 doc: &Document,
273 visibility: Option<&dyn NodeVisibility>,
274 algo: &C14nAlgorithm,
275 tracked_element: Option<NodeId>,
276 max_output_bytes: usize,
277 output: &mut Vec<u8>,
278) -> Result<Option<usize>, C14nError> {
279 canonicalize_with_visibility_and_position_impl(
280 doc,
281 visibility,
282 algo,
283 tracked_element,
284 Some(max_output_bytes),
285 output,
286 )
287}
288
289fn canonicalize_with_visibility_and_position_impl(
290 doc: &Document,
291 visibility: Option<&dyn NodeVisibility>,
292 algo: &C14nAlgorithm,
293 tracked_element: Option<NodeId>,
294 max_output_bytes: Option<usize>,
295 output: &mut Vec<u8>,
296) -> Result<Option<usize>, C14nError> {
297 match algo.mode {
301 C14nMode::Inclusive1_0 => {
302 let renderer = InclusiveNsRenderer;
303 let config = C14nConfig {
304 inherit_xml_attrs: true,
305 fixup_xml_base: false,
306 };
307 serialize_canonical_visible_with_position_dispatch(
308 doc,
309 visibility,
310 algo.with_comments,
311 &renderer,
312 config,
313 tracked_element,
314 max_output_bytes,
315 output,
316 )
317 }
318 C14nMode::Inclusive1_1 => {
319 let renderer = InclusiveNsRenderer;
320 let config = C14nConfig {
321 inherit_xml_attrs: true,
322 fixup_xml_base: true,
323 };
324 serialize_canonical_visible_with_position_dispatch(
325 doc,
326 visibility,
327 algo.with_comments,
328 &renderer,
329 config,
330 tracked_element,
331 max_output_bytes,
332 output,
333 )
334 }
335 C14nMode::Exclusive1_0 => {
336 let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
337 let config = C14nConfig {
338 inherit_xml_attrs: false,
339 fixup_xml_base: false,
340 };
341 serialize_canonical_visible_with_position_dispatch(
342 doc,
343 visibility,
344 algo.with_comments,
345 &renderer,
346 config,
347 tracked_element,
348 max_output_bytes,
349 output,
350 )
351 }
352 }
353}
354
355#[allow(clippy::too_many_arguments)]
356fn serialize_canonical_visible_with_position_dispatch(
357 doc: &Document,
358 visibility: Option<&dyn NodeVisibility>,
359 with_comments: bool,
360 renderer: &dyn serialize::NsRenderer,
361 config: C14nConfig,
362 tracked_element: Option<NodeId>,
363 max_output_bytes: Option<usize>,
364 output: &mut Vec<u8>,
365) -> Result<Option<usize>, C14nError> {
366 match max_output_bytes {
367 Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
368 doc,
369 visibility,
370 with_comments,
371 renderer,
372 config,
373 CanonicalOutputOptions::bounded(tracked_element, max_output_bytes),
374 output,
375 ),
376 None => serialize_canonical_visible_with_position(
377 doc,
378 visibility,
379 with_comments,
380 renderer,
381 config,
382 tracked_element,
383 output,
384 ),
385 }
386}
387
388pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
394 let xml_str =
395 std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
396 let doc = Document::parse(xml_str).map_err(|e| C14nError::Parse(e.to_string()))?;
397 let mut output = Vec::new();
398 canonicalize(&doc, None, algo, &mut output)?;
399 Ok(output)
400}
401
402#[cfg(test)]
403#[allow(clippy::unwrap_used)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn from_uri_roundtrip() {
409 let uris = [
410 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
411 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
412 "http://www.w3.org/2006/12/xml-c14n11",
413 "http://www.w3.org/2006/12/xml-c14n11#WithComments",
414 "http://www.w3.org/2001/10/xml-exc-c14n#",
415 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
416 ];
417 for uri in uris {
418 let algo = C14nAlgorithm::from_uri(uri).expect(uri);
419 assert_eq!(algo.uri(), uri);
420 }
421 }
422
423 #[test]
424 fn unknown_uri_returns_none() {
425 assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
426 }
427
428 #[test]
429 fn prefix_list_parsing() {
430 let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
431 .with_prefix_list("foo bar #default baz");
432 assert!(algo.inclusive_prefixes.contains("foo"));
433 assert!(algo.inclusive_prefixes.contains("bar"));
434 assert!(algo.inclusive_prefixes.contains("baz"));
435 assert!(algo.inclusive_prefixes.contains("")); assert_eq!(algo.inclusive_prefixes.len(), 4);
437 }
438
439 #[test]
440 fn canonicalize_xml_basic() {
441 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
442 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
443 let result = canonicalize_xml(xml, &algo).expect("c14n");
444 assert_eq!(
445 String::from_utf8(result).expect("utf8"),
446 r#"<root a="1" b="2"><empty></empty></root>"#
447 );
448 }
449
450 #[test]
451 fn c14n_1_1_basic() {
452 let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
454 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
455 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
456 assert_eq!(
457 String::from_utf8(result).expect("utf8"),
458 r#"<root a="1" b="2"><empty></empty></root>"#
459 );
460 }
461
462 #[test]
463 fn c14n_1_1_with_comments() {
464 let xml = b"<root><!-- comment -->text</root>";
465 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
466 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
467 assert_eq!(
468 String::from_utf8(result).expect("utf8"),
469 "<root><!-- comment -->text</root>"
470 );
471 }
472
473 #[test]
474 fn c14n_1_1_without_comments() {
475 let xml = b"<root><!-- comment -->text</root>";
476 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
477 let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
478 assert_eq!(
479 String::from_utf8(result).expect("utf8"),
480 "<root>text</root>"
481 );
482 }
483
484 #[test]
485 fn c14n_1_1_namespaces() {
486 let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
488 let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
489 let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
490 let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
491 let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
492 assert_eq!(result_10, result_11);
494 }
495
496 #[test]
497 fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
498 use roxmltree::Document;
501 use std::collections::HashSet;
502
503 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
504 let doc = Document::parse(xml).expect("parse");
505 let child = doc.root_element().first_element_child().expect("child");
506
507 let mut ids = HashSet::new();
509 let mut stack = vec![child];
510 while let Some(n) = stack.pop() {
511 ids.insert(n.id());
512 for c in n.children() {
513 stack.push(c);
514 }
515 }
516 let pred = move |n: roxmltree::Node| ids.contains(&n.id());
517
518 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
519 let mut out = Vec::new();
520 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
521 let result = String::from_utf8(out).expect("utf8");
522
523 assert!(
524 !result.contains(r#"xml:id="r1""#),
525 "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
526 );
527 }
528
529 #[test]
530 fn c14n_1_0_xml_id_is_inherited_in_subset() {
531 use roxmltree::Document;
534 use std::collections::HashSet;
535
536 let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
537 let doc = Document::parse(xml).expect("parse");
538 let child = doc.root_element().first_element_child().expect("child");
539 let ids = child
540 .descendants()
541 .map(|node| node.id())
542 .collect::<HashSet<_>>();
543 let pred = move |node: roxmltree::Node| ids.contains(&node.id());
544
545 let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
546 let mut out = Vec::new();
547 canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
548
549 assert_eq!(
550 String::from_utf8(out).expect("utf8"),
551 r#"<child xml:id="r1">text</child>"#
552 );
553 }
554
555 #[test]
556 fn bounded_canonicalization_stops_before_exceeding_the_limit() {
557 let xml = format!("<root>{}</root>", "x".repeat(4_096));
561 let document = Document::parse(&xml).expect("fixed XML must parse");
562 let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
563 let mut output = Vec::new();
564
565 let error = canonicalize_with_visibility_and_position_bounded(
566 &document,
567 None,
568 &algorithm,
569 None,
570 64,
571 &mut output,
572 )
573 .expect_err("canonicalization must stop at the caller's byte ceiling");
574
575 assert!(is_output_limit_error(&error));
576 assert!(
577 output.len() <= 64,
578 "bounded output grew to {} bytes",
579 output.len()
580 );
581 }
582}