Skip to main content

xml_sec/xml/dom/
mod.rs

1//! Backend-neutral XML semantic tree contract.
2//!
3//! One explicitly selected parser backend builds this tree. All XML Security semantics are
4//! implemented above it, so selecting a backend cannot change C14N, XPath,
5//! XMLDSig, or XMLEnc behavior.
6
7#[cfg(feature = "benchmark-internals")]
8pub(crate) mod benchmark;
9#[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
10mod differential;
11mod preflight;
12#[cfg(feature = "xml-backend-roxmltree")]
13mod roxmltree;
14#[doc(hidden)]
15pub mod storage;
16mod tree;
17#[cfg(feature = "xml-backend-xmloxide")]
18mod xmloxide;
19
20use std::fmt;
21
22use self::preflight::LexicalPreflight;
23
24pub use tree::{
25    Ancestors, Attribute, Attributes, Children, Descendants, Document, ExpandedName, Namespace,
26    Namespaces, Node, NodeId, NodeType, PI, XmlIdError,
27};
28
29/// Parser-neutral options used after bounded lexical preflight.
30#[derive(Clone, Copy, Debug)]
31pub struct ParsingOptions {
32    /// Whether an internal DTD subset is accepted.
33    pub allow_dtd: bool,
34    /// Maximum retained semantic node count.
35    pub nodes_limit: u32,
36}
37
38impl Default for ParsingOptions {
39    fn default() -> Self {
40        Self {
41            allow_dtd: false,
42            nodes_limit: u32::MAX,
43        }
44    }
45}
46
47/// XML parser implementation selected for one document or operation.
48///
49/// Cargo features control which implementations are compiled. Selecting an
50/// implementation absent from a thin build fails explicitly; no parser
51/// fallback is performed.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub enum XmlBackend {
54    /// Parse with the xmloxide adapter.
55    Xmloxide,
56    /// Parse with the roxmltree adapter.
57    Roxmltree,
58    /// Parse with both adapters and fail closed unless their semantic arenas agree.
59    Differential,
60}
61
62impl XmlBackend {
63    pub(crate) const fn build_default() -> Self {
64        if cfg!(feature = "xml-backend-differential") {
65            Self::Differential
66        } else if cfg!(feature = "xml-backend-xmloxide") {
67            Self::Xmloxide
68        } else {
69            Self::Roxmltree
70        }
71    }
72
73    /// Returns whether this backend can run in the current build.
74    #[must_use]
75    pub const fn is_available(self) -> bool {
76        match self {
77            Self::Xmloxide => cfg!(feature = "xml-backend-xmloxide"),
78            Self::Roxmltree => cfg!(feature = "xml-backend-roxmltree"),
79            Self::Differential => cfg!(all(
80                feature = "xml-backend-xmloxide",
81                feature = "xml-backend-roxmltree"
82            )),
83        }
84    }
85
86    /// Iterate over every runtime mode available in this build.
87    pub fn available() -> impl Iterator<Item = Self> {
88        [Self::Xmloxide, Self::Roxmltree, Self::Differential]
89            .into_iter()
90            .filter(|backend| backend.is_available())
91    }
92
93    fn parse<'input>(
94        self,
95        input: &'input str,
96        options: ParsingOptions,
97        preflight: &LexicalPreflight,
98    ) -> Result<Document<'input>, ParseError> {
99        match self {
100            Self::Xmloxide => parse_with_xmloxide(input, options, preflight),
101            Self::Roxmltree => parse_with_roxmltree(input, options, preflight),
102            Self::Differential => parse_differentially(input, options, preflight),
103        }
104    }
105
106    #[cfg(feature = "xmldsig")]
107    fn parse_in<'input, 'w, W: structured_asn1::memory::Work>(
108        self,
109        input: &'input str,
110        options: ParsingOptions,
111        preflight: &LexicalPreflight<'input, storage::Accounted<'w, W>>,
112        storage: storage::Accounted<'w, W>,
113    ) -> Result<Document<'input, storage::Accounted<'w, W>>, AccountedParseError> {
114        match self {
115            Self::Xmloxide => {
116                #[cfg(feature = "xml-backend-xmloxide")]
117                return xmloxide::XmloxideBackend::parse_in(input, options, preflight, storage);
118                #[cfg(not(feature = "xml-backend-xmloxide"))]
119                Err(ParseError::BackendUnavailable { backend: self }.into())
120            }
121            Self::Roxmltree => {
122                #[cfg(feature = "xml-backend-roxmltree")]
123                return roxmltree::RoxmltreeBackend::parse_in(input, options, preflight, storage);
124                #[cfg(not(feature = "xml-backend-roxmltree"))]
125                Err(ParseError::BackendUnavailable { backend: self }.into())
126            }
127            Self::Differential => {
128                #[cfg(all(feature = "xml-backend-xmloxide", feature = "xml-backend-roxmltree"))]
129                return differential::parse_in(input, options, preflight, storage);
130                #[cfg(not(all(
131                    feature = "xml-backend-xmloxide",
132                    feature = "xml-backend-roxmltree"
133                )))]
134                Err(ParseError::BackendUnavailable { backend: self }.into())
135            }
136        }
137    }
138}
139
140fn parse_with_xmloxide<'input>(
141    input: &'input str,
142    options: ParsingOptions,
143    preflight: &LexicalPreflight,
144) -> Result<Document<'input>, ParseError> {
145    #[cfg(feature = "xml-backend-xmloxide")]
146    return xmloxide::XmloxideBackend::parse(input, options, preflight);
147    #[cfg(not(feature = "xml-backend-xmloxide"))]
148    {
149        let _ = (input, options, preflight);
150        Err(ParseError::BackendUnavailable {
151            backend: XmlBackend::Xmloxide,
152        })
153    }
154}
155
156fn parse_with_roxmltree<'input>(
157    input: &'input str,
158    options: ParsingOptions,
159    preflight: &LexicalPreflight,
160) -> Result<Document<'input>, ParseError> {
161    #[cfg(feature = "xml-backend-roxmltree")]
162    return roxmltree::RoxmltreeBackend::parse(input, options, preflight);
163    #[cfg(not(feature = "xml-backend-roxmltree"))]
164    {
165        let _ = (input, options, preflight);
166        Err(ParseError::BackendUnavailable {
167            backend: XmlBackend::Roxmltree,
168        })
169    }
170}
171
172fn parse_differentially<'input>(
173    input: &'input str,
174    options: ParsingOptions,
175    preflight: &LexicalPreflight,
176) -> Result<Document<'input>, ParseError> {
177    #[cfg(all(feature = "xml-backend-xmloxide", feature = "xml-backend-roxmltree"))]
178    return differential::DifferentialBackend::parse(input, options, preflight);
179    #[cfg(not(all(feature = "xml-backend-xmloxide", feature = "xml-backend-roxmltree")))]
180    {
181        let _ = (input, options, preflight);
182        Err(ParseError::BackendUnavailable {
183            backend: XmlBackend::Differential,
184        })
185    }
186}
187
188impl Default for XmlBackend {
189    fn default() -> Self {
190        Self::build_default()
191    }
192}
193
194/// Parser failure retaining labels in their admitted allocation account.
195///
196/// This is error transport for scoped parsing, not a different XML grammar.
197#[cfg(feature = "xmldsig")]
198#[derive(Debug, thiserror::Error)]
199pub enum AccountedParseError {
200    /// Backend-neutral structural or resource refusal.
201    #[error("{0}")]
202    Semantic(#[from] ParseError),
203    /// Original lexical failure with its completed, paid diagnostic labels.
204    #[error("{0}")]
205    Lexical(
206        #[source]
207        crate::xml_input::lexical::Error<
208            structured_asn1::memory::OwnedText,
209            structured_asn1::memory::MemoryError,
210        >,
211    ),
212    /// Original roxmltree failure with its completed, paid diagnostic labels.
213    #[cfg(feature = "xml-backend-roxmltree")]
214    #[error("{0}")]
215    Roxmltree(#[source] crate::parser_roxmltree::Error<structured_asn1::memory::OwnedText>),
216    /// Original xmloxide diagnostics retain their actual allocation charges.
217    #[cfg(feature = "xml-backend-xmloxide")]
218    #[error("{0}")]
219    Xmloxide(
220        #[source]
221        crate::parser_xmloxide::error::ParseError<
222            structured_asn1::memory::OwnedText,
223            structured_asn1::memory::OwnedVec<
224                crate::parser_xmloxide::error::ParseDiagnostic<structured_asn1::memory::OwnedText>,
225            >,
226        >,
227    ),
228    /// Semantic disagreement with its completed, paid diagnostic.
229    #[error("XML backend divergence: {0}")]
230    Divergence(structured_asn1::memory::OwnedText),
231}
232
233/// Stable parser-neutral XML parse error.
234#[derive(Clone, Debug, PartialEq, Eq)]
235pub enum ParseError {
236    /// The semantic tree could not acquire its requested allocation.
237    AllocationFailed,
238    /// The current query refused tree construction work or storage.
239    #[cfg(feature = "xmldsig")]
240    SharedExecution(structured_asn1::memory::MemoryError),
241    /// The requested parser implementation was not compiled into this build.
242    BackendUnavailable {
243        /// Runtime selection that cannot be satisfied.
244        backend: XmlBackend,
245    },
246    /// The absolute source-document byte ceiling was exceeded.
247    ByteLimitReached {
248        /// Maximum accepted UTF-8 source length.
249        maximum: usize,
250        /// Source length presented by the caller.
251        actual: usize,
252    },
253    /// A DTD was found while DTD processing was disabled.
254    DtdDetected,
255    /// The retained semantic node ceiling was exceeded.
256    NodesLimitReached,
257    /// The absolute general-entity substitution ceiling was exceeded.
258    EntityExpansionLimitReached {
259        /// Maximum substitutions accepted for one document.
260        maximum: u32,
261        /// First substitution beyond the ceiling.
262        actual: u32,
263    },
264    /// The absolute entity replacement-work ceiling was exceeded.
265    EntityExpansionWorkLimitReached {
266        /// Maximum replacement bytes traversed for one document.
267        maximum: usize,
268        /// First cumulative replacement size beyond the ceiling.
269        actual: usize,
270    },
271    /// The source-position sidecar reached its absolute allocation ceiling.
272    SourcePositionLimitReached {
273        /// Maximum lexical positions retained for one document.
274        maximum: usize,
275        /// First lexical position beyond the ceiling.
276        actual: usize,
277    },
278    /// The absolute XML element nesting ceiling was exceeded.
279    DepthLimitReached {
280        /// Maximum accepted element depth.
281        maximum: usize,
282        /// First observed depth beyond the ceiling.
283        actual: usize,
284    },
285    /// The absolute in-scope namespace-binding ceiling was exceeded.
286    NamespaceBindingLimitReached {
287        /// Maximum accepted user-declared bindings in scope at once.
288        maximum: usize,
289        /// First in-scope binding count beyond the ceiling.
290        actual: usize,
291    },
292    /// A backend node does not match the original lexical source sidecar.
293    SourceMapMismatch {
294        /// Expected fixed lexical node kind.
295        expected: &'static str,
296        /// Observed fixed lexical node kind, or end of stream.
297        found: &'static str,
298    },
299    /// The selected backend rejected malformed XML.
300    Backend {
301        /// Compile-time selected backend name.
302        backend: &'static str,
303        /// Backend diagnostic retained for troubleshooting.
304        message: String,
305    },
306    /// The two parsers produced different retained XML semantics.
307    BackendDivergence {
308        /// Bounded diagnostic identifying the divergent semantic component.
309        reason: String,
310    },
311}
312
313#[cfg(feature = "xmldsig")]
314impl From<structured_asn1::memory::MemoryError> for ParseError {
315    fn from(error: structured_asn1::memory::MemoryError) -> Self {
316        Self::SharedExecution(error)
317    }
318}
319
320impl fmt::Display for ParseError {
321    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
322        match self {
323            Self::AllocationFailed => formatter.write_str("XML tree allocation failed"),
324            #[cfg(feature = "xmldsig")]
325            Self::SharedExecution(error) => write!(formatter, "XML tree execution: {error}"),
326            Self::BackendUnavailable { backend } => {
327                write!(
328                    formatter,
329                    "XML backend {backend:?} is not compiled into this build"
330                )
331            }
332            Self::ByteLimitReached { maximum, actual } => {
333                write!(
334                    formatter,
335                    "XML byte limit reached: maximum {maximum}, actual {actual}"
336                )
337            }
338            Self::DtdDetected => formatter.write_str("DTD detected"),
339            Self::NodesLimitReached => formatter.write_str("nodes limit reached"),
340            Self::EntityExpansionLimitReached { maximum, actual } => write!(
341                formatter,
342                "XML entity expansion limit {maximum} exceeded at expansion {actual}"
343            ),
344            Self::EntityExpansionWorkLimitReached { maximum, actual } => write!(
345                formatter,
346                "XML entity expansion-work limit {maximum} bytes exceeded at {actual} bytes"
347            ),
348            Self::SourcePositionLimitReached { maximum, actual } => write!(
349                formatter,
350                "XML source-position limit {maximum} exceeded at position {actual}"
351            ),
352            Self::DepthLimitReached { maximum, actual } => {
353                write!(
354                    formatter,
355                    "XML depth limit {maximum} exceeded at depth {actual}"
356                )
357            }
358            Self::NamespaceBindingLimitReached { maximum, actual } => write!(
359                formatter,
360                "XML namespace binding limit {maximum} exceeded at {actual} active bindings"
361            ),
362            Self::SourceMapMismatch { expected, found } => write!(
363                formatter,
364                "xmloxide-source-map rejected XML: expected {expected}, found {found}"
365            ),
366            Self::Backend { backend, message } => {
367                write!(formatter, "{backend} rejected XML: {message}")
368            }
369            Self::BackendDivergence { reason } => {
370                write!(formatter, "XML backend semantic divergence: {reason}")
371            }
372        }
373    }
374}
375
376impl std::error::Error for ParseError {}
377
378trait XmlBackendImplementation {
379    fn parse<'input>(
380        input: &'input str,
381        options: ParsingOptions,
382        preflight: &LexicalPreflight,
383    ) -> Result<Document<'input>, ParseError>;
384}
385
386/// Stable parser-independent identity of a semantic tree node.
387#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
388pub(crate) struct SemanticNodeId(u32);
389
390impl SemanticNodeId {
391    pub(super) const fn new(raw: u32) -> Self {
392        Self(raw)
393    }
394    pub(super) const fn raw(self) -> u32 {
395        self.0
396    }
397}
398
399/// Identity portion of the semantic contract used by owned documents.
400pub(crate) trait SemanticDocument {
401    type Node<'a>: Copy
402    where
403        Self: 'a;
404    fn node(&self, id: SemanticNodeId) -> Option<Self::Node<'_>>;
405    fn node_id<'a>(&'a self, node: Self::Node<'a>) -> SemanticNodeId;
406}
407
408impl SemanticDocument for Document<'_> {
409    type Node<'a>
410        = Node<'a, 'a>
411    where
412        Self: 'a;
413
414    fn node(&self, id: SemanticNodeId) -> Option<Self::Node<'_>> {
415        self.get_node(NodeId::from(id.raw()))
416    }
417
418    fn node_id<'a>(&'a self, node: Self::Node<'a>) -> SemanticNodeId {
419        SemanticNodeId::new(node.id().get())
420    }
421}