Skip to content

Commit 70475b5

Browse files
serial: eliminate implicit xml namespace for exclusive c14n
1 parent fb67a25 commit 70475b5

1 file changed

Lines changed: 84 additions & 2 deletions

File tree

src/serial/c14n.rs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,28 @@ struct C14nContext<'a> {
135135

136136
impl<'a> C14nContext<'a> {
137137
fn new(doc: &'a Document, options: &'a C14nOptions) -> Self {
138+
// Seed the rendered namespace stack with the implicit `xml` prefix
139+
// binding. The XML Namespaces spec reserves `xml` as always bound to
140+
// `http://www.w3.org/XML/1998/namespace`. Canonical XML 1.0 §2.3
141+
// ("Processing Model") requires that this binding is never emitted:
142+
// "omit namespace node with local name xml, which defines the
143+
// xml prefix, if its string value is
144+
// http://www.w3.org/XML/1998/namespace"
145+
// Exclusive C14N §3 defines itself as a variant of Canonical XML and
146+
// does not restate this rule — it inherits it. Pre-populating the
147+
// binding here makes the dedup check in `compute_ns_declarations`
148+
// filter it out automatically when an element uses `xml:lang`,
149+
// `xml:space`, or `xml:base`.
150+
let mut initial_bindings = NsBinding::new();
151+
initial_bindings.insert(
152+
"xml".to_string(),
153+
"http://www.w3.org/XML/1998/namespace".to_string(),
154+
);
138155
Self {
139156
doc,
140157
options,
141158
output: String::new(),
142-
// Start with the implicit xml namespace binding.
143-
rendered_ns_stack: vec![NsBinding::new()],
159+
rendered_ns_stack: vec![initial_bindings],
144160
}
145161
}
146162

@@ -1018,4 +1034,70 @@ mod tests {
10181034
let result = canonicalize(&doc, &C14nOptions::default());
10191035
assert_eq!(result, "<?before?>\n<root></root>\n<?after?>");
10201036
}
1037+
1038+
#[test]
1039+
fn test_c14n_inclusive_xml_prefix_not_emitted() {
1040+
// XML Namespaces reserves the `xml` prefix as implicitly bound to
1041+
// `http://www.w3.org/XML/1998/namespace`. Canonical XML §2.3 requires
1042+
// that this binding is never emitted as an `xmlns:xml` declaration.
1043+
let result = c14n("<root xml:lang=\"en\">hello</root>");
1044+
assert_eq!(result, "<root xml:lang=\"en\">hello</root>");
1045+
assert!(
1046+
!result.contains("xmlns:xml"),
1047+
"implicit xml namespace should not be emitted, got: {result}"
1048+
);
1049+
}
1050+
1051+
#[test]
1052+
fn test_c14n_exclusive_xml_prefix_not_emitted_on_root() {
1053+
// Exclusive C14N §2.4 treats the `xml` prefix the same as inclusive:
1054+
// it is always implicitly bound and must never be emitted.
1055+
let doc = Document::parse_str("<root xml:lang=\"en\">hello</root>").unwrap();
1056+
let result = canonicalize(
1057+
&doc,
1058+
&C14nOptions {
1059+
with_comments: false,
1060+
exclusive: true,
1061+
inclusive_prefixes: vec![],
1062+
},
1063+
);
1064+
assert_eq!(result, "<root xml:lang=\"en\">hello</root>");
1065+
assert!(!result.contains("xmlns:xml"));
1066+
}
1067+
1068+
#[test]
1069+
fn test_c14n_exclusive_xml_prefix_not_emitted_on_subtree() {
1070+
// When canonicalizing a subtree whose ancestor carries `xml:lang`,
1071+
// the subtree must not spuriously declare `xmlns:xml` just because
1072+
// an `xml:` attribute appears in scope.
1073+
let doc = Document::parse_str(
1074+
"<root xmlns=\"http://example.com\" xml:lang=\"en\">\
1075+
<child xml:space=\"preserve\">hi</child></root>",
1076+
)
1077+
.unwrap();
1078+
let root = doc.root_element().unwrap();
1079+
let child = doc
1080+
.children(root)
1081+
.find(|&n| doc.node_name(n) == Some("child"))
1082+
.unwrap();
1083+
1084+
let result = canonicalize_subtree(
1085+
&doc,
1086+
child,
1087+
&C14nOptions {
1088+
with_comments: false,
1089+
exclusive: true,
1090+
inclusive_prefixes: vec![],
1091+
},
1092+
);
1093+
1094+
assert!(
1095+
!result.contains("xmlns:xml"),
1096+
"implicit xml namespace should not be emitted in exclusive C14N, got: {result}"
1097+
);
1098+
// The default namespace from the root IS visibly utilized by the
1099+
// child element's unprefixed name, so it should appear.
1100+
assert!(result.contains("xmlns=\"http://example.com\""));
1101+
assert!(result.contains("xml:space=\"preserve\""));
1102+
}
10211103
}

0 commit comments

Comments
 (0)