1use crate::xml_input as xml_sec_xml_input;
4use std::{collections::HashSet, io::Write};
5
6use base64::Engine;
7use xml_sec_xml_input::lexical::Writer;
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, transform_chain_produces_binary,
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,
26 SignatureContext, Transform, 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#[derive(Debug, thiserror::Error)]
36pub enum SignatureBuilderError {
37 #[error("invalid XML namespace prefix: {0}")]
39 InvalidNamespacePrefix(String),
40 #[error("XML namespace URI contains a character forbidden by XML 1.0: {0:?}")]
42 InvalidNamespaceUri(String),
43 #[error("XPath namespace binding conflicts with XMLDSig prefix: {0}")]
45 NamespacePrefixConflict(String),
46 #[error("invalid {element} Id: {value}")]
48 InvalidId {
49 element: &'static str,
51 value: String,
53 },
54 #[error("a signature template requires at least one Reference")]
56 MissingReference,
57 #[error("signing policy violation: {0}")]
59 Policy(#[from] PolicyViolation),
60 #[error("invalid XPath expression: {0}")]
62 InvalidXPath(String),
63 #[error("algorithm is not allowed for signing: {0}")]
65 SigningAlgorithmDisabled(&'static str),
66 #[error("signature context is not applicable to algorithm: {0}")]
68 SignatureContextNotApplicable(&'static str),
69 #[error("RSA-PSS salt length exceeds xs:int")]
71 InvalidRsaPssParameters,
72 #[error("XML serialization error: {0}")]
74 Serialization(#[from] std::io::Error),
75 #[error("XML writer emitted invalid UTF-8: {0}")]
77 InvalidUtf8(#[from] std::string::FromUtf8Error),
78 #[error("generated XML template is invalid: {0}")]
80 GeneratedXml(#[from] crate::xml::dom::ParseError),
81 #[error("generated SignedInfo canonicalization failed: {0}")]
83 Canonicalization(#[from] crate::c14n::C14nError),
84 #[error("generated XML template is missing SignedInfo")]
86 MissingGeneratedSignedInfo,
87 #[error("generated XML template validation failed: {0}")]
89 GeneratedMutation(#[source] super::mutation::XmlMutationError),
90}
91
92#[derive(Debug, Clone)]
94pub struct ReferenceBuilder {
95 uri: String,
96 id: Option<String>,
97 ref_type: Option<String>,
98 transforms: Vec<Transform>,
99 digest_method: DigestAlgorithm,
100}
101
102impl ReferenceBuilder {
103 #[must_use]
105 pub fn new(digest_method: DigestAlgorithm) -> Self {
106 Self {
107 uri: String::new(),
108 id: None,
109 ref_type: None,
110 transforms: Vec::new(),
111 digest_method,
112 }
113 }
114
115 #[must_use]
120 pub fn uri(mut self, uri: impl Into<String>) -> Self {
121 self.uri = uri.into();
122 self
123 }
124
125 #[must_use]
127 pub fn id(mut self, id: impl Into<String>) -> Self {
128 self.id = Some(id.into());
129 self
130 }
131
132 #[must_use]
134 pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
135 self.ref_type = Some(ref_type.into());
136 self
137 }
138
139 #[must_use]
141 pub fn transform(mut self, transform: Transform) -> Self {
142 self.transforms.push(transform);
143 self
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct SignatureBuilder {
150 c14n_method: C14nAlgorithm,
151 sign_method: SignatureAlgorithm,
152 signature_context: SignatureContext,
153 ns_prefix: Option<String>,
154 signature_id: Option<String>,
155 references: Vec<ReferenceBuilder>,
156 include_key_info: bool,
157}
158
159impl SignatureBuilder {
160 #[must_use]
162 pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
163 Self {
164 c14n_method,
165 sign_method,
166 signature_context: SignatureContext::default(),
167 ns_prefix: None,
168 signature_id: None,
169 references: Vec::new(),
170 include_key_info: false,
171 }
172 }
173
174 #[must_use]
176 pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
177 self.ns_prefix = Some(prefix.into());
178 self
179 }
180
181 #[must_use]
183 pub fn signature_id(mut self, id: impl Into<String>) -> Self {
184 self.signature_id = Some(id.into());
185 self
186 }
187
188 #[must_use]
191 pub fn signature_context(mut self, context: SignatureContext) -> Self {
192 self.signature_context = context;
193 self
194 }
195
196 #[must_use]
198 pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
199 self.references.push(reference);
200 self
201 }
202
203 #[must_use]
205 pub fn key_info(mut self, include: bool) -> Self {
206 self.include_key_info = include;
207 self
208 }
209
210 pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
212 self.build_template_with_policy(&SigningPolicy::default())
213 }
214
215 pub fn build_template_with_policy(
220 &self,
221 policy: &SigningPolicy,
222 ) -> Result<String, SignatureBuilderError> {
223 let budget = TransformExecutionBudget::from_resources(&policy.resources);
224 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(&policy.resources);
225 self.build_template_with_policy_and_signature_output_len(
226 policy,
227 None,
228 &budget,
229 &mut xpath_parse_budget,
230 )
231 }
232
233 pub(super) fn build_template_with_policy_for_signature_output(
234 &self,
235 policy: &SigningPolicy,
236 signature_output_len: usize,
237 budget: &TransformExecutionBudget,
238 xpath_parse_budget: &mut XPathSignatureParseBudget,
239 ) -> Result<String, SignatureBuilderError> {
240 self.build_template_with_policy_and_signature_output_len(
241 policy,
242 Some(signature_output_len),
243 budget,
244 xpath_parse_budget,
245 )
246 }
247
248 fn build_template_with_policy_and_signature_output_len(
249 &self,
250 policy: &SigningPolicy,
251 signature_output_len: Option<usize>,
252 budget: &TransformExecutionBudget,
253 xpath_parse_budget: &mut XPathSignatureParseBudget,
254 ) -> Result<String, SignatureBuilderError> {
255 policy.validate()?;
256 self.validate(policy, xpath_parse_budget)?;
257
258 let prefix = self.ns_prefix.as_deref();
259 let mut writer = Writer::new(Vec::new());
260 let signature_name = qualified_name(prefix, "Signature");
261 let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
262 let mut attributes = vec![(namespace_attr.as_str(), XMLDSIG_NS)];
263 if let Some(id) = &self.signature_id {
264 attributes.push(("Id", id.as_str()));
265 }
266 writer.start(&signature_name, attributes)?;
267
268 write_start(&mut writer, prefix, "SignedInfo")?;
269 write_algorithm(
270 &mut writer,
271 prefix,
272 "CanonicalizationMethod",
273 self.c14n_method.uri(),
274 )?;
275 if let SignatureAlgorithm::RsaPss(parameters) = self.sign_method {
276 let method_name = qualified_name(prefix, "SignatureMethod");
277 writer.start(&method_name, [("Algorithm", self.sign_method.uri())])?;
278 writer.start("RSAPSSParams", [("xmlns", super::parse::RSA_PSS_NS)])?;
281 writer.empty(
282 "DigestMethod",
283 [
284 ("xmlns", XMLDSIG_NS),
285 ("Algorithm", parameters.digest.uri()),
286 ],
287 )?;
288 writer.start(
289 "MaskGenerationFunction",
290 [("Algorithm", "http://www.w3.org/2007/05/xmldsig-more#MGF1")],
291 )?;
292 writer.empty(
293 "DigestMethod",
294 [
295 ("xmlns", XMLDSIG_NS),
296 ("Algorithm", parameters.mgf_digest.uri()),
297 ],
298 )?;
299 writer.end("MaskGenerationFunction")?;
300 writer.start("SaltLength", std::iter::empty::<(&str, &str)>())?;
301 writer.text(¶meters.salt_len.to_string())?;
302 writer.end("SaltLength")?;
303 writer.end("RSAPSSParams")?;
304 writer.end(&method_name)?;
305 } else if self.signature_context.as_bytes().is_empty() {
306 write_algorithm(
307 &mut writer,
308 prefix,
309 "SignatureMethod",
310 self.sign_method.uri(),
311 )?;
312 } else {
313 let method_name = qualified_name(prefix, "SignatureMethod");
314 writer.start(&method_name, [("Algorithm", self.sign_method.uri())])?;
315 let context_name = self.sign_method.context_element().ok_or(
320 SignatureBuilderError::SignatureContextNotApplicable(self.sign_method.uri()),
321 )?;
322 writer.start(
323 context_name,
324 [("xmlns", super::parse::EXPERIMENTAL_SIGNATURE_NS)],
325 )?;
326 let mut encoded = [0_u8; 340];
327 let length = base64::engine::general_purpose::STANDARD
328 .encode_slice(self.signature_context.as_bytes(), &mut encoded)
329 .map_err(std::io::Error::other)?;
330 writer.text(std::str::from_utf8(&encoded[..length]).map_err(std::io::Error::other)?)?;
331 writer.end(context_name)?;
332 writer.end(&method_name)?;
333 }
334 for reference in &self.references {
335 write_reference(&mut writer, prefix, reference)?;
336 }
337 write_end(&mut writer, prefix, "SignedInfo")?;
338 write_empty(&mut writer, prefix, "SignatureValue")?;
339 if self.include_key_info {
340 write_empty(&mut writer, prefix, "KeyInfo")?;
341 }
342 writer.end(&signature_name)?;
343
344 let template = String::from_utf8(writer.into_inner())?;
345 policy.resources.validate_xml_document_len(template.len())?;
348 let digest_placeholders = self.references.iter().map(|reference| {
349 base64::engine::general_purpose::STANDARD.encode(vec![
350 0_u8;
351 reference
352 .digest_method
353 .output_len()
354 ])
355 });
356 let digest_validation_template = fill_signed_info_digest_values_with_budget(
357 &template,
358 digest_placeholders,
359 Some(policy),
360 Some(budget.xml_parse_work()),
361 )
362 .map_err(map_generated_mutation_error)?;
363 let validation_template = if let Some(signature_output_len) = signature_output_len {
364 let encoded_signature_len = padded_base64_len_for_xml(signature_output_len, policy)
365 .map_err(map_generated_mutation_error)?;
366 let projected_document_len = projected_signature_value_output_len_at_index_with_budget(
367 &digest_validation_template,
368 encoded_signature_len,
369 0,
370 Some(policy),
371 Some(budget.xml_parse_work()),
372 )
373 .map_err(map_generated_mutation_error)?;
374 policy
375 .resources
376 .validate_xml_document_len(projected_document_len)?;
377 let signature_placeholder =
378 zero_base64_placeholder(signature_output_len, encoded_signature_len);
379 fill_signature_value_with_budget(
380 &digest_validation_template,
381 &signature_placeholder,
382 Some(policy),
383 Some(budget.xml_parse_work()),
384 )
385 .map_err(map_generated_mutation_error)?
386 } else {
387 digest_validation_template
388 };
389 policy
390 .resources
391 .validate_xml_document_len(validation_template.len())?;
392 let settings =
393 crate::document::DocumentParseSettings::from_policy(&policy.xml, &policy.resources);
394 let document = super::mutation::parse_with_options_and_budget(
395 &validation_template,
396 settings,
397 Some(budget.xml_parse_work()),
398 )
399 .map_err(|error| match error.into_policy_violation(settings) {
400 Ok(error) => SignatureBuilderError::Policy(error),
401 Err(crate::document::XmlDocumentError::Parse(error)) => {
402 SignatureBuilderError::GeneratedXml(error)
403 }
404 Err(error) => SignatureBuilderError::GeneratedMutation(
405 super::mutation::XmlMutationError::Document(error),
406 ),
407 })?;
408 let signed_info = document
409 .root_element()
410 .children()
411 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
412 .ok_or(SignatureBuilderError::MissingGeneratedSignedInfo)?;
413 let signed_info_subtree: HashSet<_> =
414 signed_info.descendants().map(|node| node.id()).collect();
415 let mut canonical_signed_info = Vec::new();
416 canonicalize_bounded_with_xml_base_budget(
417 &document,
418 Some(&|node| signed_info_subtree.contains(&node.id())),
419 &self.c14n_method,
420 budget.remaining_c14n_output(),
421 budget.xml_base_resolution(),
422 &mut canonical_signed_info,
423 )
424 .map_err(|error| {
425 map_c14n_resource_policy_violation(
426 &error,
427 crate::policy::resource_name::CANONICALIZED_BYTES,
428 budget.c14n_output_limit(),
429 )
430 .map_or_else(
431 || SignatureBuilderError::Canonicalization(error),
432 SignatureBuilderError::Policy,
433 )
434 })?;
435 budget.charge_c14n_output_policy(canonical_signed_info.len())?;
436 Ok(template)
437 }
438
439 pub(super) fn signature_method(&self) -> SignatureAlgorithm {
440 self.sign_method
441 }
442
443 fn validate(
444 &self,
445 policy: &SigningPolicy,
446 xpath_signature_budget: &mut XPathSignatureParseBudget,
447 ) -> Result<(), SignatureBuilderError> {
448 if let Some(parameters) = self.sign_method.rsa_pss_parameters()
449 && parameters.salt_len > i32::MAX as usize
450 {
451 return Err(SignatureBuilderError::InvalidRsaPssParameters);
452 }
453 if !self.signature_context.as_bytes().is_empty()
454 && self.sign_method.context_element().is_none()
455 {
456 return Err(SignatureBuilderError::SignatureContextNotApplicable(
457 self.sign_method.uri(),
458 ));
459 }
460 if let Some(prefix) = &self.ns_prefix
461 && !is_namespace_prefix(prefix)
462 {
463 return Err(SignatureBuilderError::InvalidNamespacePrefix(
464 prefix.clone(),
465 ));
466 }
467 if let Some(id) = &self.signature_id
468 && !is_xml_ncname(id)
469 {
470 return Err(SignatureBuilderError::InvalidId {
471 element: "Signature",
472 value: id.clone(),
473 });
474 }
475 if self.references.is_empty() {
476 return Err(SignatureBuilderError::MissingReference);
477 }
478 if self.references.len() > policy.resources.max_references {
479 return Err(PolicyViolation::ResourceLimit {
480 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
481 maximum: policy.resources.max_references,
482 actual: self.references.len(),
483 }
484 .into());
485 }
486 for reference in &self.references {
487 validate_signing_reference_uri(&reference.uri, policy)?;
488 let generated_transforms = reference_transforms_for_generation(reference);
489 if generated_transforms.len() > policy.resources.max_transforms_per_reference {
490 return Err(PolicyViolation::ResourceLimit {
491 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
492 maximum: policy.resources.max_transforms_per_reference,
493 actual: generated_transforms.len(),
494 }
495 .into());
496 }
497 let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
498 validate_signing_transform_policy(
499 initial_binary,
500 &generated_transforms,
501 policy.transforms.allowed_algorithms.as_ref(),
502 )?;
503 for transform in &reference.transforms {
504 match transform {
505 Transform::XpathExcludeAllSignatures => {
506 validate_xpath_source(
507 ENVELOPED_SIGNATURE_XPATH_EXPR,
508 xpath_signature_budget,
509 )?;
510 }
511 Transform::XPath(xpath) => {
512 validate_xpath_source(xpath.expression(), xpath_signature_budget)?;
513 }
514 Transform::XPathFilter2(filters) => {
515 if filters.is_empty() {
516 return Err(SignatureBuilderError::InvalidXPath(
517 "XPath Filter 2.0 requires at least one expression".into(),
518 ));
519 }
520 if filters.len() > policy.resources.max_xpath_filters {
521 return Err(PolicyViolation::ResourceLimit {
522 resource: crate::policy::resource_name::XPATH_FILTERS,
523 maximum: policy.resources.max_xpath_filters,
524 actual: filters.len(),
525 }
526 .into());
527 }
528 for filter in filters {
529 validate_xpath_source(
530 filter.xpath().expression(),
531 xpath_signature_budget,
532 )?;
533 }
534 }
535 _ => {}
536 }
537 }
538 validate_xpath_namespace_budget_with_resources(
539 &reference.transforms,
540 self.ns_prefix.as_deref().map(|prefix| (prefix, XMLDSIG_NS)),
541 &policy.resources,
542 )
543 .map_err(map_transform_validation_error)?;
544 }
545 for (prefix, uri, shares_signature_namespace) in
546 self.references.iter().flat_map(|reference| {
547 reference
548 .transforms
549 .iter()
550 .flat_map(|transform| match transform {
551 Transform::XPath(xpath) => xpath
552 .namespaces()
553 .iter()
554 .map(|(prefix, uri)| (prefix, uri, true))
555 .collect::<Vec<_>>(),
556 Transform::XPathFilter2(filters) => filters
557 .iter()
558 .flat_map(|filter| {
559 filter
560 .xpath()
561 .namespaces()
562 .iter()
563 .map(|(prefix, uri)| (prefix, uri, false))
564 })
565 .collect(),
566 _ => Vec::new(),
567 })
568 })
569 {
570 if uri.is_empty() || uri == XMLNS_NS || !uri.chars().all(is_xml_1_0_character) {
573 return Err(SignatureBuilderError::InvalidNamespaceUri(uri.clone()));
574 }
575 if prefix == "xmlns"
576 || (prefix == "xml") != (uri == XML_NS)
577 || (prefix != "xml" && !is_namespace_prefix(prefix))
578 {
579 return Err(SignatureBuilderError::InvalidNamespacePrefix(
580 prefix.clone(),
581 ));
582 }
583 if shares_signature_namespace
586 && self.ns_prefix.as_ref() == Some(prefix)
587 && uri != XMLDSIG_NS
588 {
589 return Err(SignatureBuilderError::NamespacePrefixConflict(
590 prefix.clone(),
591 ));
592 }
593 }
594 policy.check_signature_algorithm(self.sign_method)?;
595 if policy
596 .transforms
597 .allowed_algorithms
598 .as_ref()
599 .is_some_and(|allowed| !allowed.contains(self.c14n_method.uri()))
600 {
601 return Err(PolicyViolation::Algorithm {
602 operation: "signing transform",
603 algorithm: self.c14n_method.uri().to_owned(),
604 }
605 .into());
606 }
607 for reference in &self.references {
608 if let Some(id) = &reference.id
609 && !is_xml_ncname(id)
610 {
611 return Err(SignatureBuilderError::InvalidId {
612 element: "Reference",
613 value: id.clone(),
614 });
615 }
616 policy.check_digest_algorithm(reference.digest_method)?;
617 }
618 Ok(())
619 }
620}
621
622fn validate_xpath_source(
623 source: &str,
624 budget: &mut XPathSignatureParseBudget,
625) -> Result<(), SignatureBuilderError> {
626 if let Some(character) = source
627 .chars()
628 .find(|character| !is_xml_1_0_character(*character))
629 {
630 return Err(SignatureBuilderError::InvalidXPath(format!(
631 "XPath expression contains a character forbidden by XML 1.0: {character:?}"
632 )));
633 }
634 budget
635 .validate_expression(source)
636 .map_err(map_transform_validation_error)
637}
638
639fn map_transform_validation_error(error: super::TransformError) -> SignatureBuilderError {
640 match error {
641 super::TransformError::Policy(error) => SignatureBuilderError::Policy(error),
642 error => SignatureBuilderError::InvalidXPath(error.to_string()),
643 }
644}
645
646fn map_generated_mutation_error(error: super::mutation::XmlMutationError) -> SignatureBuilderError {
647 match error {
648 super::mutation::XmlMutationError::Policy(violation) => {
649 SignatureBuilderError::Policy(violation)
650 }
651 other => SignatureBuilderError::GeneratedMutation(other),
652 }
653}
654
655fn write_reference<W: Write>(
656 writer: &mut Writer<W>,
657 prefix: Option<&str>,
658 reference: &ReferenceBuilder,
659) -> Result<(), std::io::Error> {
660 let name = qualified_name(prefix, "Reference");
661 let mut attributes = Vec::new();
662 if let Some(id) = &reference.id {
663 attributes.push(("Id", id.as_str()));
664 }
665 if let Some(ref_type) = &reference.ref_type {
666 attributes.push(("Type", ref_type.as_str()));
667 }
668 attributes.push(("URI", reference.uri.as_str()));
669 writer.start(&name, attributes)?;
670
671 let generated_transforms = reference_transforms_for_generation(reference);
672 if !generated_transforms.is_empty() {
673 write_start(writer, prefix, "Transforms")?;
674 for transform in &generated_transforms {
675 write_transform(writer, prefix, transform)?;
676 }
677 write_end(writer, prefix, "Transforms")?;
678 }
679 write_algorithm(
680 writer,
681 prefix,
682 "DigestMethod",
683 reference.digest_method.uri(),
684 )?;
685 write_empty(writer, prefix, "DigestValue")?;
686 writer.end(&name)?;
687 Ok(())
688}
689
690fn reference_transforms_for_generation(reference: &ReferenceBuilder) -> Vec<Transform> {
691 let mut transforms = reference.transforms.clone();
692 let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
693 if !transform_chain_produces_binary(initial_binary, &transforms) {
694 transforms.push(Transform::C14n(C14nAlgorithm::new(
695 C14nMode::Inclusive1_1,
696 false,
697 )));
698 }
699 transforms
700}
701
702fn write_transform<W: Write>(
703 writer: &mut Writer<W>,
704 prefix: Option<&str>,
705 transform: &Transform,
706) -> Result<(), std::io::Error> {
707 match transform {
708 Transform::Enveloped => {
709 write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
710 }
711 Transform::XpathExcludeAllSignatures => {
712 let name = qualified_name(prefix, "Transform");
713 writer.start(&name, [("Algorithm", XPATH_TRANSFORM_URI)])?;
714 let xpath_name = qualified_name(prefix, "XPath");
715 let namespace = format!("xmlns:{ENVELOPED_SIGNATURE_XPATH_PREFIX}");
716 writer.start(&xpath_name, [(namespace.as_str(), XMLDSIG_NS)])?;
717 writer.text(ENVELOPED_SIGNATURE_XPATH_EXPR)?;
718 writer.end(&xpath_name)?;
719 writer.end(&name)?;
720 Ok(())
721 }
722 Transform::XPath(xpath) => {
723 let transform_name = qualified_name(prefix, "Transform");
724 writer.start(&transform_name, [("Algorithm", XPATH_TRANSFORM_URI)])?;
725 write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
726 writer.end(&transform_name)?;
727 Ok(())
728 }
729 Transform::XPathFilter2(filters) => {
730 let transform_name = qualified_name(prefix, "Transform");
731 writer.start(
732 &transform_name,
733 [("Algorithm", XPATH_FILTER2_TRANSFORM_URI)],
734 )?;
735 for filter in filters {
736 write_xpath_expression(
737 writer,
738 None,
739 "XPath",
740 Some(filter.operation().as_str()),
741 filter.xpath(),
742 )?;
743 }
744 writer.end(&transform_name)?;
745 Ok(())
746 }
747 Transform::Base64Decode => {
748 write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
749 }
750 Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
751 write_algorithm(writer, prefix, "Transform", algorithm.uri())
752 }
753 Transform::C14n(algorithm) => {
754 let name = qualified_name(prefix, "Transform");
755 writer.start(&name, [("Algorithm", algorithm.uri())])?;
756
757 if algorithm.mode() == C14nMode::Exclusive1_0 {
758 let mut prefixes: Vec<&str> = algorithm
759 .inclusive_prefixes()
760 .iter()
761 .map(String::as_str)
762 .collect();
763 prefixes.sort_unstable();
764 let prefix_list = prefixes
765 .into_iter()
766 .map(|p| if p.is_empty() { "#default" } else { p })
767 .collect::<Vec<_>>()
768 .join(" ");
769 writer.empty(
770 "ec:InclusiveNamespaces",
771 [
772 ("xmlns:ec", EXCLUSIVE_C14N_NS),
773 ("PrefixList", prefix_list.as_str()),
774 ],
775 )?;
776 }
777 writer.end(&name)?;
778 Ok(())
779 }
780 }
781}
782
783fn write_xpath_expression<W: Write>(
784 writer: &mut Writer<W>,
785 prefix: Option<&str>,
786 local_name: &str,
787 filter: Option<&str>,
788 xpath: &XPathExpression,
789) -> Result<(), std::io::Error> {
790 let name = qualified_name(prefix, local_name);
791 let namespace_attributes = xpath
792 .namespaces()
793 .iter()
794 .filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
795 .map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
796 .collect::<Vec<_>>();
797 let mut attributes = Vec::new();
798 if prefix.is_none() && filter.is_some() {
799 attributes.push(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
800 }
801 if let Some(filter) = filter {
802 attributes.push(("Filter", filter));
803 }
804 for (attribute, uri) in &namespace_attributes {
805 attributes.push((attribute.as_str(), uri.as_str()));
806 }
807 writer.start(&name, attributes)?;
808 writer.text(xpath.expression())?;
809 writer.end(&name)?;
810 Ok(())
811}
812
813fn write_algorithm<W: Write>(
814 writer: &mut Writer<W>,
815 prefix: Option<&str>,
816 local_name: &str,
817 algorithm: &str,
818) -> Result<(), std::io::Error> {
819 let name = qualified_name(prefix, local_name);
820 writer.empty(&name, [("Algorithm", algorithm)])?;
821 Ok(())
822}
823
824fn write_start<W: Write>(
825 writer: &mut Writer<W>,
826 prefix: Option<&str>,
827 local_name: &str,
828) -> Result<(), std::io::Error> {
829 writer.start(&qualified_name(prefix, local_name), [])?;
830 Ok(())
831}
832
833fn write_end<W: Write>(
834 writer: &mut Writer<W>,
835 prefix: Option<&str>,
836 local_name: &str,
837) -> Result<(), std::io::Error> {
838 writer.end(&qualified_name(prefix, local_name))?;
839 Ok(())
840}
841
842fn write_empty<W: Write>(
843 writer: &mut Writer<W>,
844 prefix: Option<&str>,
845 local_name: &str,
846) -> Result<(), std::io::Error> {
847 writer.empty(&qualified_name(prefix, local_name), [])?;
848 Ok(())
849}
850
851fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
852 prefix.map_or_else(
853 || local_name.to_owned(),
854 |prefix| format!("{prefix}:{local_name}"),
855 )
856}
857
858fn is_namespace_prefix(value: &str) -> bool {
859 !matches!(value, "xml" | "xmlns") && is_xml_ncname(value)
861}