xml-sec 0.1.23

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::HashSet;

use crate::xml::dom::Node;
use crate::xml::dom::storage::Storage as DomStorage;

use super::C14nError;
use super::NodeVisibility;
use super::ns_common::collect_ns_declarations;
use super::prefix::{attribute_prefix, element_prefix};
use super::scratch::{List, NamespaceMap, NamespaceRendering, Scratch};
use super::serialize::NsRenderer;

/// 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: Prefixes<'a>,
}

enum Prefixes<'a> {
    Stored(&'a HashSet<String>),
    #[cfg(feature = "xmldsig")]
    Lexical(&'a str),
}
impl Prefixes<'_> {
    fn contains(&self, prefix: &str) -> bool {
        match self {
            Self::Stored(prefixes) => prefixes.contains(prefix),
            #[cfg(feature = "xmldsig")]
            Self::Lexical(list) => {
                for token in super::prefix::inclusive_tokens(list) {
                    if token == prefix || (prefix.is_empty() && token == "#default") {
                        return true;
                    }
                }
                false
            }
        }
    }
    fn work(&self, prefix: &str) -> usize {
        let probes = match self {
            Self::Stored(set) => set.capacity(),
            #[cfg(feature = "xmldsig")]
            Self::Lexical(list) => list.len(),
        };
        // Includes token scanning/hash computation and every possible equality
        // comparison. Four lookups per namespace cover both rendering passes.
        probes
            .saturating_add(2)
            .saturating_mul(prefix.len().saturating_add(1))
            .saturating_mul(4)
    }
}

impl<'a> ExclusiveNsRenderer<'a> {
    pub(crate) fn new(inclusive_prefixes: &'a HashSet<String>) -> Self {
        Self {
            inclusive_prefixes: Prefixes::Stored(inclusive_prefixes),
        }
    }
    #[cfg(feature = "xmldsig")]
    pub(crate) fn from_prefix_list(list: &'a str) -> Self {
        Self {
            inclusive_prefixes: Prefixes::Lexical(list),
        }
    }
}

impl<S: Scratch, D: DomStorage> NsRenderer<S, D> for ExclusiveNsRenderer<'_> {
    fn render_namespaces<'n>(
        &self,
        node: Node<'n, '_, D>,
        parent_rendered: &S::Namespaces<'n>,
        visibility: Option<&dyn NodeVisibility<D>>,
        scratch: &S,
    ) -> Result<NamespaceRendering<'n, S>, C14nError> {
        let utilized = visibly_utilized_prefixes(node, visibility, scratch)?;
        // Admit probing of the borrowed, immutable PrefixList before either
        // predicate uses it. No owned prefix strings are copied into scratch.
        for ns in node.namespaces() {
            let prefix = ns.name().unwrap_or("");
            scratch.charge(self.inclusive_prefixes.work(prefix))?;
        }
        // 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, _| {
                Ok(utilized.get(prefix, false)?.is_some()
                    || self.inclusive_prefixes.contains(prefix))
            },
            scratch,
        )?;

        if let (Some(visibility), Some(parent)) = (visibility, node.parent())
            && parent.is_element()
        {
            for (ordinal, namespace) in node.namespaces().enumerate() {
                let prefix = namespace.name().unwrap_or("");
                let uri = namespace.uri();
                scratch.charge(prefix.len().saturating_add(uri.len()).saturating_add(1))?;
                let selected_here = visibility.contains_namespace_at(node, ordinal, namespace)?;
                // No search of `declarations` is needed: the shared pipeline
                // declares a selected prefix only when its parent binding
                // differs, and its one invisible-default entry is never selected
                // here; prefixes pushed below are unique namespace nodes.
                let declaration_suppressed =
                    parent_rendered.get(prefix, false)?.flatten() == Some(uri);
                debug_assert!(
                    !(selected_here
                        && declaration_suppressed
                        && declarations.iter().any(|(declared, _)| *declared == prefix))
                );
                let nearest_utilizer_omitted_namespace =
                    parent_rendered.get(prefix, true)? == Some(None);
                let uses_exclusive_rendering = utilized.get(prefix, false)?.is_some()
                    && !self.inclusive_prefixes.contains(prefix);

                if uses_exclusive_rendering
                    && selected_here
                    && declaration_suppressed
                    && nearest_utilizer_omitted_namespace
                {
                    declarations.push((prefix, uri))?;
                    declarations.sort(|left, right| {
                        scratch.charge(left.0.len().min(right.0.len()))?;
                        Ok(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(prefix, true, selected_here.then_some(uri))?;
                }
            }
        }

        Ok((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, S: Scratch, D: DomStorage>(
    node: Node<'a, '_, D>,
    visibility: Option<&dyn NodeVisibility<D>>,
    scratch: &S,
) -> Result<S::Namespaces<'a>, C14nError> {
    let mut utilized = scratch.namespaces();

    // Element's own lexical prefix from source XML.
    let el_prefix = element_prefix(node);
    if !el_prefix.is_empty() {
        utilized.insert(el_prefix, false, None)?;
    } 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("", false, None)?;
    }

    // Attribute lexical prefixes from source XML.
    for (ordinal, attr) in node.attributes().enumerate() {
        scratch.charge(attr.name().len().saturating_add(1))?;
        if let Some(set) = visibility
            && !set.contains_attribute_at(node, ordinal, attr)?
        {
            continue;
        }
        let attr_prefix = attribute_prefix(node, &attr);
        if !attr_prefix.is_empty() {
            utilized.insert(attr_prefix, false, None)?;
        }
    }

    Ok(utilized)
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    #[cfg(feature = "xmldsig")]
    #[test]
    fn stored_and_lexical_prefix_lists_use_the_same_xml_separators() {
        // A non-XML Unicode space must not split a token in one representation
        // while remaining token content in the other representation.
        let list = "p\u{a0}q p\t#default";
        let algorithm = crate::c14n::C14nAlgorithm::new(crate::c14n::C14nMode::Exclusive1_0, false)
            .with_prefix_list(list);
        let stored = ExclusiveNsRenderer::new(algorithm.inclusive_prefixes());
        let lexical = ExclusiveNsRenderer::from_prefix_list(list);
        for (prefix, expected) in [("p", true), ("q", false), ("p\u{a0}q", true), ("", true)] {
            assert_eq!(stored.inclusive_prefixes.contains(prefix), expected);
            assert_eq!(lexical.inclusive_prefixes.contains(prefix), expected);
        }
    }

    use super::super::serialize::{
        C14nConfig, serialize_canonical, serialize_canonical_visible_with_position,
    };
    use super::*;
    use crate::xml::dom::Document;
    use std::collections::HashSet;

    struct NamespaceGapVisibility;

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

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

        fn contains_namespace(
            &self,
            owner: Node<'_, '_>,
            prefix: &str,
            _uri: &str,
        ) -> Result<bool, C14nError> {
            Ok(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>"#
        );
    }
}