use std::collections::BTreeMap;
use crate::tree::{Document, NodeId, NodeKind};
#[derive(Debug, Clone)]
pub struct C14nOptions {
pub with_comments: bool,
pub exclusive: bool,
pub inclusive_prefixes: Vec<String>,
}
impl Default for C14nOptions {
fn default() -> Self {
Self {
with_comments: true,
exclusive: false,
inclusive_prefixes: Vec::new(),
}
}
}
#[must_use]
pub fn canonicalize(doc: &Document, options: &C14nOptions) -> String {
let mut ctx = C14nContext::new(doc, options);
ctx.process_document();
ctx.output
}
#[must_use]
pub fn canonicalize_subtree(doc: &Document, node: NodeId, options: &C14nOptions) -> String {
let mut ctx = C14nContext::new(doc, options);
ctx.process_node(node);
ctx.output
}
type NsBinding = BTreeMap<String, String>;
struct C14nContext<'a> {
doc: &'a Document,
options: &'a C14nOptions,
output: String,
rendered_ns_stack: Vec<NsBinding>,
}
impl<'a> C14nContext<'a> {
fn new(doc: &'a Document, options: &'a C14nOptions) -> Self {
let mut initial_bindings = NsBinding::new();
initial_bindings.insert(
"xml".to_string(),
"http://www.w3.org/XML/1998/namespace".to_string(),
);
Self {
doc,
options,
output: String::new(),
rendered_ns_stack: vec![initial_bindings],
}
}
fn process_document(&mut self) {
let root = self.doc.root();
let children: Vec<NodeId> = self.doc.children(root).collect();
let root_elem_index = children
.iter()
.position(|&id| matches!(self.doc.node(id).kind, NodeKind::Element { .. }));
for (i, &child) in children.iter().enumerate() {
match &self.doc.node(child).kind {
NodeKind::Comment { .. } if !self.options.with_comments => {}
NodeKind::Comment { content } => {
if let Some(root_idx) = root_elem_index {
if i < root_idx {
write_c14n_comment(&mut self.output, content);
self.output.push('\n');
} else if i > root_idx {
self.output.push('\n');
write_c14n_comment(&mut self.output, content);
}
} else {
write_c14n_comment(&mut self.output, content);
}
}
NodeKind::ProcessingInstruction { target, data } => {
if let Some(root_idx) = root_elem_index {
if i < root_idx {
write_c14n_pi(&mut self.output, target, data.as_deref());
self.output.push('\n');
} else if i > root_idx {
self.output.push('\n');
write_c14n_pi(&mut self.output, target, data.as_deref());
}
} else {
write_c14n_pi(&mut self.output, target, data.as_deref());
}
}
NodeKind::Element { .. } => {
self.process_element(child);
}
_ => {}
}
}
}
fn process_node(&mut self, id: NodeId) {
match &self.doc.node(id).kind {
NodeKind::Element { .. } => {
self.process_element(id);
}
NodeKind::Text { content } => {
write_c14n_text(&mut self.output, content);
}
NodeKind::CData { content } => {
write_c14n_text(&mut self.output, content);
}
NodeKind::Comment { content } => {
if self.options.with_comments {
write_c14n_comment(&mut self.output, content);
}
}
NodeKind::ProcessingInstruction { target, data } => {
write_c14n_pi(&mut self.output, target, data.as_deref());
}
NodeKind::EntityRef { name, .. } => {
let has_children = self.doc.first_child(id).is_some();
if has_children {
for child in self.doc.children(id) {
self.process_node(child);
}
} else {
let expanded = expand_predefined_entity(name);
write_c14n_text(&mut self.output, expanded);
}
}
NodeKind::DocumentType { .. } | NodeKind::Document => {
}
}
}
fn process_element(&mut self, id: NodeId) {
let (name, prefix, namespace, attributes) = match &self.doc.node(id).kind {
NodeKind::Element {
name,
prefix,
namespace,
attributes,
} => (
name.clone(),
prefix.clone(),
namespace.clone(),
attributes.clone(),
),
_ => return,
};
let qname = match &prefix {
Some(pfx) => format!("{pfx}:{name}"),
None => name.clone(),
};
let ns_to_output = self.compute_ns_declarations(
id,
&name,
prefix.as_deref(),
namespace.as_deref(),
&attributes,
);
self.output.push('<');
self.output.push_str(&qname);
self.write_ns_declarations(&ns_to_output);
self.write_sorted_attributes(&attributes);
self.output.push('>');
for child in self.doc.children(id) {
self.process_node(child);
}
self.output.push_str("</");
self.output.push_str(&qname);
self.output.push('>');
self.rendered_ns_stack.pop();
}
fn compute_ns_declarations(
&mut self,
id: NodeId,
name: &str,
prefix: Option<&str>,
namespace: Option<&str>,
attributes: &[crate::tree::Attribute],
) -> Vec<(String, String)> {
let ns_decls = if self.options.exclusive {
collect_exclusive_ns_decls(
&self.options.inclusive_prefixes,
prefix,
namespace,
attributes,
)
} else {
collect_inclusive_ns_decls(attributes)
};
let mut current_rendered = self.rendered_ns_stack.last().cloned().unwrap_or_default();
let mut ns_to_output: Vec<(String, String)> = Vec::new();
for (ns_prefix, ns_uri) in &ns_decls {
if current_rendered.get(ns_prefix) != Some(ns_uri) {
ns_to_output.push((ns_prefix.clone(), ns_uri.clone()));
current_rendered.insert(ns_prefix.clone(), ns_uri.clone());
}
}
self.check_default_ns_undeclaration(
&ns_decls,
attributes,
&mut ns_to_output,
&mut current_rendered,
);
let _ = (id, name);
ns_to_output.sort_by(|a, b| a.0.cmp(&b.0));
self.rendered_ns_stack.push(current_rendered);
ns_to_output
}
fn check_default_ns_undeclaration(
&self,
ns_decls: &[(String, String)],
attributes: &[crate::tree::Attribute],
ns_to_output: &mut Vec<(String, String)>,
current_rendered: &mut NsBinding,
) {
let parent_default = self
.rendered_ns_stack
.last()
.and_then(|m| m.get(""))
.cloned();
let has_current_default = ns_decls.iter().any(|(p, _)| p.is_empty());
if !has_current_default && parent_default.is_some() && parent_default.as_deref() != Some("")
{
let has_explicit_undecl = attributes
.iter()
.any(|a| a.prefix.is_none() && a.name == "xmlns" && a.value.is_empty());
if has_explicit_undecl {
ns_to_output.push((String::new(), String::new()));
current_rendered.insert(String::new(), String::new());
}
}
}
fn write_ns_declarations(&mut self, ns_to_output: &[(String, String)]) {
for (ns_prefix, ns_uri) in ns_to_output {
if ns_prefix.is_empty() {
self.output.push_str(" xmlns=\"");
} else {
self.output.push_str(" xmlns:");
self.output.push_str(ns_prefix);
self.output.push_str("=\"");
}
write_c14n_attr_value(&mut self.output, ns_uri);
self.output.push('"');
}
}
fn write_sorted_attributes(&mut self, attributes: &[crate::tree::Attribute]) {
let mut regular_attrs: Vec<_> = attributes
.iter()
.filter(|a| !is_ns_declaration(a))
.collect();
regular_attrs.sort_by(|a, b| {
let a_ns = a.namespace.as_deref().unwrap_or("");
let b_ns = b.namespace.as_deref().unwrap_or("");
match a_ns.cmp(b_ns) {
std::cmp::Ordering::Equal => a.name.cmp(&b.name),
other => other,
}
});
for attr in ®ular_attrs {
self.output.push(' ');
if let Some(pfx) = &attr.prefix {
self.output.push_str(pfx);
self.output.push(':');
}
self.output.push_str(&attr.name);
self.output.push_str("=\"");
write_c14n_attr_value(&mut self.output, &attr.value);
self.output.push('"');
}
}
}
fn is_ns_declaration(attr: &crate::tree::Attribute) -> bool {
attr.prefix.as_deref() == Some("xmlns") || (attr.prefix.is_none() && attr.name == "xmlns")
}
fn collect_inclusive_ns_decls(attributes: &[crate::tree::Attribute]) -> Vec<(String, String)> {
let mut decls = Vec::new();
for attr in attributes {
if attr.prefix.as_deref() == Some("xmlns") {
decls.push((attr.name.clone(), attr.value.clone()));
} else if attr.prefix.is_none() && attr.name == "xmlns" {
decls.push((String::new(), attr.value.clone()));
}
}
decls
}
fn collect_exclusive_ns_decls(
inclusive_prefixes: &[String],
elem_prefix: Option<&str>,
elem_ns: Option<&str>,
attributes: &[crate::tree::Attribute],
) -> Vec<(String, String)> {
let mut all_decls: BTreeMap<String, String> = BTreeMap::new();
for attr in attributes {
if attr.prefix.as_deref() == Some("xmlns") {
all_decls.insert(attr.name.clone(), attr.value.clone());
} else if attr.prefix.is_none() && attr.name == "xmlns" {
all_decls.insert(String::new(), attr.value.clone());
}
}
let mut utilized: Vec<String> = Vec::new();
if let Some(pfx) = elem_prefix {
utilized.push(pfx.to_string());
} else if elem_ns.is_some() {
utilized.push(String::new());
}
for attr in attributes {
if is_ns_declaration(attr) {
continue;
}
if let Some(pfx) = &attr.prefix {
if !utilized.contains(pfx) {
utilized.push(pfx.clone());
}
}
}
for pfx in inclusive_prefixes {
let key = if pfx == "#default" {
String::new()
} else {
pfx.clone()
};
if !utilized.contains(&key) {
utilized.push(key);
}
}
let mut available_bindings = all_decls;
if let (Some(pfx), Some(uri)) = (elem_prefix, elem_ns) {
available_bindings
.entry(pfx.to_string())
.or_insert_with(|| uri.to_string());
} else if let (None, Some(uri)) = (elem_prefix, elem_ns) {
available_bindings
.entry(String::new())
.or_insert_with(|| uri.to_string());
}
for attr in attributes {
if is_ns_declaration(attr) {
continue;
}
if let (Some(pfx), Some(uri)) = (&attr.prefix, &attr.namespace) {
available_bindings
.entry(pfx.clone())
.or_insert_with(|| uri.clone());
}
}
let mut result = Vec::new();
for pfx in &utilized {
if let Some(uri) = available_bindings.get(pfx) {
result.push((pfx.clone(), uri.clone()));
}
}
result
}
fn write_c14n_pi(out: &mut String, target: &str, data: Option<&str>) {
out.push_str("<?");
out.push_str(target);
if let Some(d) = data {
out.push(' ');
out.push_str(d);
}
out.push_str("?>");
}
fn write_c14n_comment(out: &mut String, content: &str) {
out.push_str("<!--");
out.push_str(content);
out.push_str("-->");
}
fn write_c14n_text(out: &mut String, text: &str) {
for ch in text.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'\r' => out.push_str("
"),
_ => out.push(ch),
}
}
}
fn write_c14n_attr_value(out: &mut String, text: &str) {
for ch in text.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'"' => out.push_str("""),
'\t' => out.push_str("	"),
'\n' => out.push_str("
"),
'\r' => out.push_str("
"),
_ => out.push(ch),
}
}
}
fn expand_predefined_entity(name: &str) -> &str {
match name {
"amp" => "&",
"lt" => "<",
"gt" => ">",
"apos" => "'",
"quot" => "\"",
_ => "",
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tree::Attribute;
fn c14n(xml: &str) -> String {
let doc = Document::parse_str(xml).unwrap();
canonicalize(&doc, &C14nOptions::default())
}
fn c14n_no_comments(xml: &str) -> String {
let doc = Document::parse_str(xml).unwrap();
canonicalize(
&doc,
&C14nOptions {
with_comments: false,
..C14nOptions::default()
},
)
}
#[test]
fn test_c14n_empty_element_uses_start_end_tags() {
let result = c14n("<root/>");
assert_eq!(result, "<root></root>");
let result = c14n("<root><child/></root>");
assert_eq!(result, "<root><child></child></root>");
}
#[test]
fn test_c14n_attribute_sorting() {
let result = c14n("<root z=\"1\" a=\"2\" m=\"3\"/>");
assert_eq!(result, "<root a=\"2\" m=\"3\" z=\"1\"></root>");
}
#[test]
fn test_c14n_namespace_declaration_ordering() {
let result = c14n("<root xmlns:z=\"http://z.example\" xmlns:a=\"http://a.example\"/>");
assert_eq!(
result,
"<root xmlns:a=\"http://a.example\" xmlns:z=\"http://z.example\"></root>"
);
}
#[test]
fn test_c14n_text_content_escaping() {
let mut doc = Document::new();
let root = doc.root();
let elem = doc.create_node(NodeKind::Element {
name: "root".to_string(),
prefix: None,
namespace: None,
attributes: vec![],
});
let text = doc.create_node(NodeKind::Text {
content: "a & b < c > d\re".to_string(),
});
doc.append_child(root, elem);
doc.append_child(elem, text);
let result = canonicalize(&doc, &C14nOptions::default());
assert_eq!(result, "<root>a & b < c > d
e</root>");
}
#[test]
fn test_c14n_attribute_value_escaping() {
let mut doc = Document::new();
let root = doc.root();
let elem = doc.create_node(NodeKind::Element {
name: "root".to_string(),
prefix: None,
namespace: None,
attributes: vec![Attribute {
name: "val".to_string(),
value: "a&b<c\"d\te\nf\rg".to_string(),
prefix: None,
namespace: None,
raw_value: None,
}],
});
doc.append_child(root, elem);
let result = canonicalize(&doc, &C14nOptions::default());
assert_eq!(
result,
"<root val=\"a&b<c"d	e
f
g\"></root>"
);
}
#[test]
fn test_c14n_no_xml_declaration() {
let result = c14n("<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>");
assert_eq!(result, "<root></root>");
assert!(!result.contains("<?xml"));
}
#[test]
fn test_c14n_cdata_replaced_with_escaped_text() {
let mut doc = Document::new();
let root = doc.root();
let elem = doc.create_node(NodeKind::Element {
name: "root".to_string(),
prefix: None,
namespace: None,
attributes: vec![],
});
let cdata = doc.create_node(NodeKind::CData {
content: "x < 1 && y > 2".to_string(),
});
doc.append_child(root, elem);
doc.append_child(elem, cdata);
let result = canonicalize(&doc, &C14nOptions::default());
assert_eq!(result, "<root>x < 1 && y > 2</root>");
}
#[test]
fn test_c14n_comments_included_by_default() {
let result = c14n("<root><!-- hello --></root>");
assert_eq!(result, "<root><!-- hello --></root>");
}
#[test]
fn test_c14n_comments_excluded_when_option_set() {
let result = c14n_no_comments("<root><!-- hello --></root>");
assert_eq!(result, "<root></root>");
}
#[test]
fn test_c14n_doctype_removed() {
let mut doc = Document::new();
let root = doc.root();
let doctype = doc.create_node(NodeKind::DocumentType {
name: "html".to_string(),
system_id: None,
public_id: None,
internal_subset: None,
});
let elem = doc.create_node(NodeKind::Element {
name: "html".to_string(),
prefix: None,
namespace: None,
attributes: vec![],
});
doc.append_child(root, doctype);
doc.append_child(root, elem);
let result = canonicalize(&doc, &C14nOptions::default());
assert_eq!(result, "<html></html>");
assert!(!result.contains("DOCTYPE"));
}
#[test]
fn test_c14n_processing_instructions_preserved() {
let result = c14n("<root><?target data?></root>");
assert_eq!(result, "<root><?target data?></root>");
}
#[test]
fn test_c14n_simple_document_roundtrip() {
let result = c14n("<root><a>hello</a><b>world</b></root>");
assert_eq!(result, "<root><a>hello</a><b>world</b></root>");
}
#[test]
fn test_c14n_namespace_handling_default() {
let result = c14n("<root xmlns=\"http://example.com\"/>");
assert_eq!(result, "<root xmlns=\"http://example.com\"></root>");
}
#[test]
fn test_c14n_namespace_handling_prefixed() {
let result = c14n("<ns:root xmlns:ns=\"http://example.com\"/>");
assert_eq!(
result,
"<ns:root xmlns:ns=\"http://example.com\"></ns:root>"
);
}
#[test]
fn test_c14n_whitespace_only_text_preserved() {
let result = c14n("<root> </root>");
assert_eq!(result, "<root> </root>");
let result = c14n("<root> \n </root>");
assert_eq!(result, "<root> \n </root>");
}
#[test]
fn test_c14n_exclusive_namespace_scoping() {
let doc = Document::parse_str(
"<root xmlns:a=\"http://a.example\" xmlns:b=\"http://b.example\">\
<a:child/></root>",
)
.unwrap();
let root_elem = doc.root_element().unwrap();
let child = doc.first_child(root_elem).unwrap();
let result = canonicalize_subtree(
&doc,
child,
&C14nOptions {
with_comments: true,
exclusive: true,
inclusive_prefixes: vec![],
},
);
assert!(result.contains("xmlns:a="));
assert!(!result.contains("xmlns:b="));
}
#[test]
fn test_c14n_complex_document_all_node_types() {
let mut doc = Document::new();
let root = doc.root();
let comment_before = doc.create_node(NodeKind::Comment {
content: " prologue comment ".to_string(),
});
doc.append_child(root, comment_before);
let pi_before = doc.create_node(NodeKind::ProcessingInstruction {
target: "app".to_string(),
data: Some("start".to_string()),
});
doc.append_child(root, pi_before);
let elem = doc.create_node(NodeKind::Element {
name: "root".to_string(),
prefix: None,
namespace: None,
attributes: vec![
Attribute {
name: "z".to_string(),
value: "1".to_string(),
prefix: None,
namespace: None,
raw_value: None,
},
Attribute {
name: "a".to_string(),
value: "2".to_string(),
prefix: None,
namespace: None,
raw_value: None,
},
],
});
doc.append_child(root, elem);
let text = doc.create_node(NodeKind::Text {
content: "hello".to_string(),
});
doc.append_child(elem, text);
let cdata = doc.create_node(NodeKind::CData {
content: "a<b".to_string(),
});
doc.append_child(elem, cdata);
let inner_comment = doc.create_node(NodeKind::Comment {
content: " inner ".to_string(),
});
doc.append_child(elem, inner_comment);
let inner_pi = doc.create_node(NodeKind::ProcessingInstruction {
target: "proc".to_string(),
data: None,
});
doc.append_child(elem, inner_pi);
let comment_after = doc.create_node(NodeKind::Comment {
content: " epilogue ".to_string(),
});
doc.append_child(root, comment_after);
let result = canonicalize(&doc, &C14nOptions::default());
assert_eq!(
result,
"<!-- prologue comment -->\n\
<?app start?>\n\
<root a=\"2\" z=\"1\">helloa<b<!-- inner --><?proc?></root>\n\
<!-- epilogue -->"
);
}
#[test]
fn test_c14n_subtree_serialization() {
let doc = Document::parse_str("<root><child attr=\"value\">text</child></root>").unwrap();
let root_elem = doc.root_element().unwrap();
let child = doc.first_child(root_elem).unwrap();
let result = canonicalize_subtree(&doc, child, &C14nOptions::default());
assert_eq!(result, "<child attr=\"value\">text</child>");
}
#[test]
fn test_c14n_redundant_namespace_not_redeclared() {
let result = c14n(
"<root xmlns=\"http://example.com\">\
<child xmlns=\"http://example.com\"/></root>",
);
assert_eq!(
result,
"<root xmlns=\"http://example.com\"><child></child></root>"
);
}
#[test]
fn test_c14n_mixed_namespace_and_regular_attrs() {
let result =
c14n("<root xmlns:b=\"http://b\" xmlns:a=\"http://a\" b:y=\"1\" a:x=\"2\" c=\"3\"/>");
assert_eq!(
result,
"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\" c=\"3\" a:x=\"2\" b:y=\"1\"></root>"
);
}
#[test]
fn test_c14n_pi_without_data() {
let result = c14n("<root><?target?></root>");
assert_eq!(result, "<root><?target?></root>");
}
#[test]
fn test_c14n_nested_elements() {
let result = c14n("<a><b><c/></b></a>");
assert_eq!(result, "<a><b><c></c></b></a>");
}
#[test]
fn test_c14n_exclusive_with_inclusive_prefixes() {
let doc = Document::parse_str(
"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\">\
<child/></root>",
)
.unwrap();
let root_elem = doc.root_element().unwrap();
let child = doc.first_child(root_elem).unwrap();
let result = canonicalize_subtree(
&doc,
child,
&C14nOptions {
with_comments: true,
exclusive: true,
inclusive_prefixes: vec!["b".to_string()],
},
);
assert_eq!(result, "<child></child>");
}
#[test]
fn test_c14n_document_comments_and_pis_spacing() {
let mut doc = Document::new();
let root = doc.root();
let pi = doc.create_node(NodeKind::ProcessingInstruction {
target: "before".to_string(),
data: None,
});
doc.append_child(root, pi);
let elem = doc.create_node(NodeKind::Element {
name: "root".to_string(),
prefix: None,
namespace: None,
attributes: vec![],
});
doc.append_child(root, elem);
let pi_after = doc.create_node(NodeKind::ProcessingInstruction {
target: "after".to_string(),
data: None,
});
doc.append_child(root, pi_after);
let result = canonicalize(&doc, &C14nOptions::default());
assert_eq!(result, "<?before?>\n<root></root>\n<?after?>");
}
#[test]
fn test_c14n_inclusive_xml_prefix_not_emitted() {
let result = c14n("<root xml:lang=\"en\">hello</root>");
assert_eq!(result, "<root xml:lang=\"en\">hello</root>");
assert!(
!result.contains("xmlns:xml"),
"implicit xml namespace should not be emitted, got: {result}"
);
}
#[test]
fn test_c14n_exclusive_xml_prefix_not_emitted_on_root() {
let doc = Document::parse_str("<root xml:lang=\"en\">hello</root>").unwrap();
let result = canonicalize(
&doc,
&C14nOptions {
with_comments: false,
exclusive: true,
inclusive_prefixes: vec![],
},
);
assert_eq!(result, "<root xml:lang=\"en\">hello</root>");
assert!(!result.contains("xmlns:xml"));
}
#[test]
fn test_c14n_exclusive_xml_prefix_not_emitted_on_subtree() {
let doc = Document::parse_str(
"<root xmlns=\"http://example.com\" xml:lang=\"en\">\
<child xml:space=\"preserve\">hi</child></root>",
)
.unwrap();
let root = doc.root_element().unwrap();
let child = doc
.children(root)
.find(|&n| doc.node_name(n) == Some("child"))
.unwrap();
let result = canonicalize_subtree(
&doc,
child,
&C14nOptions {
with_comments: false,
exclusive: true,
inclusive_prefixes: vec![],
},
);
assert!(
!result.contains("xmlns:xml"),
"implicit xml namespace should not be emitted in exclusive C14N, got: {result}"
);
assert!(result.contains("xmlns=\"http://example.com\""));
assert!(result.contains("xml:space=\"preserve\""));
}
#[test]
fn test_c14n_inclusive_emits_default_ns_undeclaration() {
let xml =
r#"<Envelope xmlns="http://example.org/usps"><NonNs xmlns="">child</NonNs></Envelope>"#;
let result = c14n(xml);
assert!(
result.contains("<NonNs xmlns=\"\">"),
"default namespace undeclaration missing, got: {result}"
);
}
#[test]
fn test_c14n_exclusive_emits_default_ns_undeclaration() {
let xml =
r#"<Envelope xmlns="http://example.org/usps"><NonNs xmlns="">child</NonNs></Envelope>"#;
let doc = Document::parse_str(xml).unwrap();
let result = canonicalize(
&doc,
&C14nOptions {
with_comments: false,
exclusive: true,
inclusive_prefixes: vec![],
},
);
assert!(
result.contains("<NonNs xmlns=\"\">"),
"exclusive C14N must emit xmlns=\"\" to undeclare inherited default ns, got: {result}"
);
}
#[test]
fn test_c14n_exclusive_no_undeclaration_when_no_inherited_default() {
let xml = r"<root><child>x</child></root>";
let doc = Document::parse_str(xml).unwrap();
let result = canonicalize(
&doc,
&C14nOptions {
with_comments: false,
exclusive: true,
inclusive_prefixes: vec![],
},
);
assert!(
!result.contains("xmlns=\"\""),
"exclusive C14N must not emit xmlns=\"\" when no inherited default to undeclare, got: {result}"
);
}
}