xml-sec 0.1.10

Pure Rust XML Security: XMLDSig, XMLEnc, C14N. Drop-in replacement for libxmlsec1.
Documentation
//! Exclusive namespace rendering for Exclusive C14N 1.0.
//!
//! In exclusive mode, only **visibly utilized** namespace prefixes are emitted
//! on each element (plus any prefixes forced by the InclusiveNamespaces PrefixList).

use std::collections::{HashMap, HashSet};

use roxmltree::Node;

use super::NodeVisibility;
use super::ns_common::collect_ns_declarations;
use super::prefix::{attribute_prefix, element_prefix};
use super::serialize::NsRenderer;

const MISSING_NAMESPACE_NODE: &str = "\0";

fn nearest_utilizer_key(prefix: &str) -> String {
    format!("\0exclusive-nearest-utilizer:{prefix}")
}

/// Exclusive C14N namespace renderer.
///
/// Only emits namespace declarations for prefixes that are visibly utilized
/// by the element's tag name or attributes, plus any forced prefixes from
/// the `InclusiveNamespaces PrefixList`.
pub(crate) struct ExclusiveNsRenderer<'a> {
    inclusive_prefixes: &'a HashSet<String>,
}

impl<'a> ExclusiveNsRenderer<'a> {
    pub(crate) fn new(inclusive_prefixes: &'a HashSet<String>) -> Self {
        Self { inclusive_prefixes }
    }
}

impl NsRenderer for ExclusiveNsRenderer<'_> {
    fn render_namespaces<'n>(
        &self,
        node: Node<'n, '_>,
        parent_rendered: &HashMap<String, String>,
        visibility: Option<&dyn NodeVisibility>,
    ) -> (Vec<(String, String)>, HashMap<String, String>) {
        let utilized = visibly_utilized_prefixes(node, visibility);
        // Exclusive mode: only visibly-utilized prefixes and forced prefixes
        // from InclusiveNamespaces PrefixList are candidates.
        // Missing namespace nodes suppress a declaration but do not erase a
        // binding physically rendered by an output ancestor.
        let (mut declarations, mut rendered) = collect_ns_declarations(
            node,
            parent_rendered,
            visibility,
            |prefix| self.inclusive_prefixes.contains(prefix),
            |prefix, _| utilized.contains(prefix) || self.inclusive_prefixes.contains(prefix),
        );

        if let (Some(visibility), Some(parent)) = (visibility, node.parent())
            && parent.is_element()
        {
            for namespace in node.namespaces() {
                let prefix = namespace.name().unwrap_or("");
                let uri = namespace.uri();
                let selected_here = visibility.contains_namespace(node, prefix, uri);
                let declaration_suppressed = parent_rendered.get(prefix).map(String::as_str)
                    == Some(uri)
                    && !declarations
                        .iter()
                        .any(|(declared_prefix, _)| declared_prefix == prefix);
                let utilizer_key = nearest_utilizer_key(prefix);
                let nearest_utilizer_omitted_namespace = parent_rendered
                    .get(&utilizer_key)
                    .is_some_and(|selected_uri| selected_uri == MISSING_NAMESPACE_NODE);
                let uses_exclusive_rendering =
                    utilized.contains(prefix) && !self.inclusive_prefixes.contains(prefix);

                if uses_exclusive_rendering
                    && selected_here
                    && declaration_suppressed
                    && nearest_utilizer_omitted_namespace
                {
                    declarations.push((prefix.to_owned(), uri.to_owned()));
                    declarations.sort_by(|left, right| left.0.cmp(&right.0));
                }

                // Exclusive C14N section 3 compares a namespace node with the
                // nearest output ancestor that visibly utilized its prefix,
                // not merely with the physically effective output binding.
                if uses_exclusive_rendering {
                    rendered.insert(
                        utilizer_key,
                        if selected_here {
                            uri.to_owned()
                        } else {
                            MISSING_NAMESPACE_NODE.to_owned()
                        },
                    );
                }
            }
        }

        (declarations, rendered)
    }

    fn renders_selected_namespace_of_omitted_element(&self, prefix: &str) -> bool {
        self.inclusive_prefixes.contains(prefix)
    }
}

/// Determine which namespace prefixes are visibly utilized by an element.
///
/// A prefix is visibly utilized if:
/// 1. The element's tag name uses that prefix, OR
/// 2. Any attribute on the element uses that prefix.
///
/// Uses lexical prefixes extracted from source XML byte positions,
/// avoiding ambiguity when multiple prefixes bind the same namespace URI.
fn visibly_utilized_prefixes<'a>(
    node: Node<'a, '_>,
    visibility: Option<&dyn NodeVisibility>,
) -> HashSet<&'a str> {
    let mut utilized = HashSet::new();

    // Element's own lexical prefix from source XML.
    let el_prefix = element_prefix(node);
    if !el_prefix.is_empty() {
        utilized.insert(el_prefix);
    } else {
        // Unprefixed element relies on the current default-namespace binding
        // (including xmlns="" undeclaration), so the default namespace is
        // visibly utilized. This ensures xmlns="" is emitted when needed
        // to undeclare an inherited default namespace in exclusive C14N.
        utilized.insert("");
    }

    // Attribute lexical prefixes from source XML.
    for attr in node.attributes() {
        if visibility
            .is_some_and(|set| !set.contains_attribute(node, attr.namespace(), attr.name()))
        {
            continue;
        }
        let attr_prefix = attribute_prefix(node, &attr);
        if !attr_prefix.is_empty() {
            utilized.insert(attr_prefix);
        }
    }

    utilized
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::super::serialize::{
        C14nConfig, serialize_canonical, serialize_canonical_visible_with_position,
    };
    use super::*;
    use roxmltree::Document;
    use std::collections::HashSet;

    struct NamespaceGapVisibility;

    impl NodeVisibility for NamespaceGapVisibility {
        fn contains_node(&self, _node: Node<'_, '_>) -> bool {
            true
        }

        fn contains_attribute(
            &self,
            _owner: Node<'_, '_>,
            _namespace: Option<&str>,
            _local_name: &str,
        ) -> bool {
            true
        }

        fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, _uri: &str) -> bool {
            prefix != "p" || owner.tag_name().name() != "gap"
        }
    }

    fn exc_c14n(xml: &str, prefix_list: &HashSet<String>) -> String {
        let doc = Document::parse(xml).expect("parse");
        let renderer = ExclusiveNsRenderer::new(prefix_list);
        let mut out = Vec::new();
        serialize_canonical(
            &doc,
            None,
            false,
            &renderer,
            C14nConfig {
                inherit_xml_attrs: false,
                fixup_xml_base: false,
            },
            &mut out,
        )
        .expect("c14n");
        String::from_utf8(out).expect("utf8")
    }

    #[test]
    fn only_utilized_ns_rendered() {
        let xml = r#"<root xmlns:a="http://a.com" xmlns:b="http://b.com"><a:child/></root>"#;
        let result = exc_c14n(xml, &HashSet::new());
        // root uses no prefix → no ns decls on root.
        // a:child uses a: → xmlns:a on child.
        // b: is not utilized anywhere → not rendered.
        assert!(!result.contains("xmlns:b"));
        assert!(result.contains(r#"<a:child xmlns:a="http://a.com">"#));
    }

    #[test]
    fn forced_prefix_via_prefix_list() {
        let xml = r#"<root xmlns:a="http://a.com" xmlns:b="http://b.com"><child/></root>"#;
        let mut forced = HashSet::new();
        forced.insert("b".to_string());
        let result = exc_c14n(xml, &forced);
        // b: is forced via PrefixList → should appear on root.
        assert!(result.contains(r#"xmlns:b="http://b.com""#));
    }

    #[test]
    fn sibling_elements_redeclare() {
        let xml = r#"<root xmlns:a="http://a.com"><a:one/><a:two/></root>"#;
        let result = exc_c14n(xml, &HashSet::new());
        // In exclusive mode, each sibling must independently declare a:.
        assert!(result.contains(r#"<a:one xmlns:a="http://a.com">"#));
        assert!(result.contains(r#"<a:two xmlns:a="http://a.com">"#));
    }

    #[test]
    fn default_ns_utilized() {
        let xml = r#"<root xmlns="http://example.com"><child/></root>"#;
        let result = exc_c14n(xml, &HashSet::new());
        // Both root and child use the default ns.
        assert!(result.contains(r#"<root xmlns="http://example.com">"#));
        // child inherits → parent already rendered → not redeclared.
        assert_eq!(
            result,
            r#"<root xmlns="http://example.com"><child></child></root>"#
        );
    }

    #[test]
    fn unprefixed_element_undeclares_default_ns() {
        // child undeclares default ns with xmlns="". In exclusive C14N,
        // the default namespace must be visibly utilized so xmlns="" is emitted.
        let xml = r#"<root xmlns="http://example.com"><child xmlns=""/></root>"#;
        let result = exc_c14n(xml, &HashSet::new());
        assert!(
            result.contains(r#"<child xmlns="">"#),
            "xmlns=\"\" must be emitted for undeclaration. Got: {result}"
        );
    }

    #[test]
    fn redeclares_prefix_after_one_namespace_node_discontinuity() {
        // Exclusive C14N section 3 compares against the nearest output
        // ancestor that visibly utilizes the prefix. Because `gap` omits its
        // p namespace node, `leaf` must redeclare p at the first discontinuity.
        let xml = r#"<p:root xmlns:p="urn:p"><p:gap><p:leaf/></p:gap></p:root>"#;
        let doc = Document::parse(xml).expect("parse");
        let prefix_list = HashSet::new();
        let renderer = ExclusiveNsRenderer::new(&prefix_list);
        let mut out = Vec::new();
        serialize_canonical_visible_with_position(
            &doc,
            Some(&NamespaceGapVisibility),
            false,
            &renderer,
            C14nConfig {
                inherit_xml_attrs: false,
                fixup_xml_base: false,
            },
            None,
            &mut out,
        )
        .expect("c14n");

        assert_eq!(
            String::from_utf8(out).expect("utf8"),
            r#"<p:root xmlns:p="urn:p"><p:gap><p:leaf xmlns:p="urn:p"></p:leaf></p:gap></p:root>"#
        );
    }
}