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