use xml_sec::c14n::{C14nAlgorithm, C14nMode, canonicalize, canonicalize_xml};
fn assert_c14n(xml: &[u8], mode: C14nMode, with_comments: bool, expected: &str) {
let algo = C14nAlgorithm::new(mode, with_comments);
let result = canonicalize_xml(xml, &algo).expect("canonicalize failed");
let result_str = String::from_utf8(result).expect("invalid utf8");
assert_eq!(
result_str, expected,
"\n--- GOT ---\n{result_str}\n--- EXPECTED ---\n{expected}"
);
}
fn assert_exc_c14n(xml: &[u8], prefix_list: &str, expected: &str) {
let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false).with_prefix_list(prefix_list);
let result = canonicalize_xml(xml, &algo).expect("canonicalize failed");
let result_str = String::from_utf8(result).expect("invalid utf8");
assert_eq!(
result_str, expected,
"\n--- GOT ---\n{result_str}\n--- EXPECTED ---\n{expected}"
);
}
const SIMPLE_NS_XML: &[u8] =
br#"<root xmlns:z="http://z.com" xmlns:a="http://a.com" xmlns="http://default.com" b="2" a="1">
<a:child z:attr="val" plain="yes">
<inner/>
</a:child>
<z:other xmlns:a="http://a-override.com"/>
</root>"#;
#[test]
fn simple_ns_inclusive() {
assert_c14n(
SIMPLE_NS_XML,
C14nMode::Inclusive1_0,
false,
concat!(
r#"<root xmlns="http://default.com" xmlns:a="http://a.com" xmlns:z="http://z.com" a="1" b="2">"#,
"\n",
r#" <a:child plain="yes" z:attr="val">"#,
"\n",
r#" <inner></inner>"#,
"\n",
r#" </a:child>"#,
"\n",
r#" <z:other xmlns:a="http://a-override.com"></z:other>"#,
"\n",
r#"</root>"#,
),
);
}
#[test]
fn simple_ns_exclusive() {
assert_exc_c14n(
SIMPLE_NS_XML,
"",
concat!(
r#"<root xmlns="http://default.com" a="1" b="2">"#,
"\n",
r#" <a:child xmlns:a="http://a.com" xmlns:z="http://z.com" plain="yes" z:attr="val">"#,
"\n",
r#" <inner></inner>"#,
"\n",
r#" </a:child>"#,
"\n",
r#" <z:other xmlns:z="http://z.com"></z:other>"#,
"\n",
r#"</root>"#,
),
);
}
const MERLIN_DATA_XML: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
<foo:Root xmlns:bar="http://example.org/bar" xmlns:baz="http://example.org/baz" xmlns:foo="http://example.org/foo" xmlns="http://example.org/" xml:lang="en-ie">
<bar:Something>
<foo:Nothing>
<foo:Something>
<bar:Something>
<foo:Something>
<foo:Nothing>
<foo:Something>
<baz:Something />
</foo:Something>
</foo:Nothing>
</foo:Something>
</bar:Something>
</foo:Something>
</foo:Nothing>
</bar:Something>
</foo:Root>"#;
#[test]
fn merlin_data_inclusive() {
assert_c14n(
MERLIN_DATA_XML,
C14nMode::Inclusive1_0,
false,
concat!(
r#"<foo:Root xmlns="http://example.org/" xmlns:bar="http://example.org/bar" xmlns:baz="http://example.org/baz" xmlns:foo="http://example.org/foo" xml:lang="en-ie">"#,
"\n",
" <bar:Something>\n",
" <foo:Nothing>\n",
" <foo:Something>\n",
" <bar:Something>\n",
" <foo:Something>\n",
" <foo:Nothing>\n",
" <foo:Something>\n",
" <baz:Something></baz:Something>\n",
" </foo:Something>\n",
" </foo:Nothing>\n",
" </foo:Something>\n",
" </bar:Something>\n",
" </foo:Something>\n",
" </foo:Nothing>\n",
" </bar:Something>\n",
"</foo:Root>",
),
);
}
#[test]
fn merlin_data_exclusive() {
assert_exc_c14n(
MERLIN_DATA_XML,
"",
concat!(
r#"<foo:Root xmlns:foo="http://example.org/foo" xml:lang="en-ie">"#,
"\n",
r#" <bar:Something xmlns:bar="http://example.org/bar">"#,
"\n",
" <foo:Nothing>\n",
" <foo:Something>\n",
" <bar:Something>\n",
" <foo:Something>\n",
" <foo:Nothing>\n",
" <foo:Something>\n",
r#" <baz:Something xmlns:baz="http://example.org/baz"></baz:Something>"#,
"\n",
" </foo:Something>\n",
" </foo:Nothing>\n",
" </foo:Something>\n",
" </bar:Something>\n",
" </foo:Something>\n",
" </foo:Nothing>\n",
" </bar:Something>\n",
"</foo:Root>",
),
);
}
const ATTR_SORT_XML: &[u8] = br#"<root xmlns:ns2="http://ns2" xmlns:ns1="http://ns1">
<elem ns2:b="2" ns1:a="1" ns2:a="3" ns1:b="4" plain="5"/>
</root>"#;
#[test]
fn attr_sort_inclusive() {
assert_c14n(
ATTR_SORT_XML,
C14nMode::Inclusive1_0,
false,
concat!(
r#"<root xmlns:ns1="http://ns1" xmlns:ns2="http://ns2">"#,
"\n",
r#" <elem plain="5" ns1:a="1" ns1:b="4" ns2:a="3" ns2:b="2"></elem>"#,
"\n",
"</root>",
),
);
}
#[test]
fn attr_sort_exclusive() {
assert_exc_c14n(
ATTR_SORT_XML,
"",
concat!(
"<root>\n",
r#" <elem xmlns:ns1="http://ns1" xmlns:ns2="http://ns2" plain="5" ns1:a="1" ns1:b="4" ns2:a="3" ns2:b="2"></elem>"#,
"\n",
"</root>",
),
);
}
const COMMENTS_PI_XML: &[u8] = b"<!-- doc-level comment -->\n<?target data?>\n<root>\n <!-- inner comment -->\n <child></child>\n</root>\n<!-- trailing comment -->";
#[test]
fn comments_stripped() {
assert_c14n(
COMMENTS_PI_XML,
C14nMode::Inclusive1_0,
false,
concat!(
"<?target data?>\n",
"<root>\n",
" \n",
" <child></child>\n",
"</root>",
),
);
}
#[test]
fn comments_preserved() {
assert_c14n(
COMMENTS_PI_XML,
C14nMode::Inclusive1_0,
true,
concat!(
"<!-- doc-level comment -->\n<?target data?>\n",
"<root>\n",
" <!-- inner comment -->\n",
" <child></child>\n",
"</root>\n<!-- trailing comment -->",
),
);
}
#[test]
fn idempotency_inclusive() {
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
let pass1 = canonicalize_xml(MERLIN_DATA_XML, &algo).expect("pass1");
let pass2 = canonicalize_xml(&pass1, &algo).expect("pass2");
assert_eq!(pass1, pass2, "C14N must be idempotent");
}
#[test]
fn idempotency_exclusive() {
let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false);
let pass1 = canonicalize_xml(MERLIN_DATA_XML, &algo).expect("pass1");
let pass2 = canonicalize_xml(&pass1, &algo).expect("pass2");
assert_eq!(pass1, pass2, "Exclusive C14N must be idempotent");
}
#[test]
fn minimal_document() {
assert_c14n(b"<r/>", C14nMode::Inclusive1_0, false, "<r></r>");
}
#[test]
fn cdata_flattened() {
assert_c14n(
b"<r><![CDATA[a < b & c]]></r>",
C14nMode::Inclusive1_0,
false,
"<r>a < b & c</r>",
);
}
#[test]
fn aliased_prefixes_exclusive() {
let xml = br#"<root xmlns:a="http://same" xmlns:b="http://same"><b:child a:x="1"/></root>"#;
assert_exc_c14n(
xml,
"",
concat!(
"<root>",
r#"<b:child xmlns:a="http://same" xmlns:b="http://same" a:x="1"></b:child>"#,
"</root>",
),
);
}
#[test]
fn aliased_prefixes_inclusive() {
let xml = br#"<root xmlns:a="http://same" xmlns:b="http://same"><b:child a:x="1"/></root>"#;
assert_c14n(
xml,
C14nMode::Inclusive1_0,
false,
concat!(
r#"<root xmlns:a="http://same" xmlns:b="http://same">"#,
r#"<b:child a:x="1"></b:child>"#,
"</root>",
),
);
}
#[test]
fn default_ns_undeclaration() {
let xml = br#"<root xmlns="http://example.com"><child xmlns=""/></root>"#;
assert_c14n(
xml,
C14nMode::Inclusive1_0,
false,
r#"<root xmlns="http://example.com"><child xmlns=""></child></root>"#,
);
}
#[test]
fn c14n_1_1_full_document_matches_1_0() {
let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let result_10 = canonicalize_xml(MERLIN_DATA_XML, &algo_10).expect("1.0");
let result_11 = canonicalize_xml(MERLIN_DATA_XML, &algo_11).expect("1.1");
assert_eq!(
result_10, result_11,
"C14N 1.1 full-document output should match 1.0"
);
}
#[test]
fn c14n_1_1_with_comments() {
assert_c14n(
COMMENTS_PI_XML,
C14nMode::Inclusive1_1,
true,
concat!(
"<!-- doc-level comment -->\n<?target data?>\n",
"<root>\n",
" <!-- inner comment -->\n",
" <child></child>\n",
"</root>\n<!-- trailing comment -->",
),
);
}
#[test]
fn c14n_1_1_idempotency() {
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let pass1 = canonicalize_xml(MERLIN_DATA_XML, &algo).expect("pass1");
let pass2 = canonicalize_xml(&pass1, &algo).expect("pass2");
assert_eq!(pass1, pass2, "C14N 1.1 must be idempotent");
}
#[test]
fn c14n_1_1_xml_id_inherited_in_subset() {
let xml = r#"<root xml:id="doc1" xmlns:a="http://a"><a:child>text</a:child></root>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let child = doc.root_element().first_element_child().expect("child");
let child_id = child.id();
let pred =
|n: roxmltree::Node| n.id() == child_id || n.parent().is_some_and(|p| p.id() == child_id);
let mut output = Vec::new();
canonicalize(&doc, Some(&pred), &algo, &mut output).expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
result.contains(r#"xml:id="doc1""#),
"xml:id should be inherited in C14N 1.1 subset; got: {result}"
);
assert!(
result.contains(r#"xmlns:a="http://a""#),
"namespace should be rendered; got: {result}"
);
}
#[test]
fn c14n_1_1_xml_lang_and_id_both_inherited() {
let xml = r#"<root xml:lang="en" xml:id="r1"><child/></root>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let child = doc.root_element().first_element_child().expect("child");
let child_id = child.id();
let pred = |n: roxmltree::Node| n.id() == child_id;
let mut output = Vec::new();
canonicalize(&doc, Some(&pred), &algo, &mut output).expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(result.contains(r#"xml:id="r1""#), "got: {result}");
assert!(result.contains(r#"xml:lang="en""#), "got: {result}");
}
#[test]
fn c14n_1_1_xml_base_resolved_in_subset() {
let xml = r#"<root xml:base="http://example.com/"><child xml:base="sub/">text</child></root>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let child = doc.root_element().first_element_child().expect("child");
let child_id = child.id();
let pred =
|n: roxmltree::Node| n.id() == child_id || n.parent().is_some_and(|p| p.id() == child_id);
let mut output = Vec::new();
canonicalize(&doc, Some(&pred), &algo, &mut output).expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
result.contains(r#"xml:base="http://example.com/sub/""#),
"xml:base should be resolved to absolute URI; got: {result}"
);
}
#[test]
fn c14n_1_1_xml_base_inherited_resolved() {
let xml = r#"<a xml:base="http://ex.com/x/"><b xml:base="y/"><c/></b></a>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let a = doc.root_element();
let b = a.first_element_child().expect("b");
let c = b.first_element_child().expect("c");
let c_id = c.id();
let pred = |n: roxmltree::Node| n.id() == c_id;
let mut output = Vec::new();
canonicalize(&doc, Some(&pred), &algo, &mut output).expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
result.contains(r#"xml:base="http://ex.com/x/y/""#),
"inherited xml:base should be resolved; got: {result}"
);
}
#[test]
fn c14n_1_1_xml_base_dotdot_resolved() {
let xml = r#"<a xml:base="http://ex.com/a/b/"><b xml:base="../c/"><d/></b></a>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
let a = doc.root_element();
let b = a.first_element_child().expect("b");
let d = b.first_element_child().expect("d");
let d_id = d.id();
let pred = |n: roxmltree::Node| n.id() == d_id;
let mut output = Vec::new();
canonicalize(&doc, Some(&pred), &algo, &mut output).expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
result.contains(r#"xml:base="http://ex.com/a/c/""#),
"xml:base with .. should be resolved; got: {result}"
);
}
#[test]
fn c14n_1_0_xml_base_not_resolved() {
let xml = r#"<root xml:base="http://example.com/"><child xml:base="sub/">text</child></root>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
let child = doc.root_element().first_element_child().expect("child");
let child_id = child.id();
let pred =
|n: roxmltree::Node| n.id() == child_id || n.parent().is_some_and(|p| p.id() == child_id);
let mut output = Vec::new();
canonicalize(&doc, Some(&pred), &algo, &mut output).expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
result.contains(r#"xml:base="sub/""#),
"C14N 1.0 should NOT resolve xml:base; got: {result}"
);
}
#[test]
fn subset_xmlns_empty_with_excluded_default_ns_ancestor() {
let xml = r#"<root xmlns="http://example.com"><child xmlns=""/></root>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
let child_id = doc
.root_element()
.first_element_child()
.expect("child")
.id();
let mut output = Vec::new();
canonicalize(
&doc,
Some(&|node| node.id() == child_id),
&algo,
&mut output,
)
.expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
result.contains(r#"xmlns="""#),
"xmlns=\"\" must be emitted to undeclare inherited default ns. Got: {result}"
);
}
#[test]
fn subset_no_spurious_xmlns_empty() {
let xml = r#"<root xmlns:a="http://a"><child/></root>"#;
let doc = roxmltree::Document::parse(xml).expect("parse");
let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
let child_id = doc
.root_element()
.first_element_child()
.expect("child")
.id();
let mut output = Vec::new();
canonicalize(
&doc,
Some(&|node| node.id() == child_id),
&algo,
&mut output,
)
.expect("c14n");
let result = String::from_utf8(output).expect("utf8");
assert!(
!result.contains("xmlns="),
"xmlns=\"\" must NOT appear when no default ns in scope. Got: {result}"
);
}