Skip to main content

structured_xades/
xml.rs

1//! Scoped XML access without retaining DOM or callback-local string borrows.
2
3use structured_asn1::memory::{HashWork, MemoryError, Work};
4
5/// Original document identity and the exact immutable view generation.
6///
7/// A generation change invalidates earlier selection and reference evidence;
8/// it is a processing failure, never a malformed-property diagnostic.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Snapshot<D, G> {
11    /// Stable identity supplied by the XML owner, not a document fingerprint.
12    pub document: D,
13    /// Generation under which all selected nodes and evidence were obtained.
14    pub generation: G,
15}
16
17/// An expanded XML name; prefix spelling does not participate in identity.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Name<'a> {
20    /// Namespace name, empty for an unqualified name.
21    pub namespace: &'a str,
22    /// Local name after the XML owner's namespace processing.
23    pub local: &'a str,
24}
25
26/// One callback-local XML node view, preserving text-node distinctions.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Node<'a> {
29    /// An element's expanded name.
30    Element(Name<'a>),
31    /// XML-normalized character data, not original encoded document bytes.
32    Text(&'a str),
33    /// A comment, processing instruction or other non-element node.
34    Other,
35}
36
37/// The XML owner's stable, snapshot-checked access boundary.
38///
39/// Every method must reject a foreign node and a stale snapshot before access.
40/// The owner admits its complete data-dependent scans and allocations through
41/// `execution` and its own compiled XML policy before doing that work. Common
42/// dispatch admission does not prove accounting inside arbitrary adapter code.
43/// Access must not implicitly reparse, fetch a resource or change the document.
44///
45/// The higher-ranked visitors cannot retain view-local names/text in their
46/// return value. An adapter can therefore resolve nodes inside a temporary
47/// document view, without exposing its DOM types or extending that view's life.
48pub trait Xml {
49    /// Stable node identity scoped to this document, independent of a DOM borrow.
50    /// Its trusted `HashWork` bound covers all hashing/equality bytes so identity
51    /// indexes can admit their complete work before querying attacker input.
52    type Id: Copy + Eq + HashWork;
53    /// Stable document identity.
54    type Document: Copy + Eq;
55    /// Exact document generation.
56    type Generation: Copy + Eq;
57    /// Original XML processing/policy/resource failure.
58    type Error;
59
60    /// Returns identity/generation in constant time, without allocation or I/O.
61    fn snapshot(&self) -> Snapshot<Self::Document, Self::Generation>;
62
63    /// Inspects one original node within the expected snapshot.
64    fn inspect<R, W: Work>(
65        &self,
66        snapshot: Snapshot<Self::Document, Self::Generation>,
67        node: Self::Id,
68        execution: &W,
69        visitor: impl for<'view> FnOnce(Node<'view>) -> R,
70    ) -> Result<R, Self::Error>;
71
72    /// Reads an attribute by expanded name, preserving absence versus empty.
73    ///
74    /// Generic XML attribute normalization and namespace rules belong to the
75    /// owner. Unqualified attributes use the empty namespace, regardless of an
76    /// element's default namespace. Returned text is XML-normalized lexical data.
77    fn attribute<R, W: Work>(
78        &self,
79        snapshot: Snapshot<Self::Document, Self::Generation>,
80        node: Self::Id,
81        name: Name<'_>,
82        execution: &W,
83        visitor: impl for<'view> FnOnce(Option<&'view str>) -> R,
84    ) -> Result<R, Self::Error>;
85
86    /// Returns the first direct child, including text and non-element children.
87    fn first_child<W: Work>(
88        &self,
89        snapshot: Snapshot<Self::Document, Self::Generation>,
90        parent: Self::Id,
91        execution: &W,
92    ) -> Result<Option<Self::Id>, Self::Error>;
93
94    /// Returns the next direct sibling in original document order.
95    ///
96    /// `parent` pins the traversal boundary; the owner rejects a node outside
97    /// that parent's child list rather than continuing in another subtree.
98    fn next_sibling<W: Work>(
99        &self,
100        snapshot: Snapshot<Self::Document, Self::Generation>,
101        parent: Self::Id,
102        node: Self::Id,
103        execution: &W,
104    ) -> Result<Option<Self::Id>, Self::Error>;
105}
106
107/// An XML access refusal located by original node identity, not a fake byte offset.
108#[derive(Debug)]
109pub enum AccessError<E, N> {
110    /// The XML owner's original cause.
111    Xml(E),
112    /// Common admission failed before callback dispatch.
113    Resource {
114        /// Original node associated with the attempted operation.
115        node: N,
116        /// Original execution refusal.
117        cause: MemoryError,
118    },
119    /// Document identity or generation changed; previous evidence is unusable.
120    Changed {
121        /// Node associated with the attempted operation.
122        node: N,
123    },
124}
125
126/// Result retaining the selected XML adapter's cause and original node identity.
127pub type AccessResult<T, X> = Result<T, AccessError<<X as Xml>::Error, <X as Xml>::Id>>;
128
129/// Failure to traverse an element-only property container.
130#[derive(Debug)]
131pub enum ChildError<E, N> {
132    /// Original access, generation or execution refusal.
133    Access(AccessError<E, N>),
134    /// Character data is not XML whitespace in an element-only container.
135    Text {
136        /// Original text-node identity; no document byte position is invented.
137        node: N,
138    },
139}
140
141/// One snapshot and current operation shared by all scoped XML queries.
142///
143/// This guard checks the snapshot before and after every successful callback.
144/// The adapter must also check at actual access, so these checks do not replace
145/// owner synchronization or immutable-view guarantees. No clone, allocation or
146/// reference-count increment is required to construct the guard.
147pub struct Access<'a, X: Xml, W: Work> {
148    xml: &'a X,
149    snapshot: Snapshot<X::Document, X::Generation>,
150    execution: &'a W,
151}
152
153impl<'a, X: Xml, W: Work> Access<'a, X, W> {
154    /// Attaches the expected original snapshot and current operation.
155    #[must_use]
156    pub fn new(
157        xml: &'a X,
158        snapshot: Snapshot<X::Document, X::Generation>,
159        execution: &'a W,
160    ) -> Self {
161        Self {
162            xml,
163            snapshot,
164            execution,
165        }
166    }
167
168    /// The source snapshot associated with node locations and selection evidence.
169    #[must_use]
170    pub fn snapshot(&self) -> Snapshot<X::Document, X::Generation> {
171        self.snapshot
172    }
173
174    /// Borrows the actual current execution used by this access boundary.
175    /// Provider/resolver lexical scans and owned results must be admitted here,
176    /// rather than on an earlier source/storage account. No clone, allocation
177    /// or account/reference-count change occurs. The borrow cannot outlive this
178    /// access handle and does not expose the XML owner's internal DOM.
179    #[must_use]
180    pub fn execution(&self) -> &W {
181        self.execution
182    }
183
184    pub(crate) fn owner(&self) -> &'a X {
185        self.xml
186    }
187
188    /// Traverses direct element children, skipping comments and XML whitespace.
189    ///
190    /// Character data is rejected rather than silently dropped. Each skipped
191    /// node is admitted and checked in the same operation, so an empty result
192    /// cannot bypass a large whitespace/comment fan-out. Construction allocates
193    /// nothing. The first error is returned once, then iteration is fused.
194    #[must_use]
195    pub fn children(&self, parent: X::Id) -> Children<'_, 'a, X, W> {
196        Children {
197            access: self,
198            parent,
199            previous: None,
200            started: false,
201            done: false,
202        }
203    }
204
205    fn unchanged<E>(&self, node: X::Id) -> Result<(), AccessError<E, X::Id>> {
206        if self.xml.snapshot() != self.snapshot {
207            return Err(AccessError::Changed { node });
208        }
209        Ok(())
210    }
211
212    fn call<R, E>(
213        &self,
214        node: X::Id,
215        callback: impl FnOnce() -> Result<R, E>,
216    ) -> Result<R, AccessError<E, X::Id>> {
217        self.unchanged(node)?;
218        self.execution
219            .charge_work(1)
220            .map_err(|cause| AccessError::Resource { node, cause })?;
221        let result = callback().map_err(AccessError::Xml)?;
222        self.unchanged(node)?;
223        Ok(result)
224    }
225
226    pub(crate) fn query<R>(
227        &self,
228        node: X::Id,
229        callback: impl FnOnce(&X, Snapshot<X::Document, X::Generation>, &W) -> Result<R, X::Error>,
230    ) -> AccessResult<R, X> {
231        self.call(node, || callback(self.xml, self.snapshot, self.execution))
232    }
233
234    // Same dispatch/generation/admission core, preserving visitor errors before
235    // the post-success generation check. No side channel or error substitution.
236    pub(crate) fn query_fallible<R, E>(
237        &self,
238        node: X::Id,
239        callback: impl FnOnce(&X, Snapshot<X::Document, X::Generation>, &W) -> Result<R, E>,
240    ) -> Result<R, AccessError<E, X::Id>> {
241        self.call(node, || callback(self.xml, self.snapshot, self.execution))
242    }
243
244    pub(crate) fn equal(&self, left: &str, right: &str) -> Result<bool, MemoryError> {
245        equal_text(left, right, self.execution)
246    }
247
248    pub(crate) fn equal_id(&self, left: X::Id, right: X::Id) -> Result<bool, MemoryError> {
249        equal_id(left, right, self.execution)
250    }
251
252    pub(crate) fn trim_xml_whitespace<'value>(
253        &self,
254        value: &'value str,
255    ) -> Result<&'value str, MemoryError> {
256        self.execution
257            .charge_work(u64::try_from(value.len()).map_err(|_| MemoryError::WorkExceeded)?)?;
258        Ok(value.trim_matches([' ', '\t', '\r', '\n']))
259    }
260
261    /// Inspects one node without leaking callback-local XML borrows.
262    ///
263    /// ```compile_fail
264    /// use structured_xades::xml::{Access, AccessError, Node, Xml};
265    /// use structured_asn1::memory::Work;
266    /// fn escape<'a, X: Xml, W: Work>(access: &Access<'a, X, W>, node: X::Id)
267    ///     -> Result<Option<&'a str>, AccessError<X::Error, X::Id>>
268    /// {
269    ///     access.inspect(node, |value| match value {
270    ///         Node::Text(text) => Some(text),
271    ///         _ => None,
272    ///     })
273    /// }
274    /// ```
275    pub fn inspect<R>(
276        &self,
277        node: X::Id,
278        visitor: impl for<'view> FnOnce(Node<'view>) -> R,
279    ) -> AccessResult<R, X> {
280        self.call(node, || {
281            self.xml
282                .inspect(self.snapshot, node, self.execution, visitor)
283        })
284    }
285
286    /// Compares an element's expanded name with admission before each byte scan.
287    ///
288    /// A different node kind or name length needs no string scan. Namespace
289    /// names are compared exactly, without URI resolution or normalization.
290    pub fn has_name(&self, node: X::Id, expected: Name<'_>) -> AccessResult<bool, X> {
291        let compared = self.inspect(node, |value| {
292            let Node::Element(actual) = value else {
293                return Ok(false);
294            };
295            // Namespaces in XML 1.0 Third Edition §2.3: namespace-name identity
296            // is exact character-string comparison, not URI equivalence.
297            // https://www.w3.org/TR/2009/REC-xml-names-20091208/#ComparingURIReferences
298            if !equal_text(actual.namespace, expected.namespace, self.execution)? {
299                return Ok(false);
300            }
301            equal_text(actual.local, expected.local, self.execution)
302        })?;
303        compared.map_err(|cause| AccessError::Resource { node, cause })
304    }
305
306    /// Reads a normalized attribute, preserving absence versus an empty value.
307    pub fn attribute<R>(
308        &self,
309        node: X::Id,
310        name: Name<'_>,
311        visitor: impl for<'view> FnOnce(Option<&'view str>) -> R,
312    ) -> AccessResult<R, X> {
313        self.call(node, || {
314            self.xml
315                .attribute(self.snapshot, node, name, self.execution, visitor)
316        })
317    }
318
319    /// Begins a direct-child traversal, admitting even the empty-list query.
320    pub fn first_child(&self, parent: X::Id) -> AccessResult<Option<X::Id>, X> {
321        self.call(parent, || {
322            self.xml.first_child(self.snapshot, parent, self.execution)
323        })
324    }
325
326    /// Advances within the same direct-child list in original order.
327    pub fn next_sibling(&self, parent: X::Id, node: X::Id) -> AccessResult<Option<X::Id>, X> {
328        self.call(node, || {
329            self.xml
330                .next_sibling(self.snapshot, parent, node, self.execution)
331        })
332    }
333}
334
335pub(crate) fn equal_id<N: Copy + HashWork, W: Work>(
336    left: N,
337    right: N,
338    execution: &W,
339) -> Result<bool, MemoryError> {
340    let cost = left
341        .hash_work()?
342        .checked_add(right.hash_work()?)
343        .ok_or(MemoryError::WorkExceeded)?;
344    execution.charge_work(cost)?;
345    Ok(left == right)
346}
347
348/// An iterative, snapshot-checked direct-child traversal for property containers.
349pub struct Children<'a, 'xml, X: Xml, W: Work> {
350    access: &'a Access<'xml, X, W>,
351    parent: X::Id,
352    previous: Option<X::Id>,
353    started: bool,
354    done: bool,
355}
356
357impl<X: Xml, W: Work> Iterator for Children<'_, '_, X, W> {
358    type Item = Result<X::Id, ChildError<X::Error, X::Id>>;
359
360    fn next(&mut self) -> Option<Self::Item> {
361        if self.done {
362            return None;
363        }
364        loop {
365            let advanced = if self.started {
366                let Some(previous) = self.previous else {
367                    self.done = true;
368                    return None;
369                };
370                self.access.next_sibling(self.parent, previous)
371            } else {
372                self.started = true;
373                self.access.first_child(self.parent)
374            };
375            let node = match advanced {
376                Ok(Some(node)) => node,
377                Ok(None) => {
378                    self.done = true;
379                    return None;
380                }
381                Err(error) => {
382                    self.done = true;
383                    return Some(Err(ChildError::Access(error)));
384                }
385            };
386            self.previous = Some(node);
387            let inspected = self.access.inspect(node, |value| match value {
388                Node::Element(_) => Ok(Some(true)),
389                Node::Other => Ok(None),
390                Node::Text(text) => {
391                    let length =
392                        u64::try_from(text.len()).map_err(|_| MemoryError::WorkExceeded)?;
393                    self.access.execution.charge_work(length)?;
394                    // XML 1.0 Fifth Edition §2.3, production S: only these four
395                    // characters are XML whitespace, irrespective of Unicode.
396                    // https://www.w3.org/TR/2008/REC-xml-20081126/#NT-S
397                    for byte in text.bytes() {
398                        if !matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
399                            return Ok(Some(false));
400                        }
401                    }
402                    Ok(None)
403                }
404            });
405            match inspected {
406                Ok(Ok(Some(true))) => return Some(Ok(node)),
407                Ok(Ok(None)) => {}
408                Ok(Ok(Some(false))) => {
409                    self.done = true;
410                    return Some(Err(ChildError::Text { node }));
411                }
412                Ok(Err(cause)) => {
413                    self.done = true;
414                    return Some(Err(ChildError::Access(AccessError::Resource {
415                        node,
416                        cause,
417                    })));
418                }
419                Err(error) => {
420                    self.done = true;
421                    return Some(Err(ChildError::Access(error)));
422                }
423            }
424        }
425    }
426}
427
428impl<X: Xml, W: Work> core::iter::FusedIterator for Children<'_, '_, X, W> {}
429
430fn equal_text<W: Work>(left: &str, right: &str, execution: &W) -> Result<bool, MemoryError> {
431    if left.len() != right.len() {
432        return Ok(false);
433    }
434    let length = u64::try_from(left.len()).map_err(|_| MemoryError::WorkExceeded)?;
435    execution.charge_work(length)?;
436    Ok(left == right)
437}