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