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