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/// Terminal stream for any scratch and DOM storage; nothing is collected.
379#[cfg(feature = "xmldsig")]
380pub(crate) fn canonicalize_visible_to_writer_in<
381    W: std::io::Write + ?Sized,
382    S: scratch::Scratch,
383    D: DomStorage,
384>(
385    doc: &Document<'_, D>,
386    visibility: &dyn NodeVisibility<D>,
387    (algo, prefix_list): (&C14nAlgorithm, Option<&str>),
388    max_output_bytes: usize,
389    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
390    output: &mut W,
391    scratch: &S,
392) -> Result<(), C14nError> {
393    canonicalize_writer_with_scratch(
394        doc,
395        Some(visibility),
396        algo,
397        max_output_bytes,
398        xml_base_resolution,
399        output,
400        0,
401        scratch,
402        prefix_list,
403        None,
404    )
405    .map(|_| ())
406}
407
408/// Materialize only when another transform requires bytes, with the same
409/// serializer, storage admission and position provenance as terminal output.
410#[cfg(feature = "xmldsig")]
411pub(crate) fn canonicalize_buffer_in<S: scratch::Scratch, D: DomStorage>(
412    doc: &Document<'_, D>,
413    visibility: &dyn NodeVisibility<D>,
414    (algo, prefix_list): (&C14nAlgorithm, Option<&str>),
415    tracked: Option<NodeId>,
416    max_output_bytes: usize,
417    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
418    scratch: &S,
419) -> Result<(S::Bytes, Option<usize>), C14nError> {
420    struct Buffer<B> {
421        bytes: B,
422        failure: Option<C14nError>,
423    }
424    impl<B: scratch::Bytes> std::io::Write for Buffer<B> {
425        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
426            if self.failure.is_some() {
427                return Err(std::io::ErrorKind::OutOfMemory.into());
428            }
429            if let Err(error) = self.bytes.append(bytes) {
430                self.failure = Some(error);
431                return Err(std::io::ErrorKind::OutOfMemory.into());
432            }
433            Ok(bytes.len())
434        }
435        fn flush(&mut self) -> std::io::Result<()> {
436            Ok(())
437        }
438    }
439    let mut buffer = Buffer {
440        bytes: scratch.bytes(),
441        failure: None,
442    };
443    let result = canonicalize_writer_with_scratch(
444        doc,
445        Some(visibility),
446        algo,
447        max_output_bytes,
448        xml_base_resolution,
449        &mut buffer,
450        0,
451        scratch,
452        prefix_list,
453        tracked,
454    );
455    if let Some(error) = buffer.failure {
456        return Err(error);
457    }
458    Ok((buffer.bytes, result?))
459}
460
461#[cfg(feature = "xmldsig")]
462#[allow(clippy::too_many_arguments)]
463fn canonicalize_writer_with_scratch<
464    W: std::io::Write + ?Sized,
465    S: scratch::Scratch,
466    D: DomStorage,
467>(
468    doc: &Document<'_, D>,
469    visibility: Option<&dyn NodeVisibility<D>>,
470    algo: &C14nAlgorithm,
471    max_output_bytes: usize,
472    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
473    output: &mut W,
474    initial_offset: usize,
475    scratch: &S,
476    prefix_list: Option<&str>,
477    tracked: Option<NodeId>,
478) -> Result<Option<usize>, C14nError> {
479    let inclusive = InclusiveNsRenderer;
480    let exclusive = match prefix_list {
481        Some(list) => ExclusiveNsRenderer::from_prefix_list(list),
482        None => ExclusiveNsRenderer::new(&algo.inclusive_prefixes),
483    };
484    let renderer: &dyn serialize::NsRenderer<S, D> = match algo.mode {
485        C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
486        C14nMode::Exclusive1_0 => &exclusive,
487    };
488    let (position, _) = serialize::serialize_with_scratch(
489        doc,
490        visibility,
491        algo.with_comments,
492        renderer,
493        C14nConfig::for_mode(algo.mode),
494        CanonicalOutputOptions::bounded(tracked, max_output_bytes, xml_base_resolution),
495        output,
496        initial_offset,
497        scratch,
498    )?;
499    Ok(position)
500}
501
502pub(crate) fn canonicalize_with_visibility(
503    doc: &Document,
504    visibility: Option<&dyn NodeVisibility>,
505    algo: &C14nAlgorithm,
506    output: &mut Vec<u8>,
507) -> Result<(), C14nError> {
508    canonicalize_with_visibility_and_position(doc, visibility, algo, None, output)?;
509    Ok(())
510}
511
512pub(crate) fn canonicalize_with_visibility_and_position(
513    doc: &Document,
514    visibility: Option<&dyn NodeVisibility>,
515    algo: &C14nAlgorithm,
516    tracked_element: Option<NodeId>,
517    output: &mut Vec<u8>,
518) -> Result<Option<usize>, C14nError> {
519    canonicalize_with_visibility_and_position_impl(
520        doc,
521        visibility,
522        algo,
523        tracked_element,
524        None,
525        None,
526        output,
527    )
528}
529
530#[cfg(test)]
531pub(crate) fn canonicalize_with_visibility_and_position_bounded(
532    doc: &Document,
533    visibility: Option<&dyn NodeVisibility>,
534    algo: &C14nAlgorithm,
535    tracked_element: Option<NodeId>,
536    max_output_bytes: usize,
537    output: &mut Vec<u8>,
538) -> Result<Option<usize>, C14nError> {
539    canonicalize_with_visibility_and_position_impl(
540        doc,
541        visibility,
542        algo,
543        tracked_element,
544        Some(max_output_bytes),
545        None,
546        output,
547    )
548}
549
550#[cfg(feature = "xmldsig")]
551pub(crate) fn canonicalize_with_visibility_and_position_bounded_with_xml_base_budget<
552    D: DomStorage,
553>(
554    doc: &Document<'_, D>,
555    visibility: Option<&dyn NodeVisibility<D>>,
556    algo: &C14nAlgorithm,
557    tracked_element: Option<NodeId>,
558    max_output_bytes: usize,
559    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
560    output: &mut Vec<u8>,
561) -> Result<Option<usize>, C14nError> {
562    canonicalize_with_visibility_and_position_impl(
563        doc,
564        visibility,
565        algo,
566        tracked_element,
567        Some(max_output_bytes),
568        Some(xml_base_resolution),
569        output,
570    )
571}
572
573#[cfg(feature = "xmldsig")]
574pub(crate) fn canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget<
575    D: DomStorage,
576>(
577    doc: &Document<'_, D>,
578    visibility: Option<&dyn NodeVisibility<D>>,
579    algo: &C14nAlgorithm,
580    tracked_elements: &[NodeId],
581    max_output_bytes: usize,
582    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
583    output: &mut Vec<u8>,
584) -> Result<Vec<(NodeId, usize)>, C14nError> {
585    let config = C14nConfig::for_mode(algo.mode);
586    let inclusive = InclusiveNsRenderer;
587    let exclusive = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
588    let renderer: &dyn serialize::NsRenderer<scratch::Native, D> = match algo.mode {
589        C14nMode::Inclusive1_0 | C14nMode::Inclusive1_1 => &inclusive,
590        C14nMode::Exclusive1_0 => &exclusive,
591    };
592    serialize_canonical_visible_with_positions_bounded(
593        doc,
594        visibility,
595        algo.with_comments,
596        renderer,
597        config,
598        CanonicalOutputOptions::bounded_many(
599            tracked_elements,
600            max_output_bytes,
601            xml_base_resolution,
602        ),
603        output,
604    )
605}
606
607fn canonicalize_with_visibility_and_position_impl<D: DomStorage>(
608    doc: &Document<'_, D>,
609    visibility: Option<&dyn NodeVisibility<D>>,
610    algo: &C14nAlgorithm,
611    tracked_element: Option<NodeId>,
612    max_output_bytes: Option<usize>,
613    xml_base_resolution: Option<&xml_base::XmlBaseResolutionBudget>,
614    output: &mut Vec<u8>,
615) -> Result<Option<usize>, C14nError> {
616    let default_xml_base_resolution = xml_base::XmlBaseResolutionBudget::default();
617    let xml_base_resolution = xml_base_resolution.unwrap_or(&default_xml_base_resolution);
618    // inherit_xml_attrs: Inclusive C14N inherits xml:* attrs from ancestors
619    // per §2.4. Exclusive C14N explicitly omits this per Exc-C14N §3.
620    // fixup_xml_base: C14N 1.1 resolves relative xml:base URIs via RFC 3986.
621    match algo.mode {
622        C14nMode::Inclusive1_0 => {
623            let renderer = InclusiveNsRenderer;
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        C14nMode::Inclusive1_1 => {
638            let renderer = InclusiveNsRenderer;
639            let config = C14nConfig::for_mode(algo.mode);
640            serialize_canonical_visible_with_position_dispatch(
641                doc,
642                visibility,
643                algo.with_comments,
644                &renderer,
645                config,
646                tracked_element,
647                max_output_bytes,
648                xml_base_resolution,
649                output,
650            )
651        }
652        C14nMode::Exclusive1_0 => {
653            let renderer = ExclusiveNsRenderer::new(&algo.inclusive_prefixes);
654            let config = C14nConfig::for_mode(algo.mode);
655            serialize_canonical_visible_with_position_dispatch(
656                doc,
657                visibility,
658                algo.with_comments,
659                &renderer,
660                config,
661                tracked_element,
662                max_output_bytes,
663                xml_base_resolution,
664                output,
665            )
666        }
667    }
668}
669
670#[allow(clippy::too_many_arguments)]
671fn serialize_canonical_visible_with_position_dispatch<D: DomStorage>(
672    doc: &Document<'_, D>,
673    visibility: Option<&dyn NodeVisibility<D>>,
674    with_comments: bool,
675    renderer: &dyn serialize::NsRenderer<scratch::Native, D>,
676    config: C14nConfig,
677    tracked_element: Option<NodeId>,
678    max_output_bytes: Option<usize>,
679    xml_base_resolution: &xml_base::XmlBaseResolutionBudget,
680    output: &mut Vec<u8>,
681) -> Result<Option<usize>, C14nError> {
682    match max_output_bytes {
683        Some(max_output_bytes) => serialize_canonical_visible_with_position_bounded(
684            doc,
685            visibility,
686            with_comments,
687            renderer,
688            config,
689            CanonicalOutputOptions::bounded(tracked_element, max_output_bytes, xml_base_resolution),
690            output,
691        ),
692        None => serialize_canonical_visible_with_position_with_xml_base_budget(
693            doc,
694            visibility,
695            with_comments,
696            renderer,
697            config,
698            tracked_element,
699            xml_base_resolution,
700            output,
701        ),
702    }
703}
704
705/// Convenience: parse XML bytes and canonicalize the whole document.
706///
707/// Input must be valid UTF-8 (XML 1.0 documents are UTF-8 or declare their
708/// encoding; roxmltree only accepts UTF-8). DTDs and external entity resolution
709/// are disabled, and the library's absolute XML byte, node, and depth ceilings apply.
710/// Returns `C14nError::Parse` for invalid UTF-8, malformed XML, or exceeded
711/// input ceilings.
712pub fn canonicalize_xml(xml: &[u8], algo: &C14nAlgorithm) -> Result<Vec<u8>, C14nError> {
713    canonicalize_xml_with_backend(xml, algo, crate::XmlBackend::default())
714}
715
716/// Parse and canonicalize a complete XML document with an explicit backend.
717pub fn canonicalize_xml_with_backend(
718    xml: &[u8],
719    algo: &C14nAlgorithm,
720    backend: crate::XmlBackend,
721) -> Result<Vec<u8>, C14nError> {
722    if xml.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
723        return Err(C14nError::Parse(format!(
724            "input exceeds maximum XML document size of {} bytes: got {}",
725            crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
726            xml.len()
727        )));
728    }
729    let xml_str =
730        std::str::from_utf8(xml).map_err(|e| C14nError::Parse(format!("invalid UTF-8: {e}")))?;
731    let document = crate::document::parse_borrowed_with_settings_and_budget(
732        xml_str,
733        crate::document::DocumentParseSettings::default().with_backend(backend),
734        None,
735    )
736    .map_err(|error| C14nError::Parse(error.to_string()))?;
737    let mut output = Vec::new();
738    canonicalize(&document, None, algo, &mut output)?;
739    Ok(output)
740}
741
742/// Canonicalize a retained owned document without reparsing it.
743pub fn canonicalize_document(
744    document: &crate::XmlDocument,
745    algo: &C14nAlgorithm,
746) -> Result<Vec<u8>, C14nError> {
747    let mut output = Vec::new();
748    document.with_view(|view| canonicalize(view.document(), None, algo, &mut output))?;
749    Ok(output)
750}
751
752#[cfg(test)]
753#[allow(clippy::unwrap_used)]
754mod tests {
755    use super::*;
756
757    #[test]
758    fn from_uri_roundtrip() {
759        let uris = [
760            "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
761            "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
762            "http://www.w3.org/2006/12/xml-c14n11",
763            "http://www.w3.org/2006/12/xml-c14n11#WithComments",
764            "http://www.w3.org/2001/10/xml-exc-c14n#",
765            "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
766        ];
767        for uri in uris {
768            let algo = C14nAlgorithm::from_uri(uri).expect(uri);
769            assert_eq!(algo.uri(), uri);
770        }
771    }
772
773    #[test]
774    fn unknown_uri_returns_none() {
775        assert!(C14nAlgorithm::from_uri("http://example.com/unknown").is_none());
776    }
777
778    #[test]
779    fn prefix_list_parsing() {
780        let algo = C14nAlgorithm::new(C14nMode::Exclusive1_0, false)
781            .with_prefix_list("foo bar #default baz");
782        assert!(algo.inclusive_prefixes.contains("foo"));
783        assert!(algo.inclusive_prefixes.contains("bar"));
784        assert!(algo.inclusive_prefixes.contains("baz"));
785        assert!(algo.inclusive_prefixes.contains("")); // #default → ""
786        assert_eq!(algo.inclusive_prefixes.len(), 4);
787    }
788
789    #[test]
790    fn canonicalize_xml_basic() {
791        let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
792        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
793        let result = canonicalize_xml(xml, &algo).expect("c14n");
794        assert_eq!(
795            String::from_utf8(result).expect("utf8"),
796            r#"<root a="1" b="2"><empty></empty></root>"#
797        );
798    }
799
800    #[test]
801    fn canonicalize_xml_rejects_input_above_the_document_byte_ceiling_before_parsing() {
802        // The convenience parser accepts untrusted bytes, so allocation bounds
803        // must apply before UTF-8 or XML parsing can inspect the payload.
804        let xml = vec![b' '; crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1];
805        let error = match canonicalize_xml(&xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
806        {
807            Err(error) => error,
808            Ok(_) => panic!("oversized canonicalization input must be rejected"),
809        };
810        assert!(matches!(
811            error,
812            C14nError::Parse(message)
813                if message.contains("exceeds maximum XML document size")
814        ));
815    }
816
817    #[test]
818    fn canonicalize_xml_rejects_input_above_the_document_node_ceiling() {
819        // A compact document can otherwise force an effectively unbounded
820        // parser-node allocation despite staying below the byte ceiling.
821        let children = "<n/>".repeat(crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize);
822        let xml = format!("<root>{children}</root>");
823        let error = match canonicalize_xml(
824            xml.as_bytes(),
825            &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
826        ) {
827            Err(error) => error,
828            Ok(_) => panic!("excessive canonicalization nodes must be rejected"),
829        };
830        assert!(matches!(
831            error,
832            C14nError::Parse(message) if message.contains("nodes limit reached")
833        ));
834    }
835
836    #[test]
837    fn canonicalize_xml_rejects_input_above_the_document_depth_ceiling() {
838        // The convenience parser is a public untrusted-input boundary, so the
839        // absolute depth ceiling must apply before recursive C14N traversal.
840        let depth = crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING + 1;
841        let xml = format!("{}{}", "<n>".repeat(depth), "</n>".repeat(depth));
842
843        let error = canonicalize_xml(
844            xml.as_bytes(),
845            &C14nAlgorithm::new(C14nMode::Inclusive1_0, false),
846        )
847        .expect_err("over-depth canonicalization input must be rejected");
848
849        assert!(matches!(
850            error,
851            C14nError::Parse(message) if message.contains("maximum element depth")
852        ));
853    }
854
855    #[test]
856    fn canonicalize_xml_does_not_enable_dtd_or_external_entity_resolution() {
857        // Whole-document C14N is a convenience API, not an implicit opt-in to
858        // DTD parsing or external resource access.
859        let xml = br#"<!DOCTYPE root [<!ENTITY value 'expanded'>]><root>&value;</root>"#;
860        let error = canonicalize_xml(xml, &C14nAlgorithm::new(C14nMode::Inclusive1_0, false))
861            .expect_err("DTD input must remain disabled");
862        assert!(matches!(
863            error,
864            C14nError::Parse(message) if message.contains("DTD detected")
865        ));
866    }
867
868    #[test]
869    fn c14n_1_1_basic() {
870        // C14N 1.1 serialization is identical to 1.0 for full documents.
871        let xml = b"<root b=\"2\" a=\"1\"><empty/></root>";
872        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
873        let result = canonicalize_xml(xml, &algo).expect("c14n 1.1");
874        assert_eq!(
875            String::from_utf8(result).expect("utf8"),
876            r#"<root a="1" b="2"><empty></empty></root>"#
877        );
878    }
879
880    #[test]
881    fn c14n_1_1_with_comments() {
882        let xml = b"<root><!-- comment -->text</root>";
883        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, true);
884        let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 with comments");
885        assert_eq!(
886            String::from_utf8(result).expect("utf8"),
887            "<root><!-- comment -->text</root>"
888        );
889    }
890
891    #[test]
892    fn c14n_1_1_without_comments() {
893        let xml = b"<root><!-- comment -->text</root>";
894        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
895        let result = canonicalize_xml(xml, &algo).expect("c14n 1.1 without comments");
896        assert_eq!(
897            String::from_utf8(result).expect("utf8"),
898            "<root>text</root>"
899        );
900    }
901
902    #[test]
903    fn c14n_1_1_namespaces() {
904        // C14N 1.1 renders all in-scope namespaces like 1.0.
905        let xml = b"<root xmlns:a=\"http://a\" xmlns:b=\"http://b\"><child/></root>";
906        let algo_10 = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
907        let algo_11 = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
908        let result_10 = canonicalize_xml(xml, &algo_10).expect("1.0");
909        let result_11 = canonicalize_xml(xml, &algo_11).expect("1.1");
910        // For full documents, 1.0 and 1.1 produce identical output.
911        assert_eq!(result_10, result_11);
912    }
913
914    #[test]
915    fn c14n_1_1_xml_id_is_not_inherited_in_subset() {
916        // C14N 1.1 explicitly excludes xml:id from simple inheritable
917        // attributes, so omitting its owner must also omit the attribute.
918        use crate::xml::dom::Document;
919        use std::collections::HashSet;
920
921        let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
922        let doc = Document::parse(xml).expect("parse");
923        let child = doc.root_element().first_element_child().expect("child");
924
925        // Build subset: child + its descendants, excluding root
926        let mut ids = HashSet::new();
927        let mut stack = vec![child];
928        while let Some(n) = stack.pop() {
929            ids.insert(n.id());
930            for c in n.children() {
931                stack.push(c);
932            }
933        }
934        let pred = move |n: crate::xml::dom::Node| ids.contains(&n.id());
935
936        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
937        let mut out = Vec::new();
938        canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.1 subset");
939        let result = String::from_utf8(out).expect("utf8");
940
941        assert!(
942            !result.contains(r#"xml:id="r1""#),
943            "xml:id must not be inherited in C14N 1.1 subset; got: {result}"
944        );
945    }
946
947    #[test]
948    fn c14n_1_0_xml_id_is_inherited_in_subset() {
949        // C14N 1.0 predates the C14N 1.1 xml:id exception, so xml:id follows
950        // the general xml:* apex inheritance rule in a document subset.
951        use crate::xml::dom::Document;
952        use std::collections::HashSet;
953
954        let xml = r#"<root xml:id="r1"><child>text</child></root>"#;
955        let doc = Document::parse(xml).expect("parse");
956        let child = doc.root_element().first_element_child().expect("child");
957        let ids = child
958            .descendants()
959            .map(|node| node.id())
960            .collect::<HashSet<_>>();
961        let pred = move |node: crate::xml::dom::Node| ids.contains(&node.id());
962
963        let algo = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
964        let mut out = Vec::new();
965        canonicalize(&doc, Some(&pred), &algo, &mut out).expect("c14n 1.0 subset");
966
967        assert_eq!(
968            String::from_utf8(out).expect("utf8"),
969            r#"<child xml:id="r1">text</child>"#
970        );
971    }
972
973    #[test]
974    fn bounded_canonicalization_stops_before_exceeding_the_limit() {
975        // XMLDSig applies a signature-wide output ceiling. The serializer must
976        // stop at that ceiling instead of allocating the complete hostile value
977        // and rejecting it only after serialization has finished.
978        let xml = format!("<root>{}</root>", "x".repeat(4_096));
979        let document = Document::parse(&xml).expect("fixed XML must parse");
980        let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_0, false);
981        let mut output = Vec::new();
982
983        let error = canonicalize_with_visibility_and_position_bounded(
984            &document,
985            None,
986            &algorithm,
987            None,
988            64,
989            &mut output,
990        )
991        .expect_err("canonicalization must stop at the caller's byte ceiling");
992
993        assert!(is_output_limit_error(&error));
994        assert!(
995            output.len() <= 64,
996            "bounded output grew to {} bytes",
997            output.len()
998        );
999    }
1000
1001    #[test]
1002    fn c14n_1_1_bounds_inherited_xml_base_components() {
1003        // C14N 1.1 subset fixup walks ancestors outside the selected node set.
1004        // Bounding that walk prevents deeply nested xml:base chains from
1005        // multiplying URI-resolution work during canonicalization.
1006        let mut xml = String::new();
1007        for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
1008            xml.push_str(r#"<n xml:base="segment/">"#);
1009        }
1010        xml.push_str("<leaf/>");
1011        for _ in 0..=crate::hard_limits::XML_BASE_COMPONENT_CEILING {
1012            xml.push_str("</n>");
1013        }
1014        let document = Document::parse(&xml).expect("fixed XML must parse");
1015        let leaf = document
1016            .descendants()
1017            .find(|node| node.has_tag_name("leaf"))
1018            .expect("leaf");
1019        let visible = |node: Node<'_, '_>| node == leaf;
1020        let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
1021        let mut output = Vec::new();
1022
1023        let error = canonicalize_with_visibility(
1024            &document,
1025            Some(&ClosureVisibility {
1026                predicate: &visible,
1027            }),
1028            &algorithm,
1029            &mut output,
1030        )
1031        .expect_err("C14N must reject an overlong inherited xml:base chain");
1032
1033        assert!(
1034            error.to_string().contains("XML Base"),
1035            "unexpected C14N error: {error}"
1036        );
1037    }
1038
1039    #[test]
1040    fn unbounded_output_preserves_the_callers_xml_base_budget() {
1041        // Output capacity and XML Base work are independent limits. Omitting
1042        // an output ceiling must not replace the caller's XML Base policy.
1043        let document = Document::parse(
1044            r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
1045        )
1046        .unwrap();
1047        let leaf = document
1048            .descendants()
1049            .find(|node| node.has_tag_name("leaf"))
1050            .unwrap();
1051        let visible = |node: Node<'_, '_>| node == leaf;
1052        let budget = xml_base::XmlBaseResolutionBudget::with_limits(1, usize::MAX);
1053        let algorithm = C14nAlgorithm::new(C14nMode::Inclusive1_1, false);
1054        let mut output = Vec::new();
1055
1056        let error = canonicalize_with_visibility_and_position_impl(
1057            &document,
1058            Some(&ClosureVisibility {
1059                predicate: &visible,
1060            }),
1061            &algorithm,
1062            None,
1063            None,
1064            Some(&budget),
1065            &mut output,
1066        )
1067        .expect_err("the caller's component ceiling must survive unbounded dispatch");
1068
1069        assert!(matches!(
1070            error,
1071            C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
1072        ));
1073    }
1074}