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}