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