Skip to main content

xml_sec/xmldsig/
builder.rs

1//! Builders for deterministic XMLDSig signature templates.
2
3use std::{collections::HashSet, io::Write};
4
5use base64::Engine;
6use quick_xml::Writer;
7use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
8
9use crate::c14n::{C14nAlgorithm, C14nMode, canonicalize_bounded_with_xml_base_budget};
10use crate::policy::{PolicyViolation, SigningPolicy};
11use crate::xml::{is_xml_1_0_character, is_xml_ncname};
12
13use super::mutation::{
14    fill_signature_value_with_budget, fill_signed_info_digest_values_with_budget,
15    padded_base64_len_for_xml, projected_signature_value_output_len_at_index_with_budget,
16    zero_base64_placeholder,
17};
18use super::transforms::{
19    ENVELOPED_SIGNATURE_XPATH_EXPR, ENVELOPED_SIGNATURE_XPATH_PREFIX, TransformExecutionBudget,
20    XPathSignatureParseBudget, map_c14n_resource_policy_violation,
21    validate_signing_transform_policy, validate_xpath_namespace_budget_with_resources,
22};
23use super::uri::validate_signing_reference_uri;
24use super::{
25    BASE64_TRANSFORM_URI, DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm, Transform,
26    XPATH_FILTER2_TRANSFORM_URI, XPATH_TRANSFORM_URI, XPathExpression,
27};
28
29const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
30const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
31const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
32const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
33
34/// Errors produced while validating or serializing an XMLDSig template.
35#[derive(Debug, thiserror::Error)]
36pub enum SignatureBuilderError {
37    /// A namespace prefix was not a supported XML NCName.
38    #[error("invalid XML namespace prefix: {0}")]
39    InvalidNamespacePrefix(String),
40    /// A namespace URI could not be represented in an XML 1.0 declaration.
41    #[error("XML namespace URI contains a character forbidden by XML 1.0: {0:?}")]
42    InvalidNamespaceUri(String),
43    /// An XPath binding would rebind the prefix used by XMLDSig elements.
44    #[error("XPath namespace binding conflicts with XMLDSig prefix: {0}")]
45    NamespacePrefixConflict(String),
46    /// An XMLDSig Id attribute was not a valid XML NCName.
47    #[error("invalid {element} Id: {value}")]
48    InvalidId {
49        /// XMLDSig element carrying the Id attribute.
50        element: &'static str,
51        /// Rejected attribute value.
52        value: String,
53    },
54    /// XMLDSig requires at least one reference in SignedInfo.
55    #[error("a signature template requires at least one Reference")]
56    MissingReference,
57    /// The immutable signing policy rejected the template.
58    #[error("signing policy violation: {0}")]
59    Policy(#[from] PolicyViolation),
60    /// An XPath parameter cannot be parsed or exceeds its resource bounds.
61    #[error("invalid XPath expression: {0}")]
62    InvalidXPath(String),
63    /// SHA-1 algorithms are available for verification but not new signatures.
64    #[error("algorithm is not allowed for signing: {0}")]
65    SigningAlgorithmDisabled(&'static str),
66    /// The XML writer failed.
67    #[error("XML serialization error: {0}")]
68    Serialization(#[from] std::io::Error),
69    /// The writer unexpectedly emitted bytes that are not UTF-8.
70    #[error("XML writer emitted invalid UTF-8: {0}")]
71    InvalidUtf8(#[from] std::string::FromUtf8Error),
72    /// The generated template could not be parsed under the selected policy.
73    #[error("generated XML template is invalid: {0}")]
74    GeneratedXml(#[from] roxmltree::Error),
75    /// The generated SignedInfo could not be canonicalized.
76    #[error("generated SignedInfo canonicalization failed: {0}")]
77    Canonicalization(#[from] crate::c14n::C14nError),
78    /// The generated template did not contain its required SignedInfo child.
79    #[error("generated XML template is missing SignedInfo")]
80    MissingGeneratedSignedInfo,
81    /// The generated template could not be prepared for policy validation.
82    #[error("generated XML template validation failed: {0}")]
83    GeneratedMutation(#[source] super::mutation::XmlMutationError),
84}
85
86/// Builder for a single XMLDSig `<Reference>` template.
87#[derive(Debug, Clone)]
88pub struct ReferenceBuilder {
89    uri: String,
90    id: Option<String>,
91    ref_type: Option<String>,
92    transforms: Vec<Transform>,
93    digest_method: DigestAlgorithm,
94}
95
96impl ReferenceBuilder {
97    /// Create a reference using the required digest algorithm.
98    #[must_use]
99    pub fn new(digest_method: DigestAlgorithm) -> Self {
100        Self {
101            uri: String::new(),
102            id: None,
103            ref_type: None,
104            transforms: Vec::new(),
105            digest_method,
106        }
107    }
108
109    /// Set the reference URI.
110    ///
111    /// References default to the empty same-document URI because the signing
112    /// pipeline requires an explicit `URI` attribute.
113    #[must_use]
114    pub fn uri(mut self, uri: impl Into<String>) -> Self {
115        self.uri = uri.into();
116        self
117    }
118
119    /// Set the optional reference Id.
120    #[must_use]
121    pub fn id(mut self, id: impl Into<String>) -> Self {
122        self.id = Some(id.into());
123        self
124    }
125
126    /// Set the optional reference Type URI.
127    #[must_use]
128    pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
129        self.ref_type = Some(ref_type.into());
130        self
131    }
132
133    /// Append a transform, preserving insertion order.
134    #[must_use]
135    pub fn transform(mut self, transform: Transform) -> Self {
136        self.transforms.push(transform);
137        self
138    }
139}
140
141/// Builder for a complete XMLDSig `<Signature>` template.
142#[derive(Debug, Clone)]
143pub struct SignatureBuilder {
144    c14n_method: C14nAlgorithm,
145    sign_method: SignatureAlgorithm,
146    ns_prefix: Option<String>,
147    signature_id: Option<String>,
148    references: Vec<ReferenceBuilder>,
149    include_key_info: bool,
150}
151
152impl SignatureBuilder {
153    /// Create a signature template using the required algorithms.
154    #[must_use]
155    pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
156        Self {
157            c14n_method,
158            sign_method,
159            ns_prefix: None,
160            signature_id: None,
161            references: Vec::new(),
162            include_key_info: false,
163        }
164    }
165
166    /// Use a namespace prefix such as `ds`; the default is an unprefixed namespace.
167    #[must_use]
168    pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
169        self.ns_prefix = Some(prefix.into());
170        self
171    }
172
173    /// Set the optional Signature Id.
174    #[must_use]
175    pub fn signature_id(mut self, id: impl Into<String>) -> Self {
176        self.signature_id = Some(id.into());
177        self
178    }
179
180    /// Append a reference, preserving insertion order.
181    #[must_use]
182    pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
183        self.references.push(reference);
184        self
185    }
186
187    /// Control whether an empty KeyInfo placeholder is emitted.
188    #[must_use]
189    pub fn key_info(mut self, include: bool) -> Self {
190        self.include_key_info = include;
191        self
192    }
193
194    /// Build a namespace-correct XMLDSig template with empty digest and signature values.
195    pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
196        self.build_template_with_policy(&SigningPolicy::default())
197    }
198
199    /// Build a template after enforcing the same immutable policy snapshot used
200    /// by the signing operation that will consume it. This key-independent
201    /// method validates generated digest widths; `SignContext::sign_with_builder`
202    /// additionally validates the exact key-dependent `SignatureValue` width.
203    pub fn build_template_with_policy(
204        &self,
205        policy: &SigningPolicy,
206    ) -> Result<String, SignatureBuilderError> {
207        let budget = TransformExecutionBudget::from_resources(&policy.resources);
208        let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(&policy.resources);
209        self.build_template_with_policy_and_signature_output_len(
210            policy,
211            None,
212            &budget,
213            &mut xpath_parse_budget,
214        )
215    }
216
217    pub(super) fn build_template_with_policy_for_signature_output(
218        &self,
219        policy: &SigningPolicy,
220        signature_output_len: usize,
221        budget: &TransformExecutionBudget,
222        xpath_parse_budget: &mut XPathSignatureParseBudget,
223    ) -> Result<String, SignatureBuilderError> {
224        self.build_template_with_policy_and_signature_output_len(
225            policy,
226            Some(signature_output_len),
227            budget,
228            xpath_parse_budget,
229        )
230    }
231
232    fn build_template_with_policy_and_signature_output_len(
233        &self,
234        policy: &SigningPolicy,
235        signature_output_len: Option<usize>,
236        budget: &TransformExecutionBudget,
237        xpath_parse_budget: &mut XPathSignatureParseBudget,
238    ) -> Result<String, SignatureBuilderError> {
239        policy.validate()?;
240        self.validate(policy, xpath_parse_budget)?;
241
242        let prefix = self.ns_prefix.as_deref();
243        let mut writer = Writer::new(Vec::new());
244        let signature_name = qualified_name(prefix, "Signature");
245        let mut signature = BytesStart::new(&signature_name);
246        let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
247        signature.push_attribute((namespace_attr.as_str(), XMLDSIG_NS));
248        if let Some(id) = &self.signature_id {
249            signature.push_attribute(("Id", id.as_str()));
250        }
251        writer.write_event(Event::Start(signature))?;
252
253        write_start(&mut writer, prefix, "SignedInfo")?;
254        write_algorithm(
255            &mut writer,
256            prefix,
257            "CanonicalizationMethod",
258            self.c14n_method.uri(),
259        )?;
260        write_algorithm(
261            &mut writer,
262            prefix,
263            "SignatureMethod",
264            self.sign_method.uri(),
265        )?;
266        for reference in &self.references {
267            write_reference(&mut writer, prefix, reference)?;
268        }
269        write_end(&mut writer, prefix, "SignedInfo")?;
270        write_empty(&mut writer, prefix, "SignatureValue")?;
271        if self.include_key_info {
272            write_empty(&mut writer, prefix, "KeyInfo")?;
273        }
274        writer.write_event(Event::End(BytesEnd::new(signature_name)))?;
275
276        let template = String::from_utf8(writer.into_inner())?;
277        // Field-level checks bound each input class; these checks cover the
278        // completed artifact exactly as the signing operation will consume it.
279        policy.resources.validate_xml_document_len(template.len())?;
280        let digest_placeholders = self.references.iter().map(|reference| {
281            base64::engine::general_purpose::STANDARD.encode(vec![
282                0_u8;
283                reference
284                    .digest_method
285                    .output_len()
286            ])
287        });
288        let digest_validation_template = fill_signed_info_digest_values_with_budget(
289            &template,
290            digest_placeholders,
291            Some(policy),
292            Some(budget.xml_parse_work()),
293        )
294        .map_err(map_generated_mutation_error)?;
295        let validation_template = if let Some(signature_output_len) = signature_output_len {
296            let encoded_signature_len = padded_base64_len_for_xml(signature_output_len, policy)
297                .map_err(map_generated_mutation_error)?;
298            let projected_document_len = projected_signature_value_output_len_at_index_with_budget(
299                &digest_validation_template,
300                encoded_signature_len,
301                0,
302                Some(policy),
303                Some(budget.xml_parse_work()),
304            )
305            .map_err(map_generated_mutation_error)?;
306            policy
307                .resources
308                .validate_xml_document_len(projected_document_len)?;
309            let signature_placeholder =
310                zero_base64_placeholder(signature_output_len, encoded_signature_len);
311            fill_signature_value_with_budget(
312                &digest_validation_template,
313                &signature_placeholder,
314                Some(policy),
315                Some(budget.xml_parse_work()),
316            )
317            .map_err(map_generated_mutation_error)?
318        } else {
319            digest_validation_template
320        };
321        policy
322            .resources
323            .validate_xml_document_len(validation_template.len())?;
324        let settings =
325            crate::document::DocumentParseSettings::from_policy(&policy.xml, &policy.resources);
326        let document = super::mutation::parse_with_options_and_budget(
327            &validation_template,
328            settings,
329            Some(budget.xml_parse_work()),
330        )
331        .map_err(|error| match error.into_policy_violation(settings) {
332            Ok(error) => SignatureBuilderError::Policy(error),
333            Err(crate::document::XmlDocumentError::Parse(error)) => {
334                SignatureBuilderError::GeneratedXml(error)
335            }
336            Err(error) => SignatureBuilderError::GeneratedMutation(
337                super::mutation::XmlMutationError::Document(error),
338            ),
339        })?;
340        let signed_info = document
341            .root_element()
342            .children()
343            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
344            .ok_or(SignatureBuilderError::MissingGeneratedSignedInfo)?;
345        let signed_info_subtree: HashSet<_> =
346            signed_info.descendants().map(|node| node.id()).collect();
347        let mut canonical_signed_info = Vec::new();
348        canonicalize_bounded_with_xml_base_budget(
349            &document,
350            Some(&|node| signed_info_subtree.contains(&node.id())),
351            &self.c14n_method,
352            budget.remaining_c14n_output(),
353            budget.xml_base_resolution(),
354            &mut canonical_signed_info,
355        )
356        .map_err(|error| {
357            map_c14n_resource_policy_violation(
358                &error,
359                crate::policy::resource_name::CANONICALIZED_BYTES,
360                budget.c14n_output_limit(),
361            )
362            .map_or_else(
363                || SignatureBuilderError::Canonicalization(error),
364                SignatureBuilderError::Policy,
365            )
366        })?;
367        budget.charge_c14n_output_policy(canonical_signed_info.len())?;
368        Ok(template)
369    }
370
371    pub(super) fn signature_method(&self) -> SignatureAlgorithm {
372        self.sign_method
373    }
374
375    fn validate(
376        &self,
377        policy: &SigningPolicy,
378        xpath_signature_budget: &mut XPathSignatureParseBudget,
379    ) -> Result<(), SignatureBuilderError> {
380        if let Some(prefix) = &self.ns_prefix
381            && !is_namespace_prefix(prefix)
382        {
383            return Err(SignatureBuilderError::InvalidNamespacePrefix(
384                prefix.clone(),
385            ));
386        }
387        if let Some(id) = &self.signature_id
388            && !is_xml_ncname(id)
389        {
390            return Err(SignatureBuilderError::InvalidId {
391                element: "Signature",
392                value: id.clone(),
393            });
394        }
395        if self.references.is_empty() {
396            return Err(SignatureBuilderError::MissingReference);
397        }
398        if self.references.len() > policy.resources.max_references {
399            return Err(PolicyViolation::ResourceLimit {
400                resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
401                maximum: policy.resources.max_references,
402                actual: self.references.len(),
403            }
404            .into());
405        }
406        for reference in &self.references {
407            validate_signing_reference_uri(&reference.uri, policy)?;
408            if reference.transforms.len() > policy.resources.max_transforms_per_reference {
409                return Err(PolicyViolation::ResourceLimit {
410                    resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
411                    maximum: policy.resources.max_transforms_per_reference,
412                    actual: reference.transforms.len(),
413                }
414                .into());
415            }
416            let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
417            validate_signing_transform_policy(
418                initial_binary,
419                &reference.transforms,
420                policy.transforms.allowed_algorithms.as_ref(),
421            )?;
422            for transform in &reference.transforms {
423                match transform {
424                    Transform::XpathExcludeAllSignatures => {
425                        validate_xpath_source(
426                            ENVELOPED_SIGNATURE_XPATH_EXPR,
427                            xpath_signature_budget,
428                        )?;
429                    }
430                    Transform::XPath(xpath) => {
431                        validate_xpath_source(xpath.expression(), xpath_signature_budget)?;
432                    }
433                    Transform::XPathFilter2(filters) => {
434                        if filters.is_empty() {
435                            return Err(SignatureBuilderError::InvalidXPath(
436                                "XPath Filter 2.0 requires at least one expression".into(),
437                            ));
438                        }
439                        if filters.len() > policy.resources.max_xpath_filters {
440                            return Err(PolicyViolation::ResourceLimit {
441                                resource: crate::policy::resource_name::XPATH_FILTERS,
442                                maximum: policy.resources.max_xpath_filters,
443                                actual: filters.len(),
444                            }
445                            .into());
446                        }
447                        for filter in filters {
448                            validate_xpath_source(
449                                filter.xpath().expression(),
450                                xpath_signature_budget,
451                            )?;
452                        }
453                    }
454                    _ => {}
455                }
456            }
457            validate_xpath_namespace_budget_with_resources(
458                &reference.transforms,
459                self.ns_prefix.as_deref().map(|prefix| (prefix, XMLDSIG_NS)),
460                &policy.resources,
461            )
462            .map_err(map_transform_validation_error)?;
463        }
464        for (prefix, uri, shares_signature_namespace) in
465            self.references.iter().flat_map(|reference| {
466                reference
467                    .transforms
468                    .iter()
469                    .flat_map(|transform| match transform {
470                        Transform::XPath(xpath) => xpath
471                            .namespaces()
472                            .iter()
473                            .map(|(prefix, uri)| (prefix, uri, true))
474                            .collect::<Vec<_>>(),
475                        Transform::XPathFilter2(filters) => filters
476                            .iter()
477                            .flat_map(|filter| {
478                                filter
479                                    .xpath()
480                                    .namespaces()
481                                    .iter()
482                                    .map(|(prefix, uri)| (prefix, uri, false))
483                            })
484                            .collect(),
485                        _ => Vec::new(),
486                    })
487            })
488        {
489            // Namespaces in XML reserves the declaration namespace and both
490            // sides of the `xml` binding; prefixed declarations cannot be empty.
491            if uri.is_empty() || uri == XMLNS_NS || !uri.chars().all(is_xml_1_0_character) {
492                return Err(SignatureBuilderError::InvalidNamespaceUri(uri.clone()));
493            }
494            if prefix == "xmlns"
495                || (prefix == "xml") != (uri == XML_NS)
496                || (prefix != "xml" && !is_namespace_prefix(prefix))
497            {
498                return Err(SignatureBuilderError::InvalidNamespacePrefix(
499                    prefix.clone(),
500                ));
501            }
502            // Ordinary XPath parameters share the Signature namespace prefix,
503            // while Filter2 parameters are unprefixed in their own namespace.
504            if shares_signature_namespace
505                && self.ns_prefix.as_ref() == Some(prefix)
506                && uri != XMLDSIG_NS
507            {
508                return Err(SignatureBuilderError::NamespacePrefixConflict(
509                    prefix.clone(),
510                ));
511            }
512        }
513        if !self.sign_method.signing_allowed() {
514            return Err(SignatureBuilderError::SigningAlgorithmDisabled(
515                self.sign_method.uri(),
516            ));
517        }
518        if policy
519            .signature_algorithms
520            .as_ref()
521            .is_some_and(|allowed| !allowed.contains(&self.sign_method))
522        {
523            return Err(PolicyViolation::Algorithm {
524                operation: "signing",
525                algorithm: self.sign_method.uri().to_owned(),
526            }
527            .into());
528        }
529        if policy
530            .transforms
531            .allowed_algorithms
532            .as_ref()
533            .is_some_and(|allowed| !allowed.contains(self.c14n_method.uri()))
534        {
535            return Err(PolicyViolation::Algorithm {
536                operation: "signing transform",
537                algorithm: self.c14n_method.uri().to_owned(),
538            }
539            .into());
540        }
541        for reference in &self.references {
542            if let Some(id) = &reference.id
543                && !is_xml_ncname(id)
544            {
545                return Err(SignatureBuilderError::InvalidId {
546                    element: "Reference",
547                    value: id.clone(),
548                });
549            }
550            if !reference.digest_method.signing_allowed() {
551                return Err(SignatureBuilderError::SigningAlgorithmDisabled(
552                    reference.digest_method.uri(),
553                ));
554            }
555            if policy
556                .digest_algorithms
557                .as_ref()
558                .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
559            {
560                return Err(PolicyViolation::Algorithm {
561                    operation: "signing",
562                    algorithm: reference.digest_method.uri().to_owned(),
563                }
564                .into());
565            }
566        }
567        Ok(())
568    }
569}
570
571fn validate_xpath_source(
572    source: &str,
573    budget: &mut XPathSignatureParseBudget,
574) -> Result<(), SignatureBuilderError> {
575    if let Some(character) = source
576        .chars()
577        .find(|character| !is_xml_1_0_character(*character))
578    {
579        return Err(SignatureBuilderError::InvalidXPath(format!(
580            "XPath expression contains a character forbidden by XML 1.0: {character:?}"
581        )));
582    }
583    budget
584        .validate_expression(source)
585        .map_err(map_transform_validation_error)
586}
587
588fn map_transform_validation_error(error: super::TransformError) -> SignatureBuilderError {
589    match error {
590        super::TransformError::Policy(error) => SignatureBuilderError::Policy(error),
591        error => SignatureBuilderError::InvalidXPath(error.to_string()),
592    }
593}
594
595fn map_generated_mutation_error(error: super::mutation::XmlMutationError) -> SignatureBuilderError {
596    match error {
597        super::mutation::XmlMutationError::Policy(violation) => {
598            SignatureBuilderError::Policy(violation)
599        }
600        other => SignatureBuilderError::GeneratedMutation(other),
601    }
602}
603
604fn write_reference<W: Write>(
605    writer: &mut Writer<W>,
606    prefix: Option<&str>,
607    reference: &ReferenceBuilder,
608) -> Result<(), std::io::Error> {
609    let name = qualified_name(prefix, "Reference");
610    let mut element = BytesStart::new(&name);
611    if let Some(id) = &reference.id {
612        element.push_attribute(("Id", id.as_str()));
613    }
614    if let Some(ref_type) = &reference.ref_type {
615        element.push_attribute(("Type", ref_type.as_str()));
616    }
617    element.push_attribute(("URI", reference.uri.as_str()));
618    writer.write_event(Event::Start(element))?;
619
620    if !reference.transforms.is_empty() {
621        write_start(writer, prefix, "Transforms")?;
622        for transform in &reference.transforms {
623            write_transform(writer, prefix, transform)?;
624        }
625        write_end(writer, prefix, "Transforms")?;
626    }
627    write_algorithm(
628        writer,
629        prefix,
630        "DigestMethod",
631        reference.digest_method.uri(),
632    )?;
633    write_empty(writer, prefix, "DigestValue")?;
634    writer.write_event(Event::End(BytesEnd::new(name)))?;
635    Ok(())
636}
637
638fn write_transform<W: Write>(
639    writer: &mut Writer<W>,
640    prefix: Option<&str>,
641    transform: &Transform,
642) -> Result<(), std::io::Error> {
643    match transform {
644        Transform::Enveloped => {
645            write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
646        }
647        Transform::XpathExcludeAllSignatures => {
648            let name = qualified_name(prefix, "Transform");
649            let mut element = BytesStart::new(&name);
650            element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
651            writer.write_event(Event::Start(element))?;
652            let xpath_name = qualified_name(prefix, "XPath");
653            let mut xpath = BytesStart::new(&xpath_name);
654            let namespace = format!("xmlns:{ENVELOPED_SIGNATURE_XPATH_PREFIX}");
655            xpath.push_attribute((namespace.as_str(), XMLDSIG_NS));
656            writer.write_event(Event::Start(xpath))?;
657            writer.write_event(Event::Text(BytesText::new(ENVELOPED_SIGNATURE_XPATH_EXPR)))?;
658            writer.write_event(Event::End(BytesEnd::new(xpath_name)))?;
659            writer.write_event(Event::End(BytesEnd::new(name)))?;
660            Ok(())
661        }
662        Transform::XPath(xpath) => {
663            let transform_name = qualified_name(prefix, "Transform");
664            let mut transform_element = BytesStart::new(&transform_name);
665            transform_element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
666            writer.write_event(Event::Start(transform_element))?;
667            write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
668            writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
669            Ok(())
670        }
671        Transform::XPathFilter2(filters) => {
672            let transform_name = qualified_name(prefix, "Transform");
673            let mut transform_element = BytesStart::new(&transform_name);
674            transform_element.push_attribute(("Algorithm", XPATH_FILTER2_TRANSFORM_URI));
675            writer.write_event(Event::Start(transform_element))?;
676            for filter in filters {
677                write_xpath_expression(
678                    writer,
679                    None,
680                    "XPath",
681                    Some(filter.operation().as_str()),
682                    filter.xpath(),
683                )?;
684            }
685            writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
686            Ok(())
687        }
688        Transform::Base64Decode => {
689            write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
690        }
691        Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
692            write_algorithm(writer, prefix, "Transform", algorithm.uri())
693        }
694        Transform::C14n(algorithm) => {
695            let name = qualified_name(prefix, "Transform");
696            let mut element = BytesStart::new(&name);
697            element.push_attribute(("Algorithm", algorithm.uri()));
698            writer.write_event(Event::Start(element))?;
699
700            if algorithm.mode() == C14nMode::Exclusive1_0 {
701                let mut prefixes: Vec<&str> = algorithm
702                    .inclusive_prefixes()
703                    .iter()
704                    .map(String::as_str)
705                    .collect();
706                prefixes.sort_unstable();
707                let prefix_list = prefixes
708                    .into_iter()
709                    .map(|p| if p.is_empty() { "#default" } else { p })
710                    .collect::<Vec<_>>()
711                    .join(" ");
712                let mut inclusive = BytesStart::new("ec:InclusiveNamespaces");
713                inclusive.push_attribute(("xmlns:ec", EXCLUSIVE_C14N_NS));
714                inclusive.push_attribute(("PrefixList", prefix_list.as_str()));
715                writer.write_event(Event::Empty(inclusive))?;
716            }
717            writer.write_event(Event::End(BytesEnd::new(name)))?;
718            Ok(())
719        }
720    }
721}
722
723fn write_xpath_expression<W: Write>(
724    writer: &mut Writer<W>,
725    prefix: Option<&str>,
726    local_name: &str,
727    filter: Option<&str>,
728    xpath: &XPathExpression,
729) -> Result<(), std::io::Error> {
730    let name = qualified_name(prefix, local_name);
731    let mut element = BytesStart::new(&name);
732    let namespace_attributes = xpath
733        .namespaces()
734        .iter()
735        .filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
736        .map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
737        .collect::<Vec<_>>();
738    if prefix.is_none() && filter.is_some() {
739        element.push_attribute(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
740    }
741    if let Some(filter) = filter {
742        element.push_attribute(("Filter", filter));
743    }
744    for (attribute, uri) in &namespace_attributes {
745        element.push_attribute((attribute.as_str(), uri.as_str()));
746    }
747    writer.write_event(Event::Start(element))?;
748    writer.write_event(Event::Text(BytesText::new(xpath.expression())))?;
749    writer.write_event(Event::End(BytesEnd::new(name)))?;
750    Ok(())
751}
752
753fn write_algorithm<W: Write>(
754    writer: &mut Writer<W>,
755    prefix: Option<&str>,
756    local_name: &str,
757    algorithm: &str,
758) -> Result<(), std::io::Error> {
759    let name = qualified_name(prefix, local_name);
760    let mut element = BytesStart::new(name);
761    element.push_attribute(("Algorithm", algorithm));
762    writer.write_event(Event::Empty(element))?;
763    Ok(())
764}
765
766fn write_start<W: Write>(
767    writer: &mut Writer<W>,
768    prefix: Option<&str>,
769    local_name: &str,
770) -> Result<(), std::io::Error> {
771    writer.write_event(Event::Start(BytesStart::new(qualified_name(
772        prefix, local_name,
773    ))))?;
774    Ok(())
775}
776
777fn write_end<W: Write>(
778    writer: &mut Writer<W>,
779    prefix: Option<&str>,
780    local_name: &str,
781) -> Result<(), std::io::Error> {
782    writer.write_event(Event::End(BytesEnd::new(qualified_name(
783        prefix, local_name,
784    ))))?;
785    Ok(())
786}
787
788fn write_empty<W: Write>(
789    writer: &mut Writer<W>,
790    prefix: Option<&str>,
791    local_name: &str,
792) -> Result<(), std::io::Error> {
793    writer.write_event(Event::Empty(BytesStart::new(qualified_name(
794        prefix, local_name,
795    ))))?;
796    Ok(())
797}
798
799fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
800    prefix.map_or_else(
801        || local_name.to_owned(),
802        |prefix| format!("{prefix}:{local_name}"),
803    )
804}
805
806fn is_namespace_prefix(value: &str) -> bool {
807    // Namespaces in XML reserves these names regardless of the URI being bound.
808    !matches!(value, "xml" | "xmlns") && is_xml_ncname(value)
809}