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;
14mod tree;
15#[cfg(feature = "xml-backend-xmloxide")]
16mod xmloxide;
17
18use std::fmt;
19
20use self::preflight::LexicalPreflight;
21
22pub use tree::{
23    Ancestors, Attribute, Attributes, Children, Descendants, Document, ExpandedName, Namespace,
24    Namespaces, Node, NodeId, NodeType, PI, XmlIdError,
25};
26
27/// Parser-neutral options used after bounded lexical preflight.
28#[derive(Clone, Copy, Debug)]
29pub struct ParsingOptions {
30    /// Whether an internal DTD subset is accepted.
31    pub allow_dtd: bool,
32    /// Maximum retained semantic node count.
33    pub nodes_limit: u32,
34}
35
36impl Default for ParsingOptions {
37    fn default() -> Self {
38        Self {
39            allow_dtd: false,
40            nodes_limit: u32::MAX,
41        }
42    }
43}
44
45/// XML parser implementation selected for one document or operation.
46///
47/// Cargo features control which implementations are compiled. Selecting an
48/// implementation absent from a thin build fails explicitly; no parser
49/// fallback is performed.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum XmlBackend {
52    /// Parse with the xmloxide adapter.
53    Xmloxide,
54    /// Parse with the roxmltree adapter.
55    Roxmltree,
56    /// Parse with both adapters and fail closed unless their semantic arenas agree.
57    Differential,
58}
59
60impl XmlBackend {
61    pub(crate) const fn build_default() -> Self {
62        if cfg!(feature = "xml-backend-differential") {
63            Self::Differential
64        } else if cfg!(feature = "xml-backend-xmloxide") {
65            Self::Xmloxide
66        } else {
67            Self::Roxmltree
68        }
69    }
70
71    /// Returns whether this backend can run in the current build.
72    #[must_use]
73    pub const fn is_available(self) -> bool {
74        match self {
75            Self::Xmloxide => cfg!(feature = "xml-backend-xmloxide"),
76            Self::Roxmltree => cfg!(feature = "xml-backend-roxmltree"),
77            Self::Differential => cfg!(all(
78                feature = "xml-backend-xmloxide",
79                feature = "xml-backend-roxmltree"
80            )),
81        }
82    }
83
84    /// Iterate over every runtime mode available in this build.
85    pub fn available() -> impl Iterator<Item = Self> {
86        [Self::Xmloxide, Self::Roxmltree, Self::Differential]
87            .into_iter()
88            .filter(|backend| backend.is_available())
89    }
90
91    fn parse<'input>(
92        self,
93        input: &'input str,
94        options: ParsingOptions,
95        preflight: &LexicalPreflight,
96    ) -> Result<Document<'input>, ParseError> {
97        match self {
98            Self::Xmloxide => parse_with_xmloxide(input, options, preflight),
99            Self::Roxmltree => parse_with_roxmltree(input, options, preflight),
100            Self::Differential => parse_differentially(input, options, preflight),
101        }
102    }
103}
104
105fn parse_with_xmloxide<'input>(
106    input: &'input str,
107    options: ParsingOptions,
108    preflight: &LexicalPreflight,
109) -> Result<Document<'input>, ParseError> {
110    #[cfg(feature = "xml-backend-xmloxide")]
111    return xmloxide::XmloxideBackend::parse(input, options, preflight);
112    #[cfg(not(feature = "xml-backend-xmloxide"))]
113    {
114        let _ = (input, options, preflight);
115        Err(ParseError::BackendUnavailable {
116            backend: XmlBackend::Xmloxide,
117        })
118    }
119}
120
121fn parse_with_roxmltree<'input>(
122    input: &'input str,
123    options: ParsingOptions,
124    preflight: &LexicalPreflight,
125) -> Result<Document<'input>, ParseError> {
126    #[cfg(feature = "xml-backend-roxmltree")]
127    return roxmltree::RoxmltreeBackend::parse(input, options, preflight);
128    #[cfg(not(feature = "xml-backend-roxmltree"))]
129    {
130        let _ = (input, options, preflight);
131        Err(ParseError::BackendUnavailable {
132            backend: XmlBackend::Roxmltree,
133        })
134    }
135}
136
137fn parse_differentially<'input>(
138    input: &'input str,
139    options: ParsingOptions,
140    preflight: &LexicalPreflight,
141) -> Result<Document<'input>, ParseError> {
142    #[cfg(all(feature = "xml-backend-xmloxide", feature = "xml-backend-roxmltree"))]
143    return differential::DifferentialBackend::parse(input, options, preflight);
144    #[cfg(not(all(feature = "xml-backend-xmloxide", feature = "xml-backend-roxmltree")))]
145    {
146        let _ = (input, options, preflight);
147        Err(ParseError::BackendUnavailable {
148            backend: XmlBackend::Differential,
149        })
150    }
151}
152
153impl Default for XmlBackend {
154    fn default() -> Self {
155        Self::build_default()
156    }
157}
158
159/// Stable parser-neutral XML parse error.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub enum ParseError {
162    /// The requested parser implementation was not compiled into this build.
163    BackendUnavailable {
164        /// Runtime selection that cannot be satisfied.
165        backend: XmlBackend,
166    },
167    /// The absolute source-document byte ceiling was exceeded.
168    ByteLimitReached {
169        /// Maximum accepted UTF-8 source length.
170        maximum: usize,
171        /// Source length presented by the caller.
172        actual: usize,
173    },
174    /// A DTD was found while DTD processing was disabled.
175    DtdDetected,
176    /// The retained semantic node ceiling was exceeded.
177    NodesLimitReached,
178    /// The absolute general-entity substitution ceiling was exceeded.
179    EntityExpansionLimitReached {
180        /// Maximum substitutions accepted for one document.
181        maximum: u32,
182        /// First substitution beyond the ceiling.
183        actual: u32,
184    },
185    /// The absolute entity replacement-work ceiling was exceeded.
186    EntityExpansionWorkLimitReached {
187        /// Maximum replacement bytes traversed for one document.
188        maximum: usize,
189        /// First cumulative replacement size beyond the ceiling.
190        actual: usize,
191    },
192    /// The source-position sidecar reached its absolute allocation ceiling.
193    SourcePositionLimitReached {
194        /// Maximum lexical positions retained for one document.
195        maximum: usize,
196        /// First lexical position beyond the ceiling.
197        actual: usize,
198    },
199    /// The absolute XML element nesting ceiling was exceeded.
200    DepthLimitReached {
201        /// Maximum accepted element depth.
202        maximum: usize,
203        /// First observed depth beyond the ceiling.
204        actual: usize,
205    },
206    /// The absolute in-scope namespace-binding ceiling was exceeded.
207    NamespaceBindingLimitReached {
208        /// Maximum accepted user-declared bindings in scope at once.
209        maximum: usize,
210        /// First in-scope binding count beyond the ceiling.
211        actual: usize,
212    },
213    /// The selected backend rejected malformed XML.
214    Backend {
215        /// Compile-time selected backend name.
216        backend: &'static str,
217        /// Backend diagnostic retained for troubleshooting.
218        message: String,
219    },
220    /// The two parsers produced different retained XML semantics.
221    BackendDivergence {
222        /// Bounded diagnostic identifying the divergent semantic component.
223        reason: String,
224    },
225}
226
227impl fmt::Display for ParseError {
228    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match self {
230            Self::BackendUnavailable { backend } => {
231                write!(
232                    formatter,
233                    "XML backend {backend:?} is not compiled into this build"
234                )
235            }
236            Self::ByteLimitReached { maximum, actual } => {
237                write!(
238                    formatter,
239                    "XML byte limit reached: maximum {maximum}, actual {actual}"
240                )
241            }
242            Self::DtdDetected => formatter.write_str("DTD detected"),
243            Self::NodesLimitReached => formatter.write_str("nodes limit reached"),
244            Self::EntityExpansionLimitReached { maximum, actual } => write!(
245                formatter,
246                "XML entity expansion limit {maximum} exceeded at expansion {actual}"
247            ),
248            Self::EntityExpansionWorkLimitReached { maximum, actual } => write!(
249                formatter,
250                "XML entity expansion-work limit {maximum} bytes exceeded at {actual} bytes"
251            ),
252            Self::SourcePositionLimitReached { maximum, actual } => write!(
253                formatter,
254                "XML source-position limit {maximum} exceeded at position {actual}"
255            ),
256            Self::DepthLimitReached { maximum, actual } => {
257                write!(
258                    formatter,
259                    "XML depth limit {maximum} exceeded at depth {actual}"
260                )
261            }
262            Self::NamespaceBindingLimitReached { maximum, actual } => write!(
263                formatter,
264                "XML namespace binding limit {maximum} exceeded at {actual} active bindings"
265            ),
266            Self::Backend { backend, message } => {
267                write!(formatter, "{backend} rejected XML: {message}")
268            }
269            Self::BackendDivergence { reason } => {
270                write!(formatter, "XML backend semantic divergence: {reason}")
271            }
272        }
273    }
274}
275
276impl std::error::Error for ParseError {}
277
278trait XmlBackendImplementation {
279    fn parse<'input>(
280        input: &'input str,
281        options: ParsingOptions,
282        preflight: &LexicalPreflight,
283    ) -> Result<Document<'input>, ParseError>;
284}
285
286/// Stable parser-independent identity of a semantic tree node.
287#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
288pub(crate) struct SemanticNodeId(u32);
289
290impl SemanticNodeId {
291    pub(super) const fn new(raw: u32) -> Self {
292        Self(raw)
293    }
294    pub(super) const fn raw(self) -> u32 {
295        self.0
296    }
297}
298
299/// Identity portion of the semantic contract used by owned documents.
300pub(crate) trait SemanticDocument {
301    type Node<'a>: Copy
302    where
303        Self: 'a;
304    fn node(&self, id: SemanticNodeId) -> Option<Self::Node<'_>>;
305    fn node_id<'a>(&'a self, node: Self::Node<'a>) -> SemanticNodeId;
306}
307
308impl SemanticDocument for Document<'_> {
309    type Node<'a>
310        = Node<'a, 'a>
311    where
312        Self: 'a;
313
314    fn node(&self, id: SemanticNodeId) -> Option<Self::Node<'_>> {
315        self.get_node(NodeId::from(id.raw()))
316    }
317
318    fn node_id<'a>(&'a self, node: Self::Node<'a>) -> SemanticNodeId {
319        SemanticNodeId::new(node.id().get())
320    }
321}