Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 28 additions & 23 deletions src/parser/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,14 @@ impl<'a> XmlParser<'a> {
let _ = found_dup; // suppress unused warning
}

// --- Apply #FIXED default attributes from DTD ATTLIST declarations ---
// Per XML 1.0 §3.3.2, if an attribute declared with #FIXED is not
// --- Apply DTD ATTLIST default attributes (#FIXED and #DEFAULT) ---
// Per XML 1.0 §3.3.2, when an attribute declared in an ATTLIST is not
// present on the element, the parser must add it with the declared
// default value. #DEFAULT attributes are tracked for amplification
// factor checking but not inserted into the tree (matching libxml2).
// default value. This applies to both `#FIXED "v"` and bare `"v"`
// (so-called #DEFAULT) declarations. libxml2 applies both during
// normal parsing — verifiable via `xmllint --c14n` on a document
// with an ATTLIST default, which emits the default attribute in the
// canonical form.
// Namespace declarations (xmlns, xmlns:prefix) are inserted before
// other attributes to match libxml2's attribute ordering.
if let Some(defaults) = if self.attr_defaults.is_empty() {
Expand All @@ -453,27 +456,29 @@ impl<'a> XmlParser<'a> {
.any(|a| a.name == decl_local && a.prefix.as_deref() == decl_pfx);
if !already_present {
// Track expansion for amplification factor check
// (both #FIXED and #DEFAULT contribute to expansion)
// (both #FIXED and #DEFAULT contribute to expansion).
self.expansion_size += attr_name.len() + value.len();

// Only insert #FIXED attributes into the tree
if is_fixed {
let (decl_prefix, decl_local) = split_name(attr_name);
let attr = Attribute {
name: decl_local.to_string(),
value,
prefix: decl_prefix.map(String::from),
namespace: None,
raw_value: None,
};
let is_ns_decl =
attr_name == "xmlns" || attr_name.starts_with("xmlns:");
if is_ns_decl {
attributes.insert(insert_pos, attr);
insert_pos += 1;
} else {
attributes.push(attr);
}
// Insert both #FIXED and #DEFAULT defaults into the
// tree. The `is_fixed` flag is no longer used to gate
// insertion; it would only matter if we additionally
// validated source attributes against #FIXED values,
// which is a separate validation step.
let _ = is_fixed;
let (decl_prefix, decl_local) = split_name(attr_name);
let attr = Attribute {
name: decl_local.to_string(),
value,
prefix: decl_prefix.map(String::from),
namespace: None,
raw_value: None,
};
let is_ns_decl = attr_name == "xmlns" || attr_name.starts_with("xmlns:");
if is_ns_decl {
attributes.insert(insert_pos, attr);
insert_pos += 1;
} else {
attributes.push(attr);
}
}
}
Expand Down
22 changes: 19 additions & 3 deletions src/serial/c14n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,10 +329,26 @@ impl<'a> C14nContext<'a> {
let mut ns_to_output: Vec<(String, String)> = Vec::new();

for (ns_prefix, ns_uri) in &ns_decls {
if current_rendered.get(ns_prefix) != Some(ns_uri) {
ns_to_output.push((ns_prefix.clone(), ns_uri.clone()));
current_rendered.insert(ns_prefix.clone(), ns_uri.clone());
if current_rendered.get(ns_prefix) == Some(ns_uri) {
continue;
}

// Special case for `xmlns=""`: per Canonical XML 1.0 §3.7 and
// c14n11 §3.1, the empty default-namespace declaration is only
// emitted to undeclare a *non-empty* inherited default. If no
// non-empty default is currently in scope (parent has no default,
// or the inherited default is itself empty), the `xmlns=""` from
// the source must not appear in the canonical form.
if ns_prefix.is_empty() && ns_uri.is_empty() {
let has_nonempty_inherited_default =
current_rendered.get("").is_some_and(|s| !s.is_empty());
if !has_nonempty_inherited_default {
continue;
}
}

ns_to_output.push((ns_prefix.clone(), ns_uri.clone()));
current_rendered.insert(ns_prefix.clone(), ns_uri.clone());
}

if !self.options.exclusive {
Expand Down
Loading