diff --git a/src/serial/c14n.rs b/src/serial/c14n.rs index 576112c..2164e0e 100644 --- a/src/serial/c14n.rs +++ b/src/serial/c14n.rs @@ -335,14 +335,20 @@ impl<'a> C14nContext<'a> { } } - if !self.options.exclusive { - self.check_default_ns_undeclaration( - &ns_decls, - attributes, - &mut ns_to_output, - &mut current_rendered, - ); - } + // The default-namespace undeclaration rule applies in both modes. If + // the parent's default namespace is non-empty and visibly rendered in + // scope, and the current element is in no namespace (the source has + // an explicit `xmlns=""`), the canonical output must emit `xmlns=""` + // to undeclare it. Canonical XML 1.0 §2.3 covers the inclusive case; + // Exclusive C14N §3 inherits the rule (the default prefix is part of + // the visibly-utilized set when an explicit undeclaration is present + // in the source and the inherited default would otherwise propagate). + self.check_default_ns_undeclaration( + &ns_decls, + attributes, + &mut ns_to_output, + &mut current_rendered, + ); // Suppress unused variable warnings for future use let _ = (id, name); @@ -1101,4 +1107,62 @@ mod tests { assert!(result.contains("xmlns=\"http://example.com\"")); assert!(result.contains("xml:space=\"preserve\"")); } + + /// W3C Canonical XML §2.3 — when a child element is in no namespace under + /// a parent that has a non-empty default namespace, the canonical output + /// must emit `xmlns=""` to undeclare the inherited default. This is the + /// inclusive-mode baseline; libxml2's `xmllint --c14n` exhibits the same. + #[test] + fn test_c14n_inclusive_emits_default_ns_undeclaration() { + let xml = + r#"child"#; + let result = c14n(xml); + assert!( + result.contains(""), + "default namespace undeclaration missing, got: {result}" + ); + } + + /// W3C Exclusive C14N §3 inherits Canonical XML's default-namespace + /// undeclaration rule. Without it, the canonical form leaks the parent's + /// default namespace into a child that explicitly has none, producing a + /// digest that diverges from libxml2 / xmlsec. + #[test] + fn test_c14n_exclusive_emits_default_ns_undeclaration() { + let xml = + r#"child"#; + let doc = Document::parse_str(xml).unwrap(); + let result = canonicalize( + &doc, + &C14nOptions { + with_comments: false, + exclusive: true, + inclusive_prefixes: vec![], + }, + ); + assert!( + result.contains(""), + "exclusive C14N must emit xmlns=\"\" to undeclare inherited default ns, got: {result}" + ); + } + + /// Negative companion: when no inherited default exists, the output must + /// NOT emit a spurious `xmlns=""`. + #[test] + fn test_c14n_exclusive_no_undeclaration_when_no_inherited_default() { + let xml = r"x"; + 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}" + ); + } }