use crate::dsig::algorithms::C14nAlgorithm;
use crate::error::Error;
use crate::xml::parse::{Document, Element, Node, QName};
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
pub(crate) fn canonicalize(
_document: &Document,
element: &Element,
ancestor_chain: &[&Element],
algorithm: C14nAlgorithm,
inclusive_namespace_prefixes: &[&str],
) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
let rendered: Vec<(Option<String>, String)> = Vec::new();
emit_element(
element,
ancestor_chain,
&rendered,
true,
algorithm,
inclusive_namespace_prefixes,
&mut out,
)?;
Ok(out)
}
fn emit_element(
element: &Element,
ancestor_chain: &[&Element],
rendered_above: &[(Option<String>, String)],
is_apex: bool,
algorithm: C14nAlgorithm,
inclusive_namespace_prefixes: &[&str],
out: &mut Vec<u8>,
) -> Result<(), Error> {
let to_render = compute_rendered_namespaces(
element,
ancestor_chain,
rendered_above,
is_apex,
algorithm,
inclusive_namespace_prefixes,
)?;
let elem_prefix = lookup_prefix_for_uri(
element.qname().namespace(),
ancestor_chain,
element,
false,
element.source_prefix.as_deref(),
)?;
out.push(b'<');
push_qname(out, elem_prefix.as_deref(), element.qname().local());
let mut ns_sorted = to_render;
ns_sorted.sort_by(|a, b| ns_sort_key(a).cmp(&ns_sort_key(b)));
for (prefix, uri) in &ns_sorted {
out.push(b' ');
match prefix {
None => out.extend_from_slice(b"xmlns"),
Some(p) => {
out.extend_from_slice(b"xmlns:");
out.extend_from_slice(p.as_bytes());
}
}
out.extend_from_slice(b"=\"");
push_attr_value_escaped(out, uri);
out.push(b'"');
}
let mut attrs_sorted: Vec<(&QName, Option<&str>, &str)> =
element.attributes_with_source_prefix().collect();
attrs_sorted.sort_by(|(a, _, _), (b, _, _)| attr_sort_key(a).cmp(&attr_sort_key(b)));
for (qname, source_prefix, value) in &attrs_sorted {
out.push(b' ');
let attr_prefix = lookup_prefix_for_uri(
qname.namespace(),
ancestor_chain,
element,
true,
*source_prefix,
)?;
push_qname(out, attr_prefix.as_deref(), qname.local());
out.extend_from_slice(b"=\"");
push_attr_value_escaped(out, value);
out.push(b'"');
}
out.push(b'>');
let mut rendered_for_children: Vec<(Option<String>, String)> =
Vec::with_capacity(rendered_above.len().saturating_add(ns_sorted.len()));
rendered_for_children.extend_from_slice(rendered_above);
for (prefix, uri) in ns_sorted {
merge_rendered(&mut rendered_for_children, prefix, uri);
}
let mut child_chain: Vec<&Element> = ancestor_chain.to_vec();
child_chain.push(element);
for child in element.children() {
match child {
Node::Element(child_elem) => {
emit_element(
child_elem,
&child_chain,
&rendered_for_children,
false,
algorithm,
inclusive_namespace_prefixes,
out,
)?;
}
Node::Text(text) => {
push_text_escaped(out, text);
}
Node::Comment(content) => {
if algorithm.includes_comments() {
out.extend_from_slice(b"<!--");
out.extend_from_slice(content.as_bytes());
out.extend_from_slice(b"-->");
}
}
}
}
out.extend_from_slice(b"</");
push_qname(out, elem_prefix.as_deref(), element.qname().local());
out.push(b'>');
Ok(())
}
fn compute_rendered_namespaces(
element: &Element,
ancestor_chain: &[&Element],
rendered_above: &[(Option<String>, String)],
is_apex: bool,
algorithm: C14nAlgorithm,
inclusive_namespace_prefixes: &[&str],
) -> Result<Vec<(Option<String>, String)>, Error> {
let in_scope = namespaces_in_scope(element, ancestor_chain);
if algorithm.is_exclusive() {
compute_exclusive(
element,
&in_scope,
rendered_above,
inclusive_namespace_prefixes,
)
} else {
Ok(compute_inclusive(
element,
&in_scope,
rendered_above,
is_apex,
))
}
}
fn compute_exclusive(
element: &Element,
in_scope: &[(Option<String>, String)],
rendered_above: &[(Option<String>, String)],
inclusive_namespace_prefixes: &[&str],
) -> Result<Vec<(Option<String>, String)>, Error> {
let mut out: Vec<(Option<String>, String)> = Vec::new();
let utilized = utilized_prefixes(element, in_scope)?;
for prefix in &utilized {
let uri = lookup_in_scope(in_scope, prefix.as_deref());
let uri = match uri {
Some(u) => u.to_owned(),
None => {
if prefix.is_none() {
String::new()
} else {
return Err(Error::XmlEmit(format!(
"namespace prefix '{}' visibly utilized but not in scope",
prefix.as_deref().unwrap_or("")
)));
}
}
};
if should_render(prefix.as_deref(), &uri, rendered_above) {
push_unique(&mut out, prefix.clone(), uri);
}
}
for &pfx_str in inclusive_namespace_prefixes {
let prefix: Option<String> = if pfx_str == "#default" {
None
} else {
Some(pfx_str.to_owned())
};
let uri = match lookup_in_scope(in_scope, prefix.as_deref()) {
Some(u) => u.to_owned(),
None => {
if prefix.is_none() {
String::new()
} else {
continue;
}
}
};
if should_render(prefix.as_deref(), &uri, rendered_above) {
push_unique(&mut out, prefix, uri);
}
}
Ok(out)
}
fn compute_inclusive(
element: &Element,
in_scope: &[(Option<String>, String)],
rendered_above: &[(Option<String>, String)],
is_apex: bool,
) -> Vec<(Option<String>, String)> {
let mut out: Vec<(Option<String>, String)> = Vec::new();
if is_apex {
for (prefix, uri) in in_scope {
if prefix.as_deref() == Some("xml") && uri == XML_NS {
continue;
}
if prefix.is_none() && uri.is_empty() {
continue;
}
if should_render(prefix.as_deref(), uri, rendered_above) {
push_unique(&mut out, prefix.clone(), uri.clone());
}
}
} else {
for (prefix, uri) in &element.namespaces_declared_here {
if prefix.as_deref() == Some("xml") && uri == XML_NS {
continue;
}
if should_render(prefix.as_deref(), uri, rendered_above) {
push_unique(&mut out, prefix.clone(), uri.clone());
}
}
}
out
}
fn utilized_prefixes(
element: &Element,
in_scope: &[(Option<String>, String)],
) -> Result<Vec<Option<String>>, Error> {
let mut out: Vec<Option<String>> = Vec::new();
let elem_uri = element.qname().namespace();
let elem_prefix = prefix_for_uri(
in_scope,
elem_uri,
false,
element.source_prefix.as_deref(),
)?;
push_unique_opt(&mut out, elem_prefix);
for (qname, source_prefix, _value) in element.attributes_with_source_prefix() {
let Some(uri) = qname.namespace() else {
continue;
};
if uri == XML_NS {
continue;
}
let pfx = prefix_for_uri(
in_scope,
Some(uri),
true,
source_prefix,
)?;
if pfx.is_some() {
push_unique_opt(&mut out, pfx);
}
}
Ok(out)
}
fn should_render(
prefix: Option<&str>,
uri: &str,
rendered_above: &[(Option<String>, String)],
) -> bool {
if prefix == Some("xml") && uri == XML_NS {
return false;
}
let prev = rendered_above
.iter()
.rev()
.find(|(p, _)| p.as_deref() == prefix)
.map(|(_, u)| u.as_str());
match (prev, prefix, uri) {
(None, None, "") => false, (None, _, _) => true,
(Some(prev_uri), _, _) => prev_uri != uri,
}
}
fn namespaces_in_scope(
element: &Element,
ancestor_chain: &[&Element],
) -> Vec<(Option<String>, String)> {
let mut out: Vec<(Option<String>, String)> = Vec::new();
for ancestor in ancestor_chain {
for (prefix, uri) in &ancestor.namespaces_declared_here {
merge_rendered(&mut out, prefix.clone(), uri.clone());
}
}
for (prefix, uri) in &element.namespaces_declared_here {
merge_rendered(&mut out, prefix.clone(), uri.clone());
}
out
}
fn lookup_prefix_for_uri(
uri: Option<&str>,
ancestor_chain: &[&Element],
element: &Element,
is_attribute: bool,
source_prefix: Option<&str>,
) -> Result<Option<String>, Error> {
let Some(uri) = uri else {
return Ok(None);
};
if uri == XML_NS {
return Ok(Some("xml".to_owned()));
}
let in_scope = namespaces_in_scope(element, ancestor_chain);
prefix_for_uri(&in_scope, Some(uri), is_attribute, source_prefix)
}
fn prefix_for_uri(
in_scope: &[(Option<String>, String)],
uri: Option<&str>,
is_attribute: bool,
source_prefix: Option<&str>,
) -> Result<Option<String>, Error> {
let Some(uri) = uri else {
return Ok(None);
};
if uri == XML_NS {
return Ok(Some("xml".to_owned()));
}
if let Some(hint) = source_prefix
&& !hint.is_empty()
{
for (prefix, decl_uri) in in_scope.iter().rev() {
if decl_uri == uri
&& let Some(p) = prefix.as_deref()
&& p == hint
{
return Ok(Some(hint.to_owned()));
}
}
}
if source_prefix.is_none() && !is_attribute {
for (prefix, decl_uri) in in_scope.iter().rev() {
if decl_uri == uri && prefix.is_none() {
return Ok(None);
}
}
}
for (prefix, decl_uri) in in_scope.iter().rev() {
if decl_uri == uri {
if is_attribute && prefix.is_none() {
continue;
}
return Ok(prefix.clone());
}
}
Err(Error::XmlEmit(format!(
"no namespace prefix in scope for URI {uri}"
)))
}
fn lookup_in_scope<'a>(
in_scope: &'a [(Option<String>, String)],
prefix: Option<&str>,
) -> Option<&'a str> {
for (decl_prefix, uri) in in_scope.iter().rev() {
if decl_prefix.as_deref() == prefix {
return Some(uri.as_str());
}
}
None
}
fn push_unique(out: &mut Vec<(Option<String>, String)>, prefix: Option<String>, uri: String) {
if !out.iter().any(|(p, _)| *p == prefix) {
out.push((prefix, uri));
}
}
fn push_unique_opt(out: &mut Vec<Option<String>>, prefix: Option<String>) {
if !out.contains(&prefix) {
out.push(prefix);
}
}
fn merge_rendered(out: &mut Vec<(Option<String>, String)>, prefix: Option<String>, uri: String) {
if let Some(slot) = out.iter_mut().find(|(p, _)| *p == prefix) {
slot.1 = uri;
} else {
out.push((prefix, uri));
}
}
fn ns_sort_key(decl: &(Option<String>, String)) -> (u8, &[u8]) {
match &decl.0 {
None => (0, &[][..]),
Some(p) => (1, p.as_bytes()),
}
}
fn attr_sort_key(qname: &QName) -> (&[u8], &[u8]) {
(
qname.namespace().map_or(&[][..], str::as_bytes),
qname.local().as_bytes(),
)
}
fn push_qname(out: &mut Vec<u8>, prefix: Option<&str>, local: &str) {
if let Some(p) = prefix {
out.extend_from_slice(p.as_bytes());
out.push(b':');
}
out.extend_from_slice(local.as_bytes());
}
fn push_text_escaped(out: &mut Vec<u8>, text: &str) {
for byte in text.bytes() {
match byte {
b'&' => out.extend_from_slice(b"&"),
b'<' => out.extend_from_slice(b"<"),
b'>' => out.extend_from_slice(b">"),
b'\r' => out.extend_from_slice(b"
"),
b => out.push(b),
}
}
}
fn push_attr_value_escaped(out: &mut Vec<u8>, value: &str) {
for byte in value.bytes() {
match byte {
b'&' => out.extend_from_slice(b"&"),
b'<' => out.extend_from_slice(b"<"),
b'"' => out.extend_from_slice(b"""),
b'\t' => out.extend_from_slice(b"	"),
b'\n' => out.extend_from_slice(b"
"),
b'\r' => out.extend_from_slice(b"
"),
b => out.push(b),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml::parse::Document;
fn canon_named(xml: &str, local: &str, algorithm: C14nAlgorithm, prefixes: &[&str]) -> String {
let doc = Document::parse(xml.as_bytes()).expect("parse");
let (target, chain) = find_with_chain(&doc, local).expect("find target");
let bytes = canonicalize(&doc, target, &chain, algorithm, prefixes).expect("c14n");
String::from_utf8(bytes).expect("utf-8 output")
}
fn canon_root(xml: &str, algorithm: C14nAlgorithm, prefixes: &[&str]) -> String {
let doc = Document::parse(xml.as_bytes()).expect("parse");
let bytes = canonicalize(&doc, doc.root(), &[], algorithm, prefixes).expect("c14n");
String::from_utf8(bytes).expect("utf-8 output")
}
fn find_with_chain<'a>(
doc: &'a Document,
local: &str,
) -> Option<(&'a Element, Vec<&'a Element>)> {
fn walk<'b>(
element: &'b Element,
local: &str,
chain: &mut Vec<&'b Element>,
) -> Option<(&'b Element, Vec<&'b Element>)> {
if element.qname().local() == local {
return Some((element, chain.clone()));
}
for child in element.children() {
if let Node::Element(child_elem) = child {
chain.push(element);
if let Some(hit) = walk(child_elem, local, chain) {
return Some(hit);
}
chain.pop();
}
}
None
}
let mut chain: Vec<&Element> = Vec::new();
walk(doc.root(), local, &mut chain)
}
#[test]
fn exclusive_example_1_entity_escape() {
let input = r#"<doc><e attr="value with & and <"/></doc>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<doc><e attr="value with & and <"></e></doc>"#
);
}
#[test]
fn exclusive_example_2_default_namespace() {
let input = r#"<doc xmlns="urn:foo"><a/></doc>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<doc xmlns="urn:foo"><a></a></doc>"#);
}
#[test]
fn inclusive_example_2_default_namespace() {
let input = r#"<doc xmlns="urn:foo"><a/></doc>"#;
let got = canon_root(input, C14nAlgorithm::InclusiveCanonical, &[]);
assert_eq!(got, r#"<doc xmlns="urn:foo"><a></a></doc>"#);
}
#[test]
fn exclusive_example_3_drops_unused_namespaces() {
let input = r#"<doc xmlns:unused="urn:unused"><e/></doc>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, "<doc><e></e></doc>");
}
#[test]
fn inclusive_example_3_keeps_unused_namespaces_on_apex() {
let input = r#"<doc xmlns:unused="urn:unused"><e/></doc>"#;
let got = canon_root(input, C14nAlgorithm::InclusiveCanonical, &[]);
assert_eq!(got, r#"<doc xmlns:unused="urn:unused"><e></e></doc>"#);
}
#[test]
fn exclusive_example_4_prefix_list_forces_inclusion() {
let input = r#"<root xmlns:p="urn:p"><apex xmlns:q="urn:q"><leaf/></apex></root>"#;
let got = canon_named(input, "apex", C14nAlgorithm::ExclusiveCanonical, &["p"]);
assert_eq!(got, r#"<apex xmlns:p="urn:p"><leaf></leaf></apex>"#);
}
#[test]
fn exclusive_example_4_without_prefix_list_drops_unused() {
let input = r#"<root xmlns:p="urn:p"><apex xmlns:q="urn:q"><leaf/></apex></root>"#;
let got = canon_named(input, "apex", C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r"<apex><leaf></leaf></apex>");
}
#[test]
fn exclusive_example_5_attribute_sorting() {
let input = r#"<e xmlns:b="urn:b" xmlns:a="urn:a" b:z="1" a:y="2" x="3" b:w="4"><c/></e>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<e xmlns:a="urn:a" xmlns:b="urn:b" x="3" a:y="2" b:w="4" b:z="1"><c></c></e>"#
);
}
#[test]
fn inclusive_example_5_attribute_sorting() {
let input = r#"<e xmlns:b="urn:b" xmlns:a="urn:a" b:z="1" a:y="2" x="3" b:w="4"><c/></e>"#;
let got = canon_root(input, C14nAlgorithm::InclusiveCanonical, &[]);
assert_eq!(
got,
r#"<e xmlns:a="urn:a" xmlns:b="urn:b" x="3" a:y="2" b:w="4" b:z="1"><c></c></e>"#
);
}
#[test]
fn exclusive_example_6_drops_comments() {
let input = r"<doc><!-- comment --><e/></doc>";
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, "<doc><e></e></doc>");
}
#[test]
fn exclusive_with_comments_example_6_preserves_comments() {
let input = r"<doc><!-- comment --><e/></doc>";
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonicalWithComments, &[]);
assert_eq!(got, "<doc><!-- comment --><e></e></doc>");
}
#[test]
fn inclusive_example_6_drops_comments() {
let input = r"<doc><!-- comment --><e/></doc>";
let got = canon_root(input, C14nAlgorithm::InclusiveCanonical, &[]);
assert_eq!(got, "<doc><e></e></doc>");
}
#[test]
fn inclusive_with_comments_example_6_preserves_comments() {
let input = r"<doc><!-- comment --><e/></doc>";
let got = canon_root(input, C14nAlgorithm::InclusiveCanonicalWithComments, &[]);
assert_eq!(got, "<doc><!-- comment --><e></e></doc>");
}
#[test]
fn exclusive_example_7_preserves_text_whitespace() {
let input = "<doc> text </doc>";
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, "<doc> text </doc>");
}
#[test]
fn inclusive_example_7_preserves_text_whitespace() {
let input = "<doc> text </doc>";
let got = canon_root(input, C14nAlgorithm::InclusiveCanonical, &[]);
assert_eq!(got, "<doc> text </doc>");
}
#[test]
fn empty_element_serializes_with_open_and_close_tags() {
let a = canon_root("<e/>", C14nAlgorithm::ExclusiveCanonical, &[]);
let b = canon_root("<e></e>", C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(a, "<e></e>");
assert_eq!(b, "<e></e>");
assert_eq!(a, b);
}
#[test]
fn text_content_escapes_per_spec() {
let input = "<doc>a&b<c>d\te\nf</doc>";
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, "<doc>a&b<c>d\te\nf</doc>");
}
#[test]
fn attribute_value_escapes_per_spec() {
let input = r#"<e a="	 "&<"/>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<e a="	

"&<"></e>"#);
}
#[test]
fn xml_prefix_attribute_is_preserved_but_never_re_declared() {
let input = r#"<doc xml:lang="en"><e/></doc>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<doc xml:lang="en"><e></e></doc>"#);
}
#[test]
fn xml_namespaced_attribute_sorts_by_uri() {
let input = r#"<e a="1" xml:id="x" b="2"/>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<e a="1" b="2" xml:id="x"></e>"#);
}
#[test]
fn descendant_namespace_decl_not_re_emitted_on_descendant() {
let input = r#"<r xmlns:p="urn:p"><p:c><p:d/></p:c></r>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<r><p:c xmlns:p="urn:p"><p:d></p:d></p:c></r>"#);
}
#[test]
fn descendant_overriding_default_namespace_is_emitted() {
let input = r#"<a xmlns="urn:a"><b xmlns="urn:b"/></a>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<a xmlns="urn:a"><b xmlns="urn:b"></b></a>"#);
}
#[test]
fn descendant_cancels_default_namespace_with_empty_xmlns() {
let input = r#"<a xmlns="urn:a"><b xmlns=""/></a>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(got, r#"<a xmlns="urn:a"><b xmlns=""></b></a>"#);
}
#[test]
fn inclusive_propagates_unused_ns_only_to_apex() {
let input = r#"<root xmlns:unused="urn:unused"><apex><leaf/></apex></root>"#;
let got = canon_named(input, "apex", C14nAlgorithm::InclusiveCanonical, &[]);
assert_eq!(
got,
r#"<apex xmlns:unused="urn:unused"><leaf></leaf></apex>"#
);
}
#[test]
fn exclusive_prefix_list_default_token_surfaces_default_ns() {
let input = r#"<root xmlns="urn:root"><apex><leaf/></apex></root>"#;
let got = canon_named(
input,
"apex",
C14nAlgorithm::ExclusiveCanonical,
&["#default"],
);
assert_eq!(got, r#"<apex xmlns="urn:root"><leaf></leaf></apex>"#);
}
#[test]
fn exclusive_prefix_list_unbound_prefix_silently_ignored() {
let input = r"<root><apex/></root>";
let got = canon_named(
input,
"apex",
C14nAlgorithm::ExclusiveCanonical,
&["missing"],
);
assert_eq!(got, "<apex></apex>");
}
#[test]
fn saml_assertion_subtree_canonicalizes_deterministically() {
let a = r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="abc123" IssueInstant="2024-01-01T00:00:00Z"><saml:Issuer>issuer.example.com</saml:Issuer></saml:Assertion>"#;
let b = r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="abc123" IssueInstant="2024-01-01T00:00:00Z" Version="2.0"><saml:Issuer>issuer.example.com</saml:Issuer></saml:Assertion>"#;
let ca = canon_root(a, C14nAlgorithm::ExclusiveCanonical, &[]);
let cb = canon_root(b, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(ca, cb);
assert!(
ca.starts_with(r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="abc123" IssueInstant="2024-01-01T00:00:00Z" Version="2.0">"#),
"got: {ca}"
);
assert!(ca.contains("<saml:Issuer>issuer.example.com</saml:Issuer>"));
}
#[test]
fn saml_response_attribute_sort_is_stable() {
let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0" IssueInstant="2024-01-01T00:00:00Z" Destination="https://sp/acs" ID="resp-1" InResponseTo="req-1"><x/></samlp:Response>"#;
let got = canon_root(xml, C14nAlgorithm::ExclusiveCanonical, &[]);
assert!(
got.starts_with(r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://sp/acs" ID="resp-1" InResponseTo="req-1" IssueInstant="2024-01-01T00:00:00Z" Version="2.0">"#),
"got: {got}"
);
}
#[test]
fn nested_element_with_qualified_attribute_uses_correct_prefix() {
let input =
r##"<ds:Signature xmlns:ds="urn:ds"><ds:Reference URI="#abc"/></ds:Signature>"##;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r##"<ds:Signature xmlns:ds="urn:ds"><ds:Reference URI="#abc"></ds:Reference></ds:Signature>"##
);
}
#[test]
fn determinism_across_attribute_order_permutations() {
let a = r#"<e b="2" a="1" c="3"/>"#;
let b = r#"<e a="1" c="3" b="2"/>"#;
let c = r#"<e c="3" b="2" a="1"/>"#;
let ca = canon_root(a, C14nAlgorithm::ExclusiveCanonical, &[]);
let cb = canon_root(b, C14nAlgorithm::ExclusiveCanonical, &[]);
let cc = canon_root(c, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(ca, cb);
assert_eq!(cb, cc);
assert_eq!(ca, r#"<e a="1" b="2" c="3"></e>"#);
}
#[test]
fn keycloak_shape_preserves_saml_prefix_default_xmlns_first() {
let input = r#"<saml:Foo xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"/>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<saml:Foo xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:Foo>"#
);
}
#[test]
fn keycloak_shape_preserves_saml_prefix_prefixed_xmlns_first() {
let input = r#"<saml:Foo xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<saml:Foo xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:Foo>"#
);
}
#[test]
fn keycloak_shape_preserves_default_when_source_used_default() {
let input = r#"<Foo xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"/>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<Foo xmlns="urn:oasis:names:tc:SAML:2.0:assertion"></Foo>"#
);
}
#[test]
fn keycloak_shape_with_nested_assertion_subtree() {
let input = r#"<saml:Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="abc"><saml:Issuer>idp</saml:Issuer></saml:Assertion>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="abc"><saml:Issuer>idp</saml:Issuer></saml:Assertion>"#
);
}
#[test]
fn keycloak_shape_attribute_with_dual_prefix_keeps_source_prefix() {
let input = r#"<saml:Foo xmlns:other="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" saml:Bar="v"/>"#;
let got = canon_root(input, C14nAlgorithm::ExclusiveCanonical, &[]);
assert_eq!(
got,
r#"<saml:Foo xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" saml:Bar="v"></saml:Foo>"#
);
}
}