1#![forbid(unsafe_code)]
2
3use std::collections::HashSet;
18use std::io::{self, Write};
19
20use sup_xml_tree::dom::{Attribute, Document, Node, NodeKind};
21
22#[derive(Debug, Clone)]
26pub struct CanonicalizeOptions {
27 pub mode: C14nMode,
29 pub with_comments: bool,
34}
35
36impl Default for CanonicalizeOptions {
37 fn default() -> Self {
38 Self {
39 mode: C14nMode::C14n10,
40 with_comments: false,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum C14nMode {
48 C14n10,
54 ExcC14n10 {
60 inclusive_prefixes: Vec<String>,
66 },
67}
68
69pub enum VisitTarget<'a, 'doc> {
78 Node(&'a Node<'doc>),
83 Attribute(&'a Attribute<'doc>),
87}
88
89#[inline]
92pub fn include_all(_: VisitTarget<'_, '_>) -> bool {
93 true
94}
95
96pub fn canonicalize_with<W, F>(
106 doc: &Document,
107 opts: &CanonicalizeOptions,
108 out: &mut W,
109 is_visible: F,
110) -> io::Result<()>
111where
112 W: Write,
113 F: Fn(VisitTarget<'_, '_>) -> bool,
114{
115 let mut ctx = NsContext::new();
116 let root = doc.root();
123 let root_ptr = root as *const Node<'_>;
124 let mut head = root;
125 while let Some(prev) = head.prev_sibling.get() {
126 head = prev;
127 }
128 let mut seen_root = false;
129 let mut cur = Some(head);
130 while let Some(n) = cur {
131 if std::ptr::eq(n as *const Node<'_>, root_ptr) {
132 write_node(out, n, &mut ctx, opts, &is_visible)?;
133 seen_root = true;
134 } else if !matches!(n.kind, NodeKind::Comment) || opts.with_comments {
135 if seen_root {
136 out.write_all(b"\n")?;
137 write_node(out, n, &mut ctx, opts, &is_visible)?;
138 } else {
139 write_node(out, n, &mut ctx, opts, &is_visible)?;
140 out.write_all(b"\n")?;
141 }
142 }
143 cur = n.next_sibling.get();
144 }
145 Ok(())
146}
147
148pub fn canonicalize_node_with<W, F>(
152 node: &Node<'_>,
153 opts: &CanonicalizeOptions,
154 out: &mut W,
155 is_visible: F,
156) -> io::Result<()>
157where
158 W: Write,
159 F: Fn(VisitTarget<'_, '_>) -> bool,
160{
161 let mut ctx = NsContext::new();
162 write_node(out, node, &mut ctx, opts, &is_visible)
163}
164
165pub fn canonicalize_to_bytes(doc: &Document, opts: &CanonicalizeOptions) -> Vec<u8> {
171 let mut buf = Vec::with_capacity(estimate_capacity(doc));
172 canonicalize_with(doc, opts, &mut buf, include_all)
173 .expect("writes into Vec<u8> are infallible");
174 buf
175}
176
177pub fn canonicalize_node_to_bytes(node: &Node<'_>, opts: &CanonicalizeOptions) -> Vec<u8> {
181 let mut buf = Vec::with_capacity(2048);
182 canonicalize_node_with(node, opts, &mut buf, include_all)
183 .expect("writes into Vec<u8> are infallible");
184 buf
185}
186
187struct NsContext {
193 frames: Vec<NsFrame>,
194}
195
196struct NsFrame {
197 declared: Vec<(String, String)>,
200 rendered: Vec<(String, String)>,
202}
203
204impl NsContext {
205 fn new() -> Self {
206 Self { frames: Vec::with_capacity(16) }
207 }
208
209 fn push_frame(&mut self) {
210 self.frames.push(NsFrame {
211 declared: Vec::new(),
212 rendered: Vec::new(),
213 });
214 }
215
216 fn pop_frame(&mut self) {
217 self.frames.pop();
218 }
219
220 fn declare(&mut self, prefix: &str, uri: &str) {
221 if let Some(frame) = self.frames.last_mut() {
222 frame.declared.push((prefix.to_string(), uri.to_string()));
223 }
224 }
225
226 fn record_rendered(&mut self, prefix: &str, uri: &str) {
227 if let Some(frame) = self.frames.last_mut() {
228 frame.rendered.push((prefix.to_string(), uri.to_string()));
229 }
230 }
231
232 fn lookup(&self, prefix: &str) -> Option<&str> {
234 for frame in self.frames.iter().rev() {
235 for (p, u) in frame.declared.iter().rev() {
236 if p == prefix {
237 return Some(u);
238 }
239 }
240 }
241 if prefix == "xml" {
243 return Some("http://www.w3.org/XML/1998/namespace");
244 }
245 None
246 }
247
248 fn already_rendered(&self, prefix: &str, uri: &str) -> bool {
250 let last = self.frames.len();
251 if last == 0 {
252 return false;
253 }
254 for frame in &self.frames[..last - 1] {
255 for (p, u) in &frame.rendered {
256 if p == prefix && u == uri {
257 return true;
258 }
259 }
260 }
261 false
262 }
263
264 fn ancestor_rendered_different(&self, prefix: &str, uri: &str) -> bool {
267 let last = self.frames.len();
268 if last == 0 {
269 return false;
270 }
271 for frame in self.frames[..last - 1].iter().rev() {
272 for (p, u) in frame.rendered.iter().rev() {
273 if p == prefix {
274 return u != uri;
275 }
276 }
277 }
278 false
279 }
280}
281
282fn write_node(
285 out: &mut dyn Write,
286 node: &Node<'_>,
287 ctx: &mut NsContext,
288 opts: &CanonicalizeOptions,
289 is_visible: &dyn Fn(VisitTarget<'_, '_>) -> bool,
290) -> io::Result<()> {
291 if !is_visible(VisitTarget::Node(node)) {
292 return Ok(());
294 }
295 match node.kind {
296 NodeKind::Element => write_element(out, node, ctx, opts, is_visible)?,
297 NodeKind::Text => write_text_canonical(out, node.content())?,
298 NodeKind::CData => {
299 write_text_canonical(out, node.content())?;
301 }
302 NodeKind::Comment => {
303 if opts.with_comments {
304 out.write_all(b"<!--")?;
305 out.write_all(node.content().as_bytes())?;
306 out.write_all(b"-->")?;
307 }
308 }
309 NodeKind::DtdDecl => {}
318 NodeKind::Dtd => {}
319 NodeKind::Attribute => unreachable!("Attribute kind never appears on a Node"),
320 NodeKind::Document => unreachable!("Document kind never appears on a Node"),
321 NodeKind::DocumentFragment => {
327 for c in node.children() {
328 write_node(out, c, ctx, opts, is_visible)?;
329 }
330 }
331 NodeKind::EntityRef => {
332 out.write_all(node.content().as_bytes())?;
342 }
343 NodeKind::Pi => {
344 out.write_all(b"<?")?;
345 out.write_all(node.name().as_bytes())?;
346 let content = node.content();
347 if !content.is_empty() {
348 out.write_all(b" ")?;
349 out.write_all(content.as_bytes())?;
350 }
351 out.write_all(b"?>")?;
352 }
353 }
354 Ok(())
355}
356
357fn write_element(
358 out: &mut dyn Write,
359 el: &Node<'_>,
360 ctx: &mut NsContext,
361 opts: &CanonicalizeOptions,
362 is_visible: &dyn Fn(VisitTarget<'_, '_>) -> bool,
363) -> io::Result<()> {
364 ctx.push_frame();
365
366 let mut regular_attrs: Vec<(Option<&str>, &str, Option<&str>, &str, &str)> = Vec::new();
377 #[cfg(feature = "c-abi")]
378 {
379 let mut ns_cur = el.ns_def.get();
380 while let Some(ns) = ns_cur {
381 match ns.prefix() {
382 None => ctx.declare("", ns.href()),
383 Some(p) => ctx.declare(p, ns.href()),
384 }
385 ns_cur = ns.next.get();
386 }
387 }
388 for attr in el.attributes() {
389 let aname: &str = attr.name();
390 if aname == "xmlns" {
391 ctx.declare("", attr.value());
392 } else if let Some(rest) = aname.strip_prefix("xmlns:") {
393 ctx.declare(rest, attr.value());
394 } else if is_visible(VisitTarget::Attribute(attr)) {
395 #[cfg(feature = "c-abi")]
396 let ns_prefix: Option<&str> = attr.namespace.get().and_then(|ns| ns.prefix());
397 #[cfg(not(feature = "c-abi"))]
398 let ns_prefix: Option<&str> = None;
399 let (name_prefix, local) = split_qname(aname);
400 let eff_prefix = ns_prefix.or(name_prefix);
401 regular_attrs.push((ns_prefix, aname, eff_prefix, local, attr.value()));
402 }
403 }
404
405 let elem_name: &str = el.name();
416 #[cfg(feature = "c-abi")]
417 let ns_prefix: Option<&str> = el.namespace.get().and_then(|ns| ns.prefix());
418 #[cfg(not(feature = "c-abi"))]
419 let ns_prefix: Option<&str> = None;
420 let effective_prefix = ns_prefix.or_else(|| split_qname(elem_name).0);
421 let visibly_used = collect_visibly_used(effective_prefix, ®ular_attrs, opts);
422
423 out.write_all(b"<")?;
424 write_qname(out, ns_prefix, elem_name)?;
425 write_namespace_decls(out, ctx, opts, &visibly_used)?;
426 write_attributes(out, ctx, &mut regular_attrs)?;
427 out.write_all(b">")?;
428
429 for child in el.children() {
430 write_node(out, child, ctx, opts, is_visible)?;
431 }
432
433 out.write_all(b"</")?;
435 write_qname(out, ns_prefix, elem_name)?;
436 out.write_all(b">")?;
437
438 ctx.pop_frame();
439 Ok(())
440}
441
442fn collect_visibly_used(
444 elem_prefix: Option<&str>,
445 attrs: &[(Option<&str>, &str, Option<&str>, &str, &str)],
446 opts: &CanonicalizeOptions,
447) -> HashSet<String> {
448 let mut used: HashSet<String> = HashSet::with_capacity(8);
449 used.insert(elem_prefix.unwrap_or("").to_string());
451
452 for (_ns_prefix, _name, eff_prefix, _local, _value) in attrs {
454 if let Some(p) = eff_prefix {
455 used.insert(p.to_string());
456 }
457 }
458
459 if let C14nMode::ExcC14n10 { inclusive_prefixes } = &opts.mode {
460 for p in inclusive_prefixes {
461 used.insert(p.clone());
462 }
463 }
464
465 used
466}
467
468fn write_namespace_decls(
471 out: &mut dyn Write,
472 ctx: &mut NsContext,
473 opts: &CanonicalizeOptions,
474 visibly_used: &HashSet<String>,
475) -> io::Result<()> {
476 let mut to_render: Vec<(String, String)> = Vec::new();
477
478 match &opts.mode {
479 C14nMode::C14n10 => {
480 let mut seen_prefixes: HashSet<String> = HashSet::new();
483 for frame in ctx.frames.iter().rev() {
484 for (prefix, uri) in &frame.declared {
485 if seen_prefixes.insert(prefix.clone()) {
486 if uri.is_empty() && prefix.is_empty() {
487 if ctx.ancestor_rendered_different(prefix, uri) {
490 to_render.push((prefix.clone(), uri.clone()));
491 }
492 } else if !ctx.already_rendered(prefix, uri) {
493 to_render.push((prefix.clone(), uri.clone()));
494 }
495 }
496 }
497 }
498 }
499 C14nMode::ExcC14n10 { .. } => {
500 for prefix in visibly_used {
503 let uri = ctx.lookup(prefix).unwrap_or("");
504 if prefix.is_empty() && uri.is_empty() {
505 if ctx.ancestor_rendered_different(prefix, uri) {
506 to_render.push((prefix.clone(), String::new()));
507 }
508 continue;
509 }
510 if prefix == "xml" && uri == "http://www.w3.org/XML/1998/namespace" {
512 continue;
513 }
514 if !ctx.already_rendered(prefix, uri) {
515 to_render.push((prefix.clone(), uri.to_string()));
516 }
517 }
518 }
519 }
520
521 to_render.sort_by(|a, b| a.0.cmp(&b.0));
523
524 for (prefix, uri) in &to_render {
525 out.write_all(b" ")?;
526 if prefix.is_empty() {
527 out.write_all(b"xmlns=\"")?;
528 } else {
529 out.write_all(b"xmlns:")?;
530 out.write_all(prefix.as_bytes())?;
531 out.write_all(b"=\"")?;
532 }
533 write_attr_value_canonical(out, uri)?;
534 out.write_all(b"\"")?;
535 ctx.record_rendered(prefix, uri);
536 }
537 Ok(())
538}
539
540fn write_attributes(
543 out: &mut dyn Write,
544 ctx: &NsContext,
545 attrs: &mut [(Option<&str>, &str, Option<&str>, &str, &str)],
546) -> io::Result<()> {
547 attrs.sort_by(|a, b| {
551 let a_ns = a.2.and_then(|p| ctx.lookup(p)).unwrap_or("");
552 let b_ns = b.2.and_then(|p| ctx.lookup(p)).unwrap_or("");
553 match a_ns.cmp(b_ns) {
554 std::cmp::Ordering::Equal => a.3.cmp(b.3),
555 ord => ord,
556 }
557 });
558
559 for (ns_prefix, name, _eff_prefix, _local, value) in attrs {
560 out.write_all(b" ")?;
561 write_qname(out, *ns_prefix, name)?;
562 out.write_all(b"=\"")?;
563 write_attr_value_canonical(out, value)?;
564 out.write_all(b"\"")?;
565 }
566 Ok(())
567}
568
569fn write_qname(out: &mut dyn Write, ns_prefix: Option<&str>, name: &str) -> io::Result<()> {
573 if let Some(p) = ns_prefix {
574 out.write_all(p.as_bytes())?;
575 out.write_all(b":")?;
576 }
577 out.write_all(name.as_bytes())
578}
579
580fn write_text_canonical(out: &mut dyn Write, s: &str) -> io::Result<()> {
584 let bytes = s.as_bytes();
587 let mut start = 0;
588 for (i, &b) in bytes.iter().enumerate() {
589 let replacement: &[u8] = match b {
590 b'&' => b"&",
591 b'<' => b"<",
592 b'>' => b">",
593 b'\r' => b"
",
594 _ => continue,
595 };
596 if start < i {
597 out.write_all(&bytes[start..i])?;
598 }
599 out.write_all(replacement)?;
600 start = i + 1;
601 }
602 if start < bytes.len() {
603 out.write_all(&bytes[start..])?;
604 }
605 Ok(())
606}
607
608fn write_attr_value_canonical(out: &mut dyn Write, s: &str) -> io::Result<()> {
610 let bytes = s.as_bytes();
611 let mut start = 0;
612 for (i, &b) in bytes.iter().enumerate() {
613 let replacement: &[u8] = match b {
614 b'&' => b"&",
615 b'<' => b"<",
616 b'"' => b""",
617 b'\t' => b"	",
618 b'\n' => b"
",
619 b'\r' => b"
",
620 _ => continue,
621 };
622 if start < i {
623 out.write_all(&bytes[start..i])?;
624 }
625 out.write_all(replacement)?;
626 start = i + 1;
627 }
628 if start < bytes.len() {
629 out.write_all(&bytes[start..])?;
630 }
631 Ok(())
632}
633
634fn split_qname(name: &str) -> (Option<&str>, &str) {
638 match name.find(':') {
639 Some(idx) => (Some(&name[..idx]), &name[idx + 1..]),
640 None => (None, name),
641 }
642}
643
644fn estimate_capacity(_doc: &Document) -> usize {
645 4096
647}
648
649#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::parser::parse_str;
655 use crate::options::ParseOptions;
656
657 fn c14n(xml: &str, mode: C14nMode, with_comments: bool) -> String {
658 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
659 let bytes = canonicalize_to_bytes(
660 &doc,
661 &CanonicalizeOptions { mode, with_comments },
662 );
663 String::from_utf8(bytes).expect("c14n produces UTF-8")
664 }
665
666 fn c14n10(xml: &str) -> String {
667 c14n(xml, C14nMode::C14n10, false)
668 }
669
670 fn exc_c14n10(xml: &str) -> String {
671 c14n(xml, C14nMode::ExcC14n10 { inclusive_prefixes: vec![] }, false)
672 }
673
674 #[test]
677 fn simple_element_round_trips_to_explicit_close() {
678 let out = c14n10("<r/>");
679 assert_eq!(out, "<r></r>");
680 }
681
682 #[test]
683 fn empty_element_with_attrs() {
684 let out = c14n10(r#"<r a="1"/>"#);
685 assert_eq!(out, r#"<r a="1"></r>"#);
686 }
687
688 #[test]
689 fn xml_decl_dropped() {
690 let out = c14n10(r#"<?xml version="1.0"?><r/>"#);
691 assert!(!out.contains("<?xml"));
692 assert_eq!(out, "<r></r>");
693 }
694
695 #[test]
696 fn attribute_value_quotes_canonical() {
697 let out = c14n10("<r a='1'/>");
698 assert_eq!(out, r#"<r a="1"></r>"#);
699 }
700
701 #[test]
704 fn attribute_value_escapes_quote_and_control() {
705 let out = c14n10("<r a=\"a&b<c	d\"/>");
706 assert_eq!(out, r#"<r a="a&b<c	d"></r>"#);
707 }
708
709 #[test]
712 fn text_escapes_amp_lt_gt() {
713 let out = c14n10("<r>a&b<c>d</r>");
714 assert_eq!(out, "<r>a&b<c>d</r>");
715 }
716
717 #[test]
718 fn text_does_not_escape_tab_newline() {
719 let out = c14n10("<r>a\tb\nc</r>");
720 assert_eq!(out, "<r>a\tb\nc</r>");
721 }
722
723 #[test]
726 fn attributes_sorted_lexicographically_when_no_namespace() {
727 let out = c14n10(r#"<r z="1" a="2" m="3"/>"#);
728 assert_eq!(out, r#"<r a="2" m="3" z="1"></r>"#);
729 }
730
731 #[test]
732 fn namespace_decls_come_before_attributes() {
733 let out = c14n10(r#"<r a="1" xmlns:b="urn:b" b:c="2"/>"#);
734 assert_eq!(out, r#"<r xmlns:b="urn:b" a="1" b:c="2"></r>"#);
735 }
736
737 #[test]
738 fn default_namespace_sorts_before_prefixed_namespace() {
739 let out = c14n10(r#"<r xmlns:b="urn:b" xmlns="urn:default"/>"#);
740 assert_eq!(out, r#"<r xmlns="urn:default" xmlns:b="urn:b"></r>"#);
741 }
742
743 #[test]
746 fn c14n10_does_not_repeat_inherited_namespace() {
747 let out = c14n10(r#"<outer xmlns:a="urn:a"><inner/></outer>"#);
748 assert_eq!(out, r#"<outer xmlns:a="urn:a"><inner></inner></outer>"#);
749 }
750
751 #[test]
752 fn c14n10_renders_inherited_when_subtree_uses_prefix() {
753 let out = c14n10(r#"<outer xmlns:a="urn:a"><a:inner/></outer>"#);
754 assert_eq!(out, r#"<outer xmlns:a="urn:a"><a:inner></a:inner></outer>"#);
755 }
756
757 #[test]
760 fn exc_c14n_omits_unused_inherited_namespace() {
761 let out = exc_c14n10(r#"<a:outer xmlns:a="urn:a"><inner/></a:outer>"#);
762 assert_eq!(out, r#"<a:outer xmlns:a="urn:a"><inner></inner></a:outer>"#);
763 }
764
765 #[test]
766 fn exc_c14n_renders_namespace_for_used_prefix() {
767 let out = exc_c14n10(r#"<outer xmlns:a="urn:a"><a:inner/></outer>"#);
768 assert_eq!(out, r#"<outer><a:inner xmlns:a="urn:a"></a:inner></outer>"#);
769 }
770
771 #[test]
772 fn exc_c14n_inclusive_prefix_list() {
773 let doc = parse_str(
774 r#"<outer xmlns:a="urn:a"><inner/></outer>"#,
775 &ParseOptions::default(),
776 )
777 .unwrap();
778 let bytes = canonicalize_to_bytes(
779 &doc,
780 &CanonicalizeOptions {
781 mode: C14nMode::ExcC14n10 {
782 inclusive_prefixes: vec!["a".into()],
783 },
784 with_comments: false,
785 },
786 );
787 let s = String::from_utf8(bytes).unwrap();
788 assert_eq!(
789 s,
790 r#"<outer xmlns:a="urn:a"><inner></inner></outer>"#,
791 "inclusive-prefixes adds `a` to the visibly-used set on both elements, but standard de-dup means inner doesn't re-render an inherited binding"
792 );
793 }
794
795 #[test]
798 fn comments_omitted_by_default() {
799 let out = c14n10("<r><!-- hi --></r>");
800 assert_eq!(out, "<r></r>");
801 }
802
803 #[test]
804 fn comments_included_when_with_comments() {
805 let out = c14n("<r><!-- hi --></r>", C14nMode::C14n10, true);
806 assert_eq!(out, "<r><!-- hi --></r>");
807 }
808
809 #[test]
812 fn processing_instruction_preserved() {
813 let out = c14n10(r#"<r><?target value?></r>"#);
814 assert_eq!(out, r#"<r><?target value?></r>"#);
815 }
816
817 #[test]
820 fn cdata_section_becomes_text() {
821 let out = c14n10("<r><![CDATA[<raw>&]]></r>");
822 assert_eq!(out, "<r><raw>&</r>");
823 }
824
825 #[test]
828 fn c14n_is_idempotent() {
829 let xml = r#"<r xmlns:b='urn:b' a='1' z='2' b:x="hi"><child/></r>"#;
830 let once = c14n10(xml);
831 let twice = c14n10(&once);
832 assert_eq!(once, twice, "c14n must be idempotent");
833 }
834
835 #[test]
836 fn exc_c14n_is_idempotent() {
837 let xml = r#"<r xmlns:b='urn:b' a='1' z='2' b:x="hi"><b:child/></r>"#;
838 let once = exc_c14n10(xml);
839 let twice = exc_c14n10(&once);
840 assert_eq!(once, twice, "exc-c14n must be idempotent");
841 }
842
843 #[test]
846 fn canonicalize_node_works_on_subtree() {
847 let doc = parse_str(
848 r#"<root><target a="1"><child/></target></root>"#,
849 &ParseOptions::default(),
850 )
851 .unwrap();
852 let target = doc.root().first_child.get().expect("target child");
854 let bytes = canonicalize_node_to_bytes(target, &CanonicalizeOptions::default());
855 assert_eq!(
856 String::from_utf8(bytes).unwrap(),
857 r#"<target a="1"><child></child></target>"#
858 );
859 }
860
861 #[test]
864 fn canonicalize_with_matches_canonicalize_to_bytes() {
865 let xml = r#"<r xmlns:b='urn:b' a='1' z='2' b:x="hi"><c/><!--note--></r>"#;
868 let doc = parse_str(xml, &ParseOptions::default()).unwrap();
869 let opts = CanonicalizeOptions { mode: C14nMode::C14n10, with_comments: true };
870
871 let bulk = canonicalize_to_bytes(&doc, &opts);
872 let mut streamed = Vec::new();
873 canonicalize_with(&doc, &opts, &mut streamed, include_all).unwrap();
874 assert_eq!(bulk, streamed);
875 }
876
877 #[test]
878 fn canonicalize_with_skips_subtree_for_hidden_element() {
879 let doc = parse_str(
880 r#"<r><keep>x</keep><secret><nested/></secret><also/></r>"#,
881 &ParseOptions::default(),
882 )
883 .unwrap();
884 let mut out = Vec::new();
885 canonicalize_with(&doc, &CanonicalizeOptions::default(), &mut out, |t| {
886 match t {
887 VisitTarget::Node(n) => n.name() != "secret",
888 VisitTarget::Attribute(_) => true,
889 }
890 })
891 .unwrap();
892 let s = String::from_utf8(out).unwrap();
893 assert_eq!(s, "<r><keep>x</keep><also></also></r>");
894 }
895
896 #[test]
897 fn canonicalize_with_skips_individual_attribute() {
898 let doc = parse_str(
899 r#"<r keep="1" drop="2" also="3"/>"#,
900 &ParseOptions::default(),
901 )
902 .unwrap();
903 let mut out = Vec::new();
904 canonicalize_with(&doc, &CanonicalizeOptions::default(), &mut out, |t| {
905 match t {
906 VisitTarget::Attribute(a) => a.name() != "drop",
907 VisitTarget::Node(_) => true,
908 }
909 })
910 .unwrap();
911 let s = String::from_utf8(out).unwrap();
912 assert_eq!(s, r#"<r also="3" keep="1"></r>"#);
913 }
914
915 #[test]
916 fn canonicalize_with_streams_incrementally() {
917 struct CountingSink {
921 buf: Vec<u8>,
922 chunks: usize,
923 }
924 impl std::io::Write for CountingSink {
925 fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
926 if !b.is_empty() {
927 self.chunks += 1;
928 self.buf.extend_from_slice(b);
929 }
930 Ok(b.len())
931 }
932 fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
933 }
934
935 let doc = parse_str(
936 r#"<r><a/><b/><c/></r>"#,
937 &ParseOptions::default(),
938 )
939 .unwrap();
940 let mut sink = CountingSink { buf: Vec::new(), chunks: 0 };
941 canonicalize_with(
942 &doc, &CanonicalizeOptions::default(), &mut sink, include_all,
943 )
944 .unwrap();
945 assert!(
946 sink.chunks > 1,
947 "expected multiple incremental writes, got {} for {:?}",
948 sink.chunks,
949 String::from_utf8_lossy(&sink.buf),
950 );
951 assert_eq!(
952 String::from_utf8(sink.buf).unwrap(),
953 "<r><a></a><b></b><c></c></r>",
954 );
955 }
956
957 #[test]
958 fn canonicalize_with_propagates_sink_errors() {
959 struct FlakySink { remaining: usize }
962 impl std::io::Write for FlakySink {
963 fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
964 if self.remaining == 0 {
965 return Err(std::io::Error::other("boom"));
966 }
967 self.remaining -= 1;
968 Ok(b.len())
969 }
970 fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
971 }
972 let doc = parse_str(r#"<r><a/><b/></r>"#, &ParseOptions::default()).unwrap();
973 let mut sink = FlakySink { remaining: 1 };
974 let err = canonicalize_with(
975 &doc, &CanonicalizeOptions::default(), &mut sink, include_all,
976 )
977 .unwrap_err();
978 assert_eq!(err.to_string(), "boom");
979 }
980}