Skip to main content

structured_ltv/
source.rs

1//! Original source identity shared by every long-term validation fact.
2
3use structured_asn1::memory::{HashWork, MemoryError, Work};
4
5pub use structured_asn1::source::Snapshot;
6
7/// One original node in its exact source generation.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct Location<N, D, G> {
10    /// Exact original document and generation.
11    pub snapshot: Snapshot<D, G>,
12    /// Original node, such as an XML element or a CMS attribute.
13    pub node: N,
14}
15
16/// A source owner's stable identities, independent of its document model.
17///
18/// An XML owner implements it over element identities, a CMS or PDF owner over
19/// its own attribute, object or revision identities. Nothing here reads the
20/// source; long-term validation only compares identities and generations.
21pub trait Source {
22    /// Stable node identity scoped to this document, independent of a borrow.
23    /// Its trusted `HashWork` bound covers all hashing/equality bytes so identity
24    /// indexes can admit their complete work before querying attacker input.
25    type Id: Copy + Eq + HashWork;
26    /// Stable document identity.
27    type Document: Copy + Eq;
28    /// Exact document generation.
29    type Generation: Copy + Eq;
30
31    /// Returns identity/generation in constant time, without allocation or I/O.
32    fn snapshot(&self) -> Snapshot<Self::Document, Self::Generation>;
33}
34
35/// The expected snapshot changed or the current operation refused admission.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ScopeError<N> {
38    /// Document identity or generation changed; previous evidence is unusable.
39    Changed {
40        /// Node associated with the attempted operation.
41        node: N,
42    },
43    /// Admission failed before the operation.
44    Resource {
45        /// Original node associated with the attempted operation.
46        node: N,
47        /// Original execution refusal.
48        cause: MemoryError,
49    },
50}
51
52/// One current query over one expected source snapshot.
53///
54/// Every long-term validation check runs under the query that performs it:
55/// retained facts are never prepaid work. A format layer implements this over
56/// its own access guard, so its checks and these share one admission path;
57/// [`Scope`] implements it over any [`Source`].
58pub trait Query {
59    /// Original node identity.
60    type Id: Copy + Eq + HashWork;
61    /// Stable document identity.
62    type Document: Copy + Eq;
63    /// Exact document generation.
64    type Generation: Copy + Eq;
65    /// Current execution funding this query.
66    type Work: Work;
67
68    /// The expected source snapshot of every fact checked in this query.
69    fn snapshot(&self) -> Snapshot<Self::Document, Self::Generation>;
70
71    /// The current execution; storage and work for results are admitted here.
72    fn execution(&self) -> &Self::Work;
73
74    /// Requires the live source at the expected snapshot, admits `units` of
75    /// work, and requires the snapshot again.
76    fn admit(&self, node: Self::Id, units: u64) -> Result<(), ScopeError<Self::Id>>;
77
78    /// Compares two identities after admitting their complete `HashWork`.
79    fn equal(&self, left: Self::Id, right: Self::Id) -> Result<bool, MemoryError> {
80        equal_id(left, right, self.execution())
81    }
82}
83
84/// The query of a [`Source`] that has no access guard of its own.
85///
86/// No clone, allocation or reference-count increment is needed to construct
87/// it.
88pub struct Scope<'a, S: Source, W: Work> {
89    source: &'a S,
90    snapshot: Snapshot<S::Document, S::Generation>,
91    execution: &'a W,
92}
93
94impl<'a, S: Source, W: Work> Scope<'a, S, W> {
95    /// Attaches the expected original snapshot and current operation.
96    #[must_use]
97    pub fn new(
98        source: &'a S,
99        snapshot: Snapshot<S::Document, S::Generation>,
100        execution: &'a W,
101    ) -> Self {
102        Self {
103            source,
104            snapshot,
105            execution,
106        }
107    }
108}
109
110impl<S: Source, W: Work> Query for Scope<'_, S, W> {
111    type Id = S::Id;
112    type Document = S::Document;
113    type Generation = S::Generation;
114    type Work = W;
115
116    fn snapshot(&self) -> Snapshot<S::Document, S::Generation> {
117        self.snapshot
118    }
119
120    fn execution(&self) -> &W {
121        self.execution
122    }
123
124    fn admit(&self, node: S::Id, units: u64) -> Result<(), ScopeError<S::Id>> {
125        if self.source.snapshot() != self.snapshot {
126            return Err(ScopeError::Changed { node });
127        }
128        self.execution
129            .charge_work(units)
130            .map_err(|cause| ScopeError::Resource { node, cause })?;
131        if self.source.snapshot() != self.snapshot {
132            return Err(ScopeError::Changed { node });
133        }
134        Ok(())
135    }
136}
137
138/// Compares two identities after admitting their complete `HashWork`.
139pub fn equal_id<N: Copy + Eq + HashWork, W: Work>(
140    left: N,
141    right: N,
142    execution: &W,
143) -> Result<bool, MemoryError> {
144    let cost = left
145        .hash_work()?
146        .checked_add(right.hash_work()?)
147        .ok_or(MemoryError::WorkExceeded)?;
148    execution.charge_work(cost)?;
149    Ok(left == right)
150}