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, 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#[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("XML serialization error: {0}")]
68 Serialization(#[from] std::io::Error),
69 #[error("XML writer emitted invalid UTF-8: {0}")]
71 InvalidUtf8(#[from] std::string::FromUtf8Error),
72 #[error("generated XML template is invalid: {0}")]
74 GeneratedXml(#[from] crate::xml::dom::ParseError),
75 #[error("generated SignedInfo canonicalization failed: {0}")]
77 Canonicalization(#[from] crate::c14n::C14nError),
78 #[error("generated XML template is missing SignedInfo")]
80 MissingGeneratedSignedInfo,
81 #[error("generated XML template validation failed: {0}")]
83 GeneratedMutation(#[source] super::mutation::XmlMutationError),
84}
85
86#[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 #[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 #[must_use]
114 pub fn uri(mut self, uri: impl Into<String>) -> Self {
115 self.uri = uri.into();
116 self
117 }
118
119 #[must_use]
121 pub fn id(mut self, id: impl Into<String>) -> Self {
122 self.id = Some(id.into());
123 self
124 }
125
126 #[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 #[must_use]
135 pub fn transform(mut self, transform: Transform) -> Self {
136 self.transforms.push(transform);
137 self
138 }
139}
140
141#[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 #[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 #[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 #[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 #[must_use]
182 pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
183 self.references.push(reference);
184 self
185 }
186
187 #[must_use]
189 pub fn key_info(mut self, include: bool) -> Self {
190 self.include_key_info = include;
191 self
192 }
193
194 pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
196 self.build_template_with_policy(&SigningPolicy::default())
197 }
198
199 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 namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
246 let mut attributes = vec![(namespace_attr.as_str(), XMLDSIG_NS)];
247 if let Some(id) = &self.signature_id {
248 attributes.push(("Id", id.as_str()));
249 }
250 writer.start(&signature_name, attributes)?;
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.end(&signature_name)?;
274
275 let template = String::from_utf8(writer.into_inner())?;
276 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 = fill_signed_info_digest_values_with_budget(
288 &template,
289 digest_placeholders,
290 Some(policy),
291 Some(budget.xml_parse_work()),
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 = projected_signature_value_output_len_at_index_with_budget(
298 &digest_validation_template,
299 encoded_signature_len,
300 0,
301 Some(policy),
302 Some(budget.xml_parse_work()),
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 fill_signature_value_with_budget(
311 &digest_validation_template,
312 &signature_placeholder,
313 Some(policy),
314 Some(budget.xml_parse_work()),
315 )
316 .map_err(map_generated_mutation_error)?
317 } else {
318 digest_validation_template
319 };
320 policy
321 .resources
322 .validate_xml_document_len(validation_template.len())?;
323 let settings =
324 crate::document::DocumentParseSettings::from_policy(&policy.xml, &policy.resources);
325 let document = super::mutation::parse_with_options_and_budget(
326 &validation_template,
327 settings,
328 Some(budget.xml_parse_work()),
329 )
330 .map_err(|error| match error.into_policy_violation(settings) {
331 Ok(error) => SignatureBuilderError::Policy(error),
332 Err(crate::document::XmlDocumentError::Parse(error)) => {
333 SignatureBuilderError::GeneratedXml(error)
334 }
335 Err(error) => SignatureBuilderError::GeneratedMutation(
336 super::mutation::XmlMutationError::Document(error),
337 ),
338 })?;
339 let signed_info = document
340 .root_element()
341 .children()
342 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
343 .ok_or(SignatureBuilderError::MissingGeneratedSignedInfo)?;
344 let signed_info_subtree: HashSet<_> =
345 signed_info.descendants().map(|node| node.id()).collect();
346 let mut canonical_signed_info = Vec::new();
347 canonicalize_bounded_with_xml_base_budget(
348 &document,
349 Some(&|node| signed_info_subtree.contains(&node.id())),
350 &self.c14n_method,
351 budget.remaining_c14n_output(),
352 budget.xml_base_resolution(),
353 &mut canonical_signed_info,
354 )
355 .map_err(|error| {
356 map_c14n_resource_policy_violation(
357 &error,
358 crate::policy::resource_name::CANONICALIZED_BYTES,
359 budget.c14n_output_limit(),
360 )
361 .map_or_else(
362 || SignatureBuilderError::Canonicalization(error),
363 SignatureBuilderError::Policy,
364 )
365 })?;
366 budget.charge_c14n_output_policy(canonical_signed_info.len())?;
367 Ok(template)
368 }
369
370 pub(super) fn signature_method(&self) -> SignatureAlgorithm {
371 self.sign_method
372 }
373
374 fn validate(
375 &self,
376 policy: &SigningPolicy,
377 xpath_signature_budget: &mut XPathSignatureParseBudget,
378 ) -> Result<(), SignatureBuilderError> {
379 if let Some(prefix) = &self.ns_prefix
380 && !is_namespace_prefix(prefix)
381 {
382 return Err(SignatureBuilderError::InvalidNamespacePrefix(
383 prefix.clone(),
384 ));
385 }
386 if let Some(id) = &self.signature_id
387 && !is_xml_ncname(id)
388 {
389 return Err(SignatureBuilderError::InvalidId {
390 element: "Signature",
391 value: id.clone(),
392 });
393 }
394 if self.references.is_empty() {
395 return Err(SignatureBuilderError::MissingReference);
396 }
397 if self.references.len() > policy.resources.max_references {
398 return Err(PolicyViolation::ResourceLimit {
399 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
400 maximum: policy.resources.max_references,
401 actual: self.references.len(),
402 }
403 .into());
404 }
405 for reference in &self.references {
406 validate_signing_reference_uri(&reference.uri, policy)?;
407 let generated_transforms = reference_transforms_for_generation(reference);
408 if generated_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: generated_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 &generated_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 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 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 policy.check_signature_algorithm(self.sign_method)?;
514 if policy
515 .transforms
516 .allowed_algorithms
517 .as_ref()
518 .is_some_and(|allowed| !allowed.contains(self.c14n_method.uri()))
519 {
520 return Err(PolicyViolation::Algorithm {
521 operation: "signing transform",
522 algorithm: self.c14n_method.uri().to_owned(),
523 }
524 .into());
525 }
526 for reference in &self.references {
527 if let Some(id) = &reference.id
528 && !is_xml_ncname(id)
529 {
530 return Err(SignatureBuilderError::InvalidId {
531 element: "Reference",
532 value: id.clone(),
533 });
534 }
535 policy.check_digest_algorithm(reference.digest_method)?;
536 }
537 Ok(())
538 }
539}
540
541fn validate_xpath_source(
542 source: &str,
543 budget: &mut XPathSignatureParseBudget,
544) -> Result<(), SignatureBuilderError> {
545 if let Some(character) = source
546 .chars()
547 .find(|character| !is_xml_1_0_character(*character))
548 {
549 return Err(SignatureBuilderError::InvalidXPath(format!(
550 "XPath expression contains a character forbidden by XML 1.0: {character:?}"
551 )));
552 }
553 budget
554 .validate_expression(source)
555 .map_err(map_transform_validation_error)
556}
557
558fn map_transform_validation_error(error: super::TransformError) -> SignatureBuilderError {
559 match error {
560 super::TransformError::Policy(error) => SignatureBuilderError::Policy(error),
561 error => SignatureBuilderError::InvalidXPath(error.to_string()),
562 }
563}
564
565fn map_generated_mutation_error(error: super::mutation::XmlMutationError) -> SignatureBuilderError {
566 match error {
567 super::mutation::XmlMutationError::Policy(violation) => {
568 SignatureBuilderError::Policy(violation)
569 }
570 other => SignatureBuilderError::GeneratedMutation(other),
571 }
572}
573
574fn write_reference<W: Write>(
575 writer: &mut Writer<W>,
576 prefix: Option<&str>,
577 reference: &ReferenceBuilder,
578) -> Result<(), std::io::Error> {
579 let name = qualified_name(prefix, "Reference");
580 let mut attributes = Vec::new();
581 if let Some(id) = &reference.id {
582 attributes.push(("Id", id.as_str()));
583 }
584 if let Some(ref_type) = &reference.ref_type {
585 attributes.push(("Type", ref_type.as_str()));
586 }
587 attributes.push(("URI", reference.uri.as_str()));
588 writer.start(&name, attributes)?;
589
590 let generated_transforms = reference_transforms_for_generation(reference);
591 if !generated_transforms.is_empty() {
592 write_start(writer, prefix, "Transforms")?;
593 for transform in &generated_transforms {
594 write_transform(writer, prefix, transform)?;
595 }
596 write_end(writer, prefix, "Transforms")?;
597 }
598 write_algorithm(
599 writer,
600 prefix,
601 "DigestMethod",
602 reference.digest_method.uri(),
603 )?;
604 write_empty(writer, prefix, "DigestValue")?;
605 writer.end(&name)?;
606 Ok(())
607}
608
609fn reference_transforms_for_generation(reference: &ReferenceBuilder) -> Vec<Transform> {
610 let mut transforms = reference.transforms.clone();
611 let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
612 if !transform_chain_produces_binary(initial_binary, &transforms) {
613 transforms.push(Transform::C14n(C14nAlgorithm::new(
614 C14nMode::Inclusive1_1,
615 false,
616 )));
617 }
618 transforms
619}
620
621fn write_transform<W: Write>(
622 writer: &mut Writer<W>,
623 prefix: Option<&str>,
624 transform: &Transform,
625) -> Result<(), std::io::Error> {
626 match transform {
627 Transform::Enveloped => {
628 write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
629 }
630 Transform::XpathExcludeAllSignatures => {
631 let name = qualified_name(prefix, "Transform");
632 writer.start(&name, [("Algorithm", XPATH_TRANSFORM_URI)])?;
633 let xpath_name = qualified_name(prefix, "XPath");
634 let namespace = format!("xmlns:{ENVELOPED_SIGNATURE_XPATH_PREFIX}");
635 writer.start(&xpath_name, [(namespace.as_str(), XMLDSIG_NS)])?;
636 writer.text(ENVELOPED_SIGNATURE_XPATH_EXPR)?;
637 writer.end(&xpath_name)?;
638 writer.end(&name)?;
639 Ok(())
640 }
641 Transform::XPath(xpath) => {
642 let transform_name = qualified_name(prefix, "Transform");
643 writer.start(&transform_name, [("Algorithm", XPATH_TRANSFORM_URI)])?;
644 write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
645 writer.end(&transform_name)?;
646 Ok(())
647 }
648 Transform::XPathFilter2(filters) => {
649 let transform_name = qualified_name(prefix, "Transform");
650 writer.start(
651 &transform_name,
652 [("Algorithm", XPATH_FILTER2_TRANSFORM_URI)],
653 )?;
654 for filter in filters {
655 write_xpath_expression(
656 writer,
657 None,
658 "XPath",
659 Some(filter.operation().as_str()),
660 filter.xpath(),
661 )?;
662 }
663 writer.end(&transform_name)?;
664 Ok(())
665 }
666 Transform::Base64Decode => {
667 write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
668 }
669 Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
670 write_algorithm(writer, prefix, "Transform", algorithm.uri())
671 }
672 Transform::C14n(algorithm) => {
673 let name = qualified_name(prefix, "Transform");
674 writer.start(&name, [("Algorithm", algorithm.uri())])?;
675
676 if algorithm.mode() == C14nMode::Exclusive1_0 {
677 let mut prefixes: Vec<&str> = algorithm
678 .inclusive_prefixes()
679 .iter()
680 .map(String::as_str)
681 .collect();
682 prefixes.sort_unstable();
683 let prefix_list = prefixes
684 .into_iter()
685 .map(|p| if p.is_empty() { "#default" } else { p })
686 .collect::<Vec<_>>()
687 .join(" ");
688 writer.empty(
689 "ec:InclusiveNamespaces",
690 [
691 ("xmlns:ec", EXCLUSIVE_C14N_NS),
692 ("PrefixList", prefix_list.as_str()),
693 ],
694 )?;
695 }
696 writer.end(&name)?;
697 Ok(())
698 }
699 }
700}
701
702fn write_xpath_expression<W: Write>(
703 writer: &mut Writer<W>,
704 prefix: Option<&str>,
705 local_name: &str,
706 filter: Option<&str>,
707 xpath: &XPathExpression,
708) -> Result<(), std::io::Error> {
709 let name = qualified_name(prefix, local_name);
710 let namespace_attributes = xpath
711 .namespaces()
712 .iter()
713 .filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
714 .map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
715 .collect::<Vec<_>>();
716 let mut attributes = Vec::new();
717 if prefix.is_none() && filter.is_some() {
718 attributes.push(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
719 }
720 if let Some(filter) = filter {
721 attributes.push(("Filter", filter));
722 }
723 for (attribute, uri) in &namespace_attributes {
724 attributes.push((attribute.as_str(), uri.as_str()));
725 }
726 writer.start(&name, attributes)?;
727 writer.text(xpath.expression())?;
728 writer.end(&name)?;
729 Ok(())
730}
731
732fn write_algorithm<W: Write>(
733 writer: &mut Writer<W>,
734 prefix: Option<&str>,
735 local_name: &str,
736 algorithm: &str,
737) -> Result<(), std::io::Error> {
738 let name = qualified_name(prefix, local_name);
739 writer.empty(&name, [("Algorithm", algorithm)])?;
740 Ok(())
741}
742
743fn write_start<W: Write>(
744 writer: &mut Writer<W>,
745 prefix: Option<&str>,
746 local_name: &str,
747) -> Result<(), std::io::Error> {
748 writer.start(&qualified_name(prefix, local_name), [])?;
749 Ok(())
750}
751
752fn write_end<W: Write>(
753 writer: &mut Writer<W>,
754 prefix: Option<&str>,
755 local_name: &str,
756) -> Result<(), std::io::Error> {
757 writer.end(&qualified_name(prefix, local_name))?;
758 Ok(())
759}
760
761fn write_empty<W: Write>(
762 writer: &mut Writer<W>,
763 prefix: Option<&str>,
764 local_name: &str,
765) -> Result<(), std::io::Error> {
766 writer.empty(&qualified_name(prefix, local_name), [])?;
767 Ok(())
768}
769
770fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
771 prefix.map_or_else(
772 || local_name.to_owned(),
773 |prefix| format!("{prefix}:{local_name}"),
774 )
775}
776
777fn is_namespace_prefix(value: &str) -> bool {
778 !matches!(value, "xml" | "xmlns") && is_xml_ncname(value)
780}