Skip to main content

xml_sec/c14n/
mod.rs

1//! XML Canonicalization (C14N).
2//!
3//! Implements:
4//! - [Canonical XML 1.0](https://www.w3.org/TR/xml-c14n/) (inclusive)
5//! - [Canonical XML 1.1](https://www.w3.org/TR/xml-c14n11/) (inclusive; xml:id non-inheritance and xml:base fixup)
6//! - [Exclusive XML Canonicalization 1.0](https://www.w3.org/TR/xml-exc-c14n/) (exclusive)
7//!
8//! # Example
9//!
10//! ```
11//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
12//! use xml_sec::c14n::{C14nAlgorithm, C14nMode, canonicalize_xml};
13//!
14//! let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
15//! let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
16//! let canonical = canonicalize_xml(xml, &algo)?;
17//! assert_eq!(
18//!     String::from_utf8(canonical)?,
19//!     "<root a=\"1\" b=\"2\"><empty></empty></root>"
20//! );
21//! # Ok(())
22//! # }
23//! ```
24
25mod escape;
26pub(crate) mod scratch;
27#[cfg(feature = "xmldsig")]
28pub(crate) use escape::escape_attr;
29mod ns_common;
30pub(crate) mod ns_exclusive;
31pub(crate) mod ns_inclusive;
32pub(crate) mod prefix;
33pub(crate) mod serialize;
34pub(crate) mod xml_base;
35
36use std::collections::HashSet;
37
38use crate::xml::dom::storage::{Native as NativeDom, Storage as DomStorage};
39use crate::xml::dom::{Document, Node, NodeId};
40
41use ns_exclusive::ExclusiveNsRenderer;
42use ns_inclusive::InclusiveNsRenderer;
43#[cfg(feature = "xmldsig")]
44use serialize::serialize_canonical_visible_with_positions_bounded;
45use serialize::{
46    C14nConfig, CanonicalOutputOptions, serialize_canonical_visible_with_position_bounded,
47    serialize_canonical_visible_with_position_with_xml_base_budget,
48};
49
50/// C14N algorithm mode (without the comments flag).
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum C14nMode {
53    /// Inclusive C14N 1.0 — all in-scope namespaces rendered.
54    Inclusive1_0,
55    /// Inclusive C14N 1.1 — like 1.0 with xml:id non-inheritance and xml:base fixup.
56    Inclusive1_1,
57    /// Exclusive C14N 1.0 — only visibly-utilized namespaces rendered.
58    Exclusive1_0,
59}
60
61/// Full C14N algorithm identifier.
62///
63/// Constructed from algorithm URIs found in `<CanonicalizationMethod>` or
64/// `<Transform>` elements.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct C14nAlgorithm {
67    mode: C14nMode,
68    with_comments: bool,
69    /// For Exclusive C14N: prefixes forced via InclusiveNamespaces PrefixList.
70    /// `"#default"` is normalized to `""` (empty string) by `with_prefix_list()`.
71    inclusive_prefixes: HashSet<String>,
72}
73
74impl C14nAlgorithm {
75    /// The canonicalization mode.
76    pub fn mode(&self) -> C14nMode {
77        self.mode
78    }
79
80    /// Whether comment nodes are preserved.
81    pub fn with_comments(&self) -> bool {
82        self.with_comments
83    }
84
85    /// Prefixes forced via InclusiveNamespaces PrefixList (exclusive C14N).
86    pub fn inclusive_prefixes(&self) -> &HashSet<String> {
87        &self.inclusive_prefixes
88    }
89
90    /// Create a new algorithm with the given mode and comments flag.
91    pub fn new(mode: C14nMode, with_comments: bool) -> Self {
92        Self {
93            mode,
94            with_comments,
95            inclusive_prefixes: HashSet::new(),
96        }
97    }
98
99    /// Parse from an algorithm URI. Returns `None` for unrecognized URIs.
100    pub fn from_uri(uri: &str) -> Option<Self> {
101        let (mode, with_comments) = match uri {
102            "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" => (C14nMode::Inclusive1_0, false),
103            "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" => {
104                (C14nMode::Inclusive1_0, true)
105            }
106            "http://www.w3.org/2006/12/xml-c14n11" => (C14nMode::Inclusive1_1, false),
107            "http://www.w3.org/2006/12/xml-c14n11#WithComments" => (C14nMode::Inclusive1_1, true),
108            "http://www.w3.org/2001/10/xml-exc-c14n#" => (C14nMode::Exclusive1_0, false),
109            "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" => (C14nMode::Exclusive1_0, true),
110            _ => return None,
111        };
112        Some(Self {
113            mode,
114            with_comments,
115            inclusive_prefixes: HashSet::new(),
116        })
117    }
118
119    /// Set the InclusiveNamespaces PrefixList (exclusive C14N only).
120    /// `"#default"` is normalized to empty string `""`.
121    ///
122    /// Only meaningful for [`C14nMode::Exclusive1_0`]. For inclusive modes,
123    /// the prefix list is ignored during canonicalization.
124    pub fn with_prefix_list(mut self, prefix_list: &str) -> Self {
125        self.inclusive_prefixes = prefix::inclusive_tokens(prefix_list)
126            .map(|p| {
127                if p == "#default" {
128                    String::new()
129                } else {
130                    p.to_string()
131                }
132            })
133            .collect();
134        self
135    }
136
137    /// Get the algorithm URI for this configuration.
138    pub fn uri(&self) -> &'static str {
139        match (self.mode, self.with_comments) {
140            (C14nMode::Inclusive1_0, false) => "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
141            (C14nMode::Inclusive1_0, true) => {
142                "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
143            }
144            (C14nMode::Inclusive1_1, false) => "http://www.w3.org/2006/12/xml-c14n11",
145            (C14nMode::Inclusive1_1, true) => "http://www.w3.org/2006/12/xml-c14n11#WithComments",
146            (C14nMode::Exclusive1_0, false) => "http://www.w3.org/2001/10/xml-exc-c14n#",
147            (C14nMode::Exclusive1_0, true) => "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
148        }
149    }
150}
151
152/// Error type for C14N operations.
153#[derive(Debug, thiserror::Error)]
154pub enum C14nError {
155    /// Current scoped execution refused work or scratch storage.
156    #[cfg(feature = "xmldsig")]
157    #[error("canonicalization shared execution: {0}")]
158    SharedExecution(#[from] structured_asn1::memory::MemoryError),
159    /// XML parsing error.
160    #[error("XML parse error: {0}")]
161    Parse(String),
162    /// Invalid node reference.
163    #[error("invalid node reference")]
164    InvalidNode,
165    /// Algorithm not yet implemented.
166    #[error("unsupported algorithm: {0}")]
167    UnsupportedAlgorithm(String),
168    /// The inherited `xml:base` chain exceeds the configured component limit.
169    #[error("XML Base resolution exceeds maximum of {max} inherited components: got {actual}")]
170    XmlBaseComponentsTooLarge {
171        /// Configured maximum inherited components.
172        max: usize,
173        /// Number of inherited components encountered.
174        actual: usize,
175    },
176    /// Cumulative `xml:base` resolution work exceeds the configured byte limit.
177    #[error("XML Base resolution exceeds maximum of {max_bytes} bytes: got at least {actual}")]
178    XmlBaseResolutionTooLarge {
179        /// Configured maximum cumulative bytes.
180        max_bytes: usize,
181        /// Minimum cumulative byte count that exceeded the maximum.
182        actual: usize,
183    },
184    /// Canonical output was refused before exceeding its admitted byte bound.
185    #[error("canonical output exceeds maximum of {max_bytes} bytes")]
186    OutputLimitExceeded {
187        /// Maximum bytes admitted for this serialization, excluding any prefix.
188        max_bytes: usize,
189    },
190    /// I/O error.
191    #[error("I/O error: {0}")]
192    Io(#[from] std::io::Error),
193}
194
195#[cfg(any(feature = "xmldsig", test))]
196pub(crate) fn is_output_limit_error(error: &C14nError) -> bool {
197    matches!(error, C14nError::OutputLimitExceeded { .. })
198}
199
200/// Visibility contract for canonicalizing a precise XPath node-set.
201///
202/// XPath can select attributes and namespace nodes independently from their
203/// owner element. The public closure API predates that requirement and treats
204/// both categories as visible whenever their owner is visible; XMLDSig uses
205/// this richer crate-private contract for standards-compliant subsets.
206pub(crate) trait NodeVisibility<D: DomStorage = NativeDom> {
207    fn contains_node(&self, node: Node<'_, '_, D>) -> Result<bool, C14nError>;
208
209    fn contains_attribute(
210        &self,
211        owner: Node<'_, '_, D>,
212        namespace: Option<&str>,
213        local_name: &str,
214    ) -> Result<bool, C14nError>;
215
216    fn contains_namespace(
217        &self,
218        owner: Node<'_, '_, D>,
219        prefix: &str,
220        uri: &str,
221    ) -> Result<bool, C14nError>;
222
223    fn contains_attribute_at(
224        &self,
225        owner: Node<'_, '_, D>,
226        _ordinal: usize,
227        attribute: crate::xml::dom::Attribute<'_, D>,
228    ) -> Result<bool, C14nError> {
229        self.contains_attribute(owner, attribute.namespace(), attribute.name())
230    }
231    fn contains_namespace_at(
232        &self,
233        owner: Node<'_, '_, D>,
234        _ordinal: usize,
235        namespace: crate::xml::dom::Namespace<'_, D>,
236    ) -> Result<bool, C14nError> {
237        self.contains_namespace(owner, namespace.name().unwrap_or(""), namespace.uri())
238    }
239}
240
241struct ClosureVisibility<'a> {
242    predicate: &'a dyn Fn(Node<'_, '_>) -> bool,
243}
244
245impl NodeVisibility for ClosureVisibility<'_> {
246    fn contains_node(&self, node: Node<'_, '_>) -> Result<bool, C14nError> {
247        Ok((self.predicate)(node))
248    }
249
250    fn contains_attribute(
251        &self,
252        owner: Node<'_, '_>,
253        _namespace: Option<&str>,
254        _local_name: &str,
255    ) -> Result<bool, C14nError> {
256        Ok((self.predicate)(owner))
257    }
258
259    fn contains_namespace(
260        &self,
261        owner: Node<'_, '_>,
262        _prefix: &str,
263        _uri: &str,
264    ) -> Result<bool, C14nError> {
265        Ok((self.predicate)(owner))
266    }
267}
268
269/// Canonicalize an XML document or document subset.
270///
271/// - `doc`: parsed roxmltree document (read-only DOM).
272/// - `node_set`: optional predicate controlling which nodes appear in output.
273///   `None` means the entire document.
274/// - `algo`: algorithm parameters (mode, comments, prefix list).
275/// - `output`: byte buffer receiving canonical XML.
276pub fn canonicalize(
277    doc: &Document,
278    node_set: Option<&dyn Fn(Node) -> bool>,
279    algo: &C14nAlgorithm,
280    output: &mut Vec<u8>,
281) -> Result<(), C14nError> {
282    let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
283    canonicalize_with_visibility(
284        doc,
285        visibility
286            .as_ref()
287            .map(|visibility| visibility as &dyn NodeVisibility),
288        algo,
289        output,
290    )
291}
292
293/// Canonicalize through the closure visibility API while enforcing both the
294/// output ceiling and the caller's operation-wide XML Base work budget.
295#[cfg(feature = "xmldsig")]
296pub(crate) fn canonicalize_bounded_with_xml_base_budget(
297    doc: &Document,
298    node_set: Option<&dyn Fn(Node) -> bool>,
299    algo: &C14nAlgorithm,
300    max_output_bytes: usize,
301    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
302    output: &mut Vec<u8>,
303) -> Result<(), C14nError> {
304    let visibility = node_set.map(|predicate| ClosureVisibility { predicate });
305    let initial_offset = output.len();
306    canonicalize_visible_to_writer_bounded(
307        doc,
308        visibility
309            .as_ref()
310            .map(|visibility| visibility as &dyn NodeVisibility),
311        algo,
312        max_output_bytes,
313        xml_base_resolution,
314        output,
315        initial_offset,
316    )?;
317    Ok(())
318}
319
320/// Same bounded serializer for terminal streams and retained collecting output.
321/// The supplied initial offset preserves absolute positions for append callers.
322#[cfg(feature = "xmldsig")]
323pub(crate) fn canonicalize_visible_to_writer_bounded<W: std::io::Write + ?Sized>(
324    doc: &Document,
325    visibility: Option<&dyn NodeVisibility>,
326    algo: &C14nAlgorithm,
327    max_output_bytes: usize,
328    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
329    output: &mut W,
330    initial_offset: usize,
331) -> Result<(), C14nError> {
332    canonicalize_writer_with_scratch(
333        doc,
334        visibility,
335        algo,
336        max_output_bytes,
337        xml_base_resolution,
338        output,
339        initial_offset,
340        &scratch::Native,
341        None,
342        None,
343    )
344    .map(|_| ())
345}
346
347#[cfg(feature = "xmldsig")]
348#[allow(clippy::too_many_arguments)]
349pub(crate) fn canonicalize_visible_to_writer_accounted<
350    W: std::io::Write + ?Sized,
351    E: structured_asn1::memory::Work,
352    D: DomStorage,
353>(
354    doc: &Document<'_, D>,
355    visibility: Option<&dyn NodeVisibility<D>>,
356    algo: &C14nAlgorithm,
357    max_output_bytes: usize,
358    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
359    output: &mut W,
360    work: &E,
361    prefix_list: Option<&str>,
362) -> Result<(), C14nError> {
363    canonicalize_writer_with_scratch(
364        doc,
365        visibility,
366        algo,
367        max_output_bytes,
368        xml_base_resolution,
369        output,
370        0,
371        &scratch::Accounted(work),
372        prefix_list,
373        None,
374    )
375    .map(|_| ())
376}
377
378/// Materialize only when another transform requires bytes, with the same
379/// serializer, storage admission and position provenance as terminal output.
380#[cfg(feature = "xmldsig")]
381pub(crate) fn canonicalize_buffer_in<S: scratch::Scratch, D: DomStorage>(
382    doc: &Document<'_, D>,
383    visibility: &dyn NodeVisibility<D>,
384    (algo, prefix_list): (&C14nAlgorithm, Option<&str>),
385    tracked: Option<NodeId>,
386    max_output_bytes: usize,
387    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
388    scratch: &S,
389) -> Result<(S::Bytes, Option<usize>), C14nError> {
390    struct Buffer<B> {
391        bytes: B,
392        failure: Option<C14nError>,
393    }
394    impl<B: scratch::Bytes> std::io::Write for Buffer<B> {
395        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
396            if self.failure.is_some() {
397                return Err(std::io::ErrorKind::OutOfMemory.into());
398            }
399            if let Err(error) = self.bytes.append(bytes) {
400                self.failure = Some(error);
401                return Err(std::io::ErrorKind::OutOfMemory.into());
402            }
403            Ok(bytes.len())
404        }
405        fn flush(&mut self) -> std::io::Result<()> {
406            Ok(())
407        }
408    }
409    let mut buffer = Buffer {
410        bytes: scratch.bytes(),
411        failure: None,
412    };
413    let result = canonicalize_writer_with_scratch(
414        doc,
415        Some(visibility),
416        algo,
417        max_output_bytes,
418        xml_base_resolution,
419        &mut buffer,
420        0,
421        scratch,
422        prefix_list,
423        tracked,
424    );
425    if let Some(error) = buffer.failure {
426        return Err(error);
427    }
428    Ok((buffer.bytes, result?))
429}
430
431#[cfg(feature = "xmldsig")]
432#[allow(clippy::too_many_arguments)]
433fn canonicalize_writer_with_scratch<
434    W: std::io::Write + ?Sized,
435    S: scratch::Scratch,
436    D: DomStorage,
437>(
438    doc: &Document<'_, D>,
439    visibility: Option<&dyn NodeVisibility<D>>,
440    algo: &C14nAlgorithm,
441    max_output_bytes: usize,
442    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
443    output: &mut W,
444    initial_offset: usize,
445    scratch: &S,
446    prefix_list: Option<&str>,
447    tracked: Option<NodeId>,
448) -> Result<Option<usize>, C14nError> {
449    let inclusive = InclusiveNsRenderer;
450    let exclusive = match prefix_list {
451        Some(list) => ExclusiveNsRenderer::from_prefix_list(list),
452        None => ExclusiveNsRenderer::new(&algo.inclusive_prefixes),
453    };
454    let renderer: &dyn serialize::NsRenderer<S, D> = match algo.mode {
455        C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
456        C14nMode::Exclusive1_0 => &exclusive,
457    };
458    let (position, _) = serialize::serialize_with_scratch(
459        doc,
460        visibility,
461        algo.with_comments,
462        renderer,
463        C14nConfig::for_mode(algo.mode),
464        CanonicalOutputOptions::bounded(tracked, max_output_bytes, xml_base_resolution),
465        output,
466        initial_offset,
467        scratch,
468    )?;
469    Ok(position)
470}
471
472pub(crate) fn canonicalize_with_visibility(
473    doc: &Document,
474    visibility: Option<&dyn NodeVisibility>,
475    algo: &C14nAlgorithm,
476    output: &mut Vec<u8>,
477) -> Result<(), C14nError> {
478    canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
479    Ok(())
480}
481
482pub(crate) fn canonicalize_with_visibility_and_position(
483    doc: &Document,
484    visibility: Option<&dyn NodeVisibility>,
485    algo: &C14nAlgorithm,
486    tracked_element: Option<NodeId>,
487    output: &mut Vec<u8>,
488) -> Result<Option<usize>, C14nError> {
489    canonicalize_with_visibility_and_position_impl(
490        doc,
491        visibility,
492        algo,
493        tracked_element,
494        None,
495        None,
496        output,
497    )
498}
499
500#[cfg(test)]
501pub(crate) fn canonicalize_with_visibility_and_position_bounded(
502    doc: &Document,
503    visibility: Option<&dyn NodeVisibility>,
504    algo: &C14nAlgorithm,
505    tracked_element: Option<NodeId>,
506    max_output_bytes: usize,
507    output: &mut Vec<u8>,
508) -> Result<Option<usize>, C14nError> {
509    canonicalize_with_visibility_and_position_impl(
510        doc,
511        visibility,
512        algo,
513        tracked_element,
514        Some(max_output_bytes),
515        None,
516        output,
517    )
518}
519
520#[cfg(feature = "xmldsig")]
521pub(crate) fn canonicalize_with_visibility_and_position_bounded_with_xml_base_budget<
522    D: DomStorage,
523>(
524    doc: &Document<'_, D>,
525    visibility: Option<&dyn NodeVisibility<D>>,
526    algo: &C14nAlgorithm,
527    tracked_element: Option<NodeId>,
528    max_output_bytes: usize,
529    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
530    output: &mut Vec<u8>,
531) -> Result<Option<usize>, C14nError> {
532    canonicalize_with_visibility_and_position_impl(
533        doc,
534        visibility,
535        algo,
536        tracked_element,
537        Some(max_output_bytes),
538        Some(xml_base_resolution),
539        output,
540    )
541}
542
543#[cfg(feature = "xmldsig")]
544pub(crate) fn canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget<
545    D: DomStorage,
546>(
547    doc: &Document<'_, D>,
548    visibility: Option<&dyn NodeVisibility<D>>,
549    algo: &C14nAlgorithm,
550    tracked_elements: &[NodeId],
551    max_output_bytes: usize,
552    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
553    output: &mut Vec<u8>,
554) -> Result<Vec<(NodeId, usize)>, C14nError> {
555    let config = C14nConfig::for_mode(algo.mode);
556    let inclusive = InclusiveNsRenderer;
557    let exclusive = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
558    let renderer: &dyn serialize::NsRenderer<scratch::Native, D> = match algo.mode {
559        C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
560        C14nMode::Exclusive1_0 => &exclusive,
561    };
562    serialize_canonical_visible_with_positions_bounded(
563        doc,
564        visibility,
565        algo.with_comments,
566        renderer,
567        config,
568        CanonicalOutputOptions::bounded_many(
569            tracked_elements,
570            max_output_bytes,
571            xml_base_resolution,
572        ),
573        output,
574    )
575}
576
577fn canonicalize_with_visibility_and_position_impl<D: DomStorage>(
578    doc: &Document<'_, D>,
579    visibility: Option<&dyn NodeVisibility<D>>,
580    algo: &C14nAlgorithm,
581    tracked_element: Option<NodeId>,
582    max_output_bytes: Option<usize>,
583    xml_base_resolution: Option<&xml_base::XmlBaseResolutionBudget>,
584    output: &mut Vec<u8>,
585) -> Result<Option<usize>, C14nError> {
586    let default_xml_base_resolution = xml_base::XmlBaseResolutionBudget::default();
587    let xml_base_resolution = xml_base_resolution.unwrap_or(&default_xml_base_resolution);
588    // inherit_xml_attrs: Inclusive C14N inherits xml:* attrs from ancestors
589    // per §2.4. Exclusive C14N explicitly omits this per Exc-C14N §3.
590    // fixup_xml_base: C14N 1.1 resolves relative xml:base URIs via RFC 3986.
591    match algo.mode {
592        C14nMode::Inclusive1_0 => {
593            let renderer = InclusiveNsRenderer;
594            let config = C14nConfig::for_mode(algo.mode);
595            serialize_canonical_visible_with_position_dispatch(
596                doc,
597                visibility,
598                algo.with_comments,
599                &renderer,
600                config,
601                tracked_element,
602                max_output_bytes,
603                xml_base_resolution,
604                output,
605            )
606        }
607        C14nMode::Inclusive1_1 => {
608            let renderer = InclusiveNsRenderer;
609            let config = C14nConfig::for_mode(algo.mode);
610            serialize_canonical_visible_with_position_dispatch(
611                doc,
612                visibility,
613                algo.with_comments,
614                &renderer,
615                config,
616                tracked_element,
617                max_output_bytes,
618                xml_base_resolution,
619                output,
620            )
621        }
622        C14nMode::Exclusive1_0 => {
623            let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
624            let config = C14nConfig::for_mode(algo.mode);
625            serialize_canonical_visible_with_position_dispatch(
626                doc,
627                visibility,
628                algo.with_comments,
629                &renderer,
630                config,
631                tracked_element,
632                max_output_bytes,
633                xml_base_resolution,
634                output,
635            )
636        }
637    }
638}
639
640#[allow(clippy::too_many_arguments)]
641fn serialize_canonical_visible_with_position_dispatch<D: DomStorage>(
642    doc: &Document<'_, D>,
643    visibility: Option<&dyn NodeVisibility<D>>,
644    with_comments: bool,
645    renderer: &dyn serialize::NsRenderer<scratch::Native, D>,
646    config: C14nConfig,
647    tracked_element: Option<NodeId>,
648    max_output_bytes: Option<usize>,
649    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
650    output: &mut Vec<u8>,
651) -> Result<Option<usize>, C14nError> {
652    match max_output_bytes {
653        Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
654            doc,
655            visibility,
656            with_comments,
657            renderer,
658            config,
659            CanonicalOutputOptions::bounded(tracked_element, max_output_bytes, xml_base_resolution),
660            output,
661        ),
662        None => serialize_canonical_visible_with_position_with_xml_base_budget(
663            doc,
664            visibility,
665            with_comments,
666            renderer,
667            config,
668            tracked_element,
669            xml_base_resolution,
670            output,
671        ),
672    }
673}
674
675/// Convenience: parse XML bytes and canonicalize the whole document.
676///
677/// Input must be valid UTF-8 (XML 1.0 documents are UTF-8 or declare their
678/// encoding; roxmltree only accepts UTF-8). DTDs and external entity resolution
679/// are disabled, and the library's absolute XML byte, node, and depth ceilings apply.
680/// Returns `C14nError::Parse` for invalid UTF-8, malformed XML, or exceeded
681/// input ceilings.
682pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
683    canonicalize_xml_with_backend(xml, algo, crate::XmlBackend::default())
684}
685
686/// Parse and canonicalize a complete XML document with an explicit backend.
687pub fn canonicalize_xml_with_backend(
688    xml: &[u8],
689    algo: &C14nAlgorithm,
690    backend: crate::XmlBackend,
691) -> Result<Vec<u8>, C14nError> {
692    if xml.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
693        return Err(C14nError::Parse(format!(
694            "input exceeds maximum XML document size of {} bytes: got {}",
695            crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
696            xml.len()
697        )));
698    }
699    let xml_str =
700        std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
701    let document = crate::document::parse_borrowed_with_settings_and_budget(
702        xml_str,
703        crate::document::DocumentParseSettings::default().with_backend(backend),
704        None,
705    )
706    .map_err(|error| C14nError::Parse(error.to_string()))?;
707    let mut output = Vec::new();
708    canonicalize(&document, None, algo, &mut output)?;
709    Ok(output)
710}
711
712/// Canonicalize a retained owned document without reparsing it.
713pub fn canonicalize_document(
714    document: &crate::XmlDocument,
715    algo: &C14nAlgorithm,
716) -> Result<Vec<u8>, C14nError> {
717    let mut output = Vec::new();
718    document.with_view(|view| canonicalize(view.document(), None, algo, &mut output))?;
719    Ok(output)
720}
721
722#[cfg(test)]
723#[allow(clippy::unwrap_used)]
724mod tests {
725    use super::*;
726
727    #[test]
728    fn from_uri_roundtrip() {
729        let uris = [
730            "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
731            "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
732            "http://www.w3.org/2006/12/xml-c14n11",
733            "http://www.w3.org/2006/12/xml-c14n11#WithComments",
734            "http://www.w3.org/2001/10/xml-exc-c14n#",
735            "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
736        ];
737        for uri in uris {
738            let algo = C14nAlgorithm::from_uri(uri).expect(uri);
739            assert_eq!(algo.uri(), uri);
740        }
741    }
742
743    #[test]
744    fn unknown_uri_returns_none() {
745        assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
746    }
747
748    #[test]
749    fn prefix_list_parsing() {
750        let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
751            .with_prefix_list("foo bar #default baz");
752        assert!(algo.inclusive_prefixes.contains("foo"));
753        assert!(algo.inclusive_prefixes.contains("bar"));
754        assert!(algo.inclusive_prefixes.contains("baz"));
755        assert!(algo.inclusive_prefixes.contains("")); // #default → ""
756        assert_eq!(algo.inclusive_prefixes.len(), 4);
757    }
758
759    #[test]
760    fn canonicalize_xml_basic() {
761        let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
762        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
763        let result = canonicalize_xml(xml, &algo).expect("c14n");
764        assert_eq!(
765            String::from_utf8(result).expect("utf8"),
766            r#"<root a="1" b="2"><empty></empty></root>"#
767        );
768    }
769
770    #[test]
771    fn canonicalize_xml_rejects_input_above_the_document_byte_ceiling_before_parsing() {
772        // The convenience parser accepts untrusted bytes, so allocation bounds
773        // must apply before UTF-8 or XML parsing can inspect the payload.
774        let xml = vec![b' '; crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1];
775        let error = match canonicalize_xml(&xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
776        {
777            Err(error) => error,
778            Ok(_) => panic!("oversized canonicalization input must be rejected"),
779        };
780        assert!(matches!(
781            error,
782            C14nError::Parse(message)
783                if message.contains("exceeds maximum XML document size")
784        ));
785    }
786
787    #[test]
788    fn canonicalize_xml_rejects_input_above_the_document_node_ceiling() {
789        // A compact document can otherwise force an effectively unbounded
790        // parser-node allocation despite staying below the byte ceiling.
791        let children = "<n/>".repeat(crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize);
792        let xml = format!("<root>{children}</root>");
793        let error = match canonicalize_xml(
794            xml.as_bytes(),
795            &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
796        ) {
797            Err(error) => error,
798            Ok(_) => panic!("excessive canonicalization nodes must be rejected"),
799        };
800        assert!(matches!(
801            error,
802            C14nError::Parse(message) if message.contains("nodes limit reached")
803        ));
804    }
805
806    #[test]
807    fn canonicalize_xml_rejects_input_above_the_document_depth_ceiling() {
808        // The convenience parser is a public untrusted-input boundary, so the
809        // absolute depth ceiling must apply before recursive C14N traversal.
810        let depth = crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING + 1;
811        let xml = format!("{}{}", "<n>".repeat(depth), "</n>".repeat(depth));
812
813        let error = canonicalize_xml(
814            xml.as_bytes(),
815            &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
816        )
817        .expect_err("over-depth canonicalization input must be rejected");
818
819        assert!(matches!(
820            error,
821            C14nError::Parse(message) if message.contains("maximum element depth")
822        ));
823    }
824
825    #[test]
826    fn canonicalize_xml_does_not_enable_dtd_or_external_entity_resolution() {
827        // Whole-document C14N is a convenience API, not an implicit opt-in to
828        // DTD parsing or external resource access.
829        let xml = br#"<!DOCTYPE root [<!ENTITY value 'expanded'>]><root>&value;</root>"#;
830        let error = canonicalize_xml(xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
831            .expect_err("DTD input must remain disabled");
832        assert!(matches!(
833            error,
834            C14nError::Parse(message) if message.contains("DTD detected")
835        ));
836    }
837
838    #[test]
839    fn c14n_1_1_basic() {
840        // C14N 1.1 serialization is identical to 1.0 for full documents.
841        let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
842        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
843        let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
844        assert_eq!(
845            String::from_utf8(result).expect("utf8"),
846            r#"<root a="1" b="2"><empty></empty></root>"#
847        );
848    }
849
850    #[test]
851    fn c14n_1_1_with_comments() {
852        let xml = b"<root><!-- comment -->text</root>";
853        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
854        let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
855        assert_eq!(
856            String::from_utf8(result).expect("utf8"),
857            "<root><!-- comment -->text</root>"
858        );
859    }
860
861    #[test]
862    fn c14n_1_1_without_comments() {
863        let xml = b"<root><!-- comment -->text</root>";
864        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
865        let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
866        assert_eq!(
867            String::from_utf8(result).expect("utf8"),
868            "<root>text</root>"
869        );
870    }
871
872    #[test]
873    fn c14n_1_1_namespaces() {
874        // C14N 1.1 renders all in-scope namespaces like 1.0.
875        let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
876        let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
877        let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
878        let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
879        let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
880        // For full documents, 1.0 and 1.1 produce identical output.
881        assert_eq!(result_10, result_11);
882    }
883
884    #[test]
885    fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
886        // C14N 1.1 explicitly excludes xml:id from simple inheritable
887        // attributes, so omitting its owner must also omit the attribute.
888        use crate::xml::dom::Document;
889        use std::collections::HashSet;
890
891        let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
892        let doc = Document::parse(xml).expect("parse");
893        let child = doc.root_element().first_element_child().expect("child");
894
895        // Build subset: child + its descendants, excluding root
896        let mut ids = HashSet::new();
897        let mut stack = vec![child];
898        while let Some(n) = stack.pop() {
899            ids.insert(n.id());
900            for c in n.children() {
901                stack.push(c);
902            }
903        }
904        let pred = move |n: crate::xml::dom::Node| ids.contains(&n.id());
905
906        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
907        let mut out = Vec::new();
908        canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
909        let result = String::from_utf8(out).expect("utf8");
910
911        assert!(
912            !result.contains(r#"xml:id="r1""#),
913            "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
914        );
915    }
916
917    #[test]
918    fn c14n_1_0_xml_id_is_inherited_in_subset() {
919        // C14N 1.0 predates the C14N 1.1 xml:id exception, so xml:id follows
920        // the general xml:* apex inheritance rule in a document subset.
921        use crate::xml::dom::Document;
922        use std::collections::HashSet;
923
924        let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
925        let doc = Document::parse(xml).expect("parse");
926        let child = doc.root_element().first_element_child().expect("child");
927        let ids = child
928            .descendants()
929            .map(|node| node.id())
930            .collect::<HashSet<_>>();
931        let pred = move |node: crate::xml::dom::Node| ids.contains(&node.id());
932
933        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
934        let mut out = Vec::new();
935        canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
936
937        assert_eq!(
938            String::from_utf8(out).expect("utf8"),
939            r#"<child xml:id="r1">text</child>"#
940        );
941    }
942
943    #[test]
944    fn bounded_canonicalization_stops_before_exceeding_the_limit() {
945        // XMLDSig applies a signature-wide output ceiling. The serializer must
946        // stop at that ceiling instead of allocating the complete hostile value
947        // and rejecting it only after serialization has finished.
948        let xml = format!("<root>{}</root>", "x".repeat(4_096));
949        let document = Document::parse(&xml).expect("fixed XML must parse");
950        let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
951        let mut output = Vec::new();
952
953        let error = canonicalize_with_visibility_and_position_bounded(
954            &document,
955            None,
956            &algorithm,
957            None,
958            64,
959            &mut output,
960        )
961        .expect_err("canonicalization must stop at the caller's byte ceiling");
962
963        assert!(is_output_limit_error(&error));
964        assert!(
965            output.len() <= 64,
966            "bounded output grew to {} bytes",
967            output.len()
968        );
969    }
970
971    #[test]
972    fn c14n_1_1_bounds_inherited_xml_base_components() {
973        // C14N 1.1 subset fixup walks ancestors outside the selected node set.
974        // Bounding that walk prevents deeply nested xml:base chains from
975        // multiplying URI-resolution work during canonicalization.
976        let mut xml = String::new();
977        for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
978            xml.push_str(r#"<n xml:base="segment/">"#);
979        }
980        xml.push_str("<leaf/>");
981        for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
982            xml.push_str("</n>");
983        }
984        let document = Document::parse(&xml).expect("fixed XML must parse");
985        let leaf = document
986            .descendants()
987            .find(|node| node.has_tag_name("leaf"))
988            .expect("leaf");
989        let visible = |node: Node<'_, '_>| node == leaf;
990        let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
991        let mut output = Vec::new();
992
993        let error = canonicalize_with_visibility(
994            &document,
995            Some(&ClosureVisibility {
996                predicate: &visible,
997            }),
998            &algorithm,
999            &mut output,
1000        )
1001        .expect_err("C14N must reject an overlong inherited xml:base chain");
1002
1003        assert!(
1004            error.to_string().contains("XML Base"),
1005            "unexpected C14N error: {error}"
1006        );
1007    }
1008
1009    #[test]
1010    fn unbounded_output_preserves_the_callers_xml_base_budget() {
1011        // Output capacity and XML Base work are independent limits. Omitting
1012        // an output ceiling must not replace the caller's XML Base policy.
1013        let document = Document::parse(
1014            r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
1015        )
1016        .unwrap();
1017        let leaf = document
1018            .descendants()
1019            .find(|node| node.has_tag_name("leaf"))
1020            .unwrap();
1021        let visible = |node: Node<'_, '_>| node == leaf;
1022        let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
1023        let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
1024        let mut output = Vec::new();
1025
1026        let error = canonicalize_with_visibility_and_position_impl(
1027            &document,
1028            Some(&ClosureVisibility {
1029                predicate: &visible,
1030            }),
1031            &algorithm,
1032            None,
1033            None,
1034            Some(&budget),
1035            &mut output,
1036        )
1037        .expect_err("the caller's component ceiling must survive unbounded dispatch");
1038
1039        assert!(matches!(
1040            error,
1041            C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
1042        ));
1043    }
1044}