use std::collections::BTreeMap;
use quick_xml::Reader;
use quick_xml::events::{BytesStart, Event};
type NsMap = BTreeMap<String, String>;
pub fn canonicalize_element(
xml: &str,
local: &str,
inclusive_prefixes: &[String],
) -> Option<Vec<u8>> {
canonicalize(xml, &Target::Local(local), inclusive_prefixes, false)
}
pub fn canonicalize_element_by_id(
xml: &str,
id: &str,
inclusive_prefixes: &[String],
) -> Option<Vec<u8>> {
canonicalize(xml, &Target::Id(id), inclusive_prefixes, true)
}
enum Target<'a> {
Local(&'a str),
Id(&'a str),
}
impl Target<'_> {
fn matches(&self, e: &BytesStart<'_>) -> bool {
match self {
Target::Local(local) => local_name(e.name().as_ref()) == **local,
Target::Id(id) => element_id(e).as_deref() == Some(*id),
}
}
}
fn element_id(e: &BytesStart<'_>) -> Option<String> {
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let lname = local_name(key);
if lname == "ID" || lname == "Id" || lname == "id" {
return Some(unescape_bytes(&attr.value));
}
}
None
}
fn canonicalize(
xml: &str,
target: &Target<'_>,
inclusive_prefixes: &[String],
omit_enveloped_signature: bool,
) -> Option<Vec<u8>> {
let mut reader = Reader::from_str(xml);
reader.trim_text(false);
reader.expand_empty_elements(false);
reader.check_end_names(false);
let mut ctx_stack: Vec<NsMap> = vec![BTreeMap::new()];
let mut out: Vec<u8> = Vec::new();
let mut in_target_depth: Option<usize> = None;
let mut rendered_stack: Vec<NsMap> = Vec::new();
let mut open_qnames: Vec<Vec<u8>> = Vec::new();
let mut depth = 0usize;
let mut skip_depth: Option<usize> = None;
loop {
let emit = in_target_depth.is_some() && skip_depth.is_none();
match reader.read_event() {
Ok(Event::Eof) => break,
Ok(Event::Start(e)) => {
let frame = push_ns_frame(&mut ctx_stack, &e);
let entering_target = in_target_depth.is_none() && target.matches(&e);
if entering_target {
in_target_depth = Some(depth);
rendered_stack.clear();
open_qnames.clear();
}
if omit_enveloped_signature
&& in_target_depth.is_some()
&& !entering_target
&& skip_depth.is_none()
&& local_name(e.name().as_ref()) == "Signature"
{
skip_depth = Some(depth);
}
if in_target_depth.is_some() && skip_depth.is_none() {
let prev_rendered = rendered_stack.last().cloned().unwrap_or_default();
let emitted =
write_start_tag(&mut out, &e, &frame, &prev_rendered, inclusive_prefixes);
rendered_stack.push(emitted);
open_qnames.push(e.name().as_ref().to_vec());
}
depth += 1;
}
Ok(Event::End(_)) => {
depth = depth.saturating_sub(1);
ctx_stack.pop();
if let Some(sd) = skip_depth {
if depth == sd {
skip_depth = None;
}
if skip_depth.is_some() {
continue;
}
}
if emit {
if let Some(td) = in_target_depth {
if depth >= td {
rendered_stack.pop();
if let Some(qname) = open_qnames.pop() {
out.extend_from_slice(b"</");
out.extend_from_slice(&qname);
out.push(b'>');
}
}
}
}
if let Some(td) = in_target_depth {
if depth == td {
break;
}
}
}
Ok(Event::Empty(e)) => {
let frame = push_ns_frame(&mut ctx_stack, &e);
ctx_stack.pop();
let entering_target = in_target_depth.is_none() && target.matches(&e);
let is_skipped_sig = omit_enveloped_signature
&& in_target_depth.is_some()
&& !entering_target
&& local_name(e.name().as_ref()) == "Signature";
if (emit || entering_target) && !is_skipped_sig {
let prev_rendered = if entering_target {
BTreeMap::new()
} else {
rendered_stack.last().cloned().unwrap_or_default()
};
write_start_tag(&mut out, &e, &frame, &prev_rendered, inclusive_prefixes);
out.extend_from_slice(b"</");
out.extend_from_slice(e.name().as_ref());
out.push(b'>');
if entering_target {
in_target_depth = Some(depth);
break;
}
}
}
Ok(Event::Text(t)) => {
if emit {
let raw = t.into_inner();
let s = unescape_bytes(&raw);
out.extend_from_slice(canon_text(&s).as_bytes());
}
}
Ok(Event::CData(t)) => {
if emit {
let raw = t.into_inner();
let s = String::from_utf8_lossy(&raw);
out.extend_from_slice(canon_text(&s).as_bytes());
}
}
Ok(_) => {}
Err(_) => return None,
}
}
if in_target_depth.is_some() {
Some(out)
} else {
None
}
}
fn push_ns_frame(ctx_stack: &mut Vec<NsMap>, e: &BytesStart<'_>) -> NsMap {
let mut frame = ctx_stack.last().cloned().unwrap_or_default();
for (p, u) in collect_ns_decls(e) {
if u.is_empty() && p.is_empty() {
frame.remove("");
} else {
frame.insert(p, u);
}
}
ctx_stack.push(frame.clone());
frame
}
fn collect_ns_decls(e: &BytesStart<'_>) -> Vec<(String, String)> {
let mut v = Vec::new();
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
if key == b"xmlns" {
let uri = unescape_bytes(&attr.value);
v.push((String::new(), uri));
} else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
let p = String::from_utf8_lossy(prefix).to_string();
let uri = unescape_bytes(&attr.value);
v.push((p, uri));
}
}
v
}
fn write_start_tag(
out: &mut Vec<u8>,
e: &BytesStart<'_>,
in_scope: &NsMap,
ancestor_rendered: &NsMap,
inclusive_prefixes: &[String],
) -> NsMap {
let qname = e.name();
let qname = qname.as_ref();
out.push(b'<');
out.extend_from_slice(qname);
let mut utilised: BTreeMap<String, ()> = BTreeMap::new();
utilised.insert(prefix_of(qname), ());
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
if key == b"xmlns" || key.starts_with(b"xmlns:") {
continue;
}
let p = prefix_of(key);
if !p.is_empty() {
utilised.insert(p, ());
}
}
for p in inclusive_prefixes {
let key = if p == "#default" {
String::new()
} else {
p.clone()
};
utilised.insert(key, ());
}
let mut this_rendered = ancestor_rendered.clone();
let mut ns_to_emit: Vec<(String, String)> = Vec::new();
for (prefix, _) in &utilised {
if let Some(uri) = in_scope.get(prefix) {
let already = ancestor_rendered
.get(prefix)
.map(|u| u == uri)
.unwrap_or(false);
if !already && !(prefix.is_empty() && uri.is_empty()) {
ns_to_emit.push((prefix.clone(), uri.clone()));
this_rendered.insert(prefix.clone(), uri.clone());
}
} else if prefix.is_empty() {
if let Some(prev) = ancestor_rendered.get("") {
if !prev.is_empty() {
ns_to_emit.push((String::new(), String::new()));
this_rendered.insert(String::new(), String::new());
}
}
}
}
ns_to_emit.sort_by(|a, b| match (a.0.is_empty(), b.0.is_empty()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.0.cmp(&b.0),
});
for (prefix, uri) in &ns_to_emit {
if prefix.is_empty() {
out.extend_from_slice(b" xmlns=\"");
} else {
out.extend_from_slice(b" xmlns:");
out.extend_from_slice(prefix.as_bytes());
out.extend_from_slice(b"=\"");
}
out.extend_from_slice(canon_attr_value(uri).as_bytes());
out.push(b'"');
}
let mut attrs: Vec<(String, String, Vec<u8>, Vec<u8>)> = Vec::new();
for attr in e.attributes().flatten() {
let key = attr.key.as_ref().to_vec();
if key == b"xmlns" || key.starts_with(b"xmlns:") {
continue;
}
let prefix = prefix_of(&key);
let ns_uri = if prefix.is_empty() {
String::new()
} else {
in_scope.get(&prefix).cloned().unwrap_or_default()
};
let local = local_name(&key);
attrs.push((ns_uri, local, key, attr.value.to_vec()));
}
attrs.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
for (_, _, key, value) in &attrs {
out.push(b' ');
out.extend_from_slice(key);
out.extend_from_slice(b"=\"");
let v = unescape_bytes(value);
out.extend_from_slice(canon_attr_value(&v).as_bytes());
out.push(b'"');
}
out.push(b'>');
this_rendered
}
fn local_name(qname: &[u8]) -> String {
let s = String::from_utf8_lossy(qname);
match s.rsplit_once(':') {
Some((_, local)) => local.to_string(),
None => s.to_string(),
}
}
fn prefix_of(qname: &[u8]) -> String {
let s = String::from_utf8_lossy(qname);
match s.split_once(':') {
Some((p, _)) => p.to_string(),
None => String::new(),
}
}
fn unescape_bytes(raw: &[u8]) -> String {
let s = String::from_utf8_lossy(raw);
unescape_str(&s)
}
fn unescape_str(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'&' {
if let Some(semi) = s[i..].find(';') {
let ent = &s[i + 1..i + semi];
let replaced = match ent {
"amp" => Some('&'),
"lt" => Some('<'),
"gt" => Some('>'),
"quot" => Some('"'),
"apos" => Some('\''),
_ if ent.starts_with("#x") || ent.starts_with("#X") => {
u32::from_str_radix(&ent[2..], 16)
.ok()
.and_then(char::from_u32)
}
_ if ent.starts_with('#') => {
ent[1..].parse::<u32>().ok().and_then(char::from_u32)
}
_ => None,
};
if let Some(c) = replaced {
out.push(c);
i += semi + 1;
continue;
}
}
}
let ch_len = utf8_len(bytes[i]);
let end = (i + ch_len).min(bytes.len());
out.push_str(&s[i..end]);
i = end;
}
out
}
fn utf8_len(b: u8) -> usize {
if b < 0x80 {
1
} else if b >> 5 == 0b110 {
2
} else if b >> 4 == 0b1110 {
3
} else if b >> 3 == 0b11110 {
4
} else {
1
}
}
fn canon_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'\r' => out.push_str("
"),
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn c14n(xml: &str, local: &str) -> String {
let bytes = canonicalize_element(xml, local, &[]).expect("canonicalize");
String::from_utf8(bytes).expect("utf8")
}
#[test]
fn attributes_are_sorted_and_ns_emitted_at_apex() {
let xml = r#"<root xmlns:a="urn:a"><a:el z="2" a="1" a:b="x">text</a:el></root>"#;
let out = c14n(xml, "el");
assert_eq!(
out,
r#"<a:el xmlns:a="urn:a" a="1" z="2" a:b="x">text</a:el>"#
);
}
#[test]
fn unused_namespace_is_dropped_exclusive() {
let xml = r#"<root xmlns:unused="urn:x" xmlns:a="urn:a"><a:el>v</a:el></root>"#;
let out = c14n(xml, "el");
assert_eq!(out, r#"<a:el xmlns:a="urn:a">v</a:el>"#);
}
#[test]
fn empty_element_is_expanded() {
let xml = r#"<root><inner/></root>"#;
assert_eq!(c14n(xml, "inner"), "<inner></inner>");
}
#[test]
fn comments_are_omitted() {
let with = r#"<root><x>a<!-- c -->b</x></root>"#;
let without = r#"<root><x>ab</x></root>"#;
assert_eq!(c14n(with, "x"), c14n(without, "x"));
assert_eq!(c14n(with, "x"), "<x>ab</x>");
}
#[test]
fn text_and_attr_special_chars_are_escaped() {
let xml = r#"<root><x t="a<b	c">1&2<3</x></root>"#;
let out = c14n(xml, "x");
assert_eq!(out, r#"<x t="a<b	c">1&2<3</x>"#);
}
#[test]
fn inclusive_prefix_list_is_honored() {
let xml = r#"<root xmlns:xs="urn:xs" xmlns:a="urn:a"><a:el>v</a:el></root>"#;
let bytes = canonicalize_element(xml, "el", &["xs".to_string()]).expect("canonicalize");
let out = String::from_utf8(bytes).unwrap();
assert_eq!(out, r#"<a:el xmlns:a="urn:a" xmlns:xs="urn:xs">v</a:el>"#);
}
#[test]
fn transform_changes_canonical_form() {
let a = c14n(r#"<root><x>hello</x></root>"#, "x");
let b = c14n(r#"<root><x>hell0</x></root>"#, "x");
assert_ne!(a, b);
}
#[test]
fn whitespace_inside_element_is_preserved() {
let xml = "<root><x> a\n b </x></root>";
assert_eq!(c14n(xml, "x"), "<x> a\n b </x>");
}
}
fn canon_attr_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'"' => out.push_str("""),
'\t' => out.push_str("	"),
'\n' => out.push_str("
"),
'\r' => out.push_str("
"),
_ => out.push(c),
}
}
out
}