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}