Skip to main content

structured_cms/timestamp/
computed.rs

1//! Scoped computed imprints: alternatives and Merkle roots without double hashing.
2
3use super::{ImprintAgreement, ImprintError, MessageImprint};
4use crate::{Error, binding::DigestBinding, digest::DigestError, provider::Finished};
5use core::cell::{Cell, RefCell};
6use structured_asn1::{Reader, Span, digest::Resolution, memory::OwnedVec};
7use structured_pkix::algorithm::digest::AsnDigest;
8
9/// An input recipe executes under the token's one resolved digest identity.
10///
11/// A recipe can stream several normative alternatives together, or compute a
12/// reduced hash tree. It cannot construct an affirmative proof from bytes or
13/// a boolean: only this operation's completed states can be compared with the
14/// original declared hash. Provider errors cannot borrow temporary joined
15/// information. No identity/state/output cloning is required.
16pub trait ComputedInput<P, PE> {
17    /// Original source or recipe failure, independent of semantic disagreement.
18    type Error;
19
20    /// Complete the input, preserving refusal even when a callback ignores it.
21    fn compute<'context, 'reader, 'document>(
22        &mut self,
23        context: &'context ImprintContext<'reader, 'document, P, Self::Error>,
24    ) -> Result<ImprintProof<'context>, InputFailure<Self::Error>>
25    where
26        P: AsnDigest<'document, Error = PE>;
27}
28
29/// Original recipe failure or a marker whose cause the operation retains.
30#[derive(Debug)]
31pub enum InputFailure<E> {
32    /// Original source/recipe cause; no successful prefix is returned.
33    Source(E),
34    /// The context keeps the first processing/provider/acquisition refusal.
35    Refused(crate::stream::Refused),
36}
37impl<E> From<crate::stream::Refused> for InputFailure<E> {
38    fn from(value: crate::stream::Refused) -> Self {
39        Self::Refused(value)
40    }
41}
42
43/// Closed comparison receipt, tied to this non-zero-sized operation owner.
44///
45/// This is not a standalone timestamp or trust proof. It cannot outlive the
46/// computation or be substituted from another token/context.
47pub struct ImprintProof<'context> {
48    owner: &'context u8,
49    agreement: ImprintAgreement,
50}
51impl ImprintProof<'_> {
52    /// Actual completed comparison, for choosing among finished alternatives.
53    #[must_use]
54    pub fn agreement(&self) -> ImprintAgreement {
55        self.agreement
56    }
57}
58
59enum Expected<'a> {
60    Borrowed(&'a [u8]),
61    Joined(OwnedVec<u8>),
62}
63impl Expected<'_> {
64    fn bytes(&self) -> &[u8] {
65        match self {
66            Self::Borrowed(bytes) => bytes,
67            Self::Joined(bytes) => bytes,
68        }
69    }
70}
71
72/// One original imprint, one provider identity and the current producing work.
73///
74/// The context is constructed only after schema/session/width preflight. Its
75/// non-zero-sized owner distinguishes even otherwise identical operations. All state
76/// transitions retain the first refusal; no `RefCell` borrow crosses a callback.
77pub struct ImprintContext<'reader, 'document, P: AsnDigest<'document>, E> {
78    provider: &'document P,
79    identity: P::Identity,
80    reader: &'reader Reader<'document>,
81    expected: Expected<'document>,
82    field: &'static str,
83    span: Span,
84    owner: u8,
85    active: Cell<usize>,
86    abandoned: Cell<bool>,
87    failure: RefCell<Option<ImprintError<E, P::Error>>>,
88}
89
90/// One unfinished state of this operation's exact resolved identity.
91/// Dropping it unfinished prevents a successful computation, even if another
92/// alternative matched. Opaque provider state owns its admitted storage.
93pub struct ImprintState<'context, 'reader, 'document, P: AsnDigest<'document>, E> {
94    context: &'context ImprintContext<'reader, 'document, P, E>,
95    state: Option<P::State>,
96}
97
98/// The provider's own completed output, retaining its context and identity.
99/// Intermediate tree hashes can be borrowed without copying, re-resolving or
100/// reconstructing a state. There is no arbitrary-byte constructor.
101pub struct ImprintDigest<'context, 'reader, 'document, P: AsnDigest<'document>, E> {
102    context: &'context ImprintContext<'reader, 'document, P, E>,
103    output: P::Output,
104}
105impl<'document, P: AsnDigest<'document>, E> ImprintDigest<'_, '_, 'document, P, E> {
106    /// Complete output bytes, never a digest of a partial/refused state.
107    #[must_use]
108    pub fn as_bytes(&self) -> &[u8] {
109        self.output.as_ref()
110    }
111    /// Typed finalized output for existing common digest consumers.
112    #[must_use]
113    pub fn finished(&self) -> Finished<'_, P, P::Identity, P::Output> {
114        Finished::new(self.context.provider, &self.context.identity, &self.output)
115    }
116}
117
118impl<'reader, 'document, P: AsnDigest<'document>, E> ImprintContext<'reader, 'document, P, E> {
119    /// Original producing information reader and current operation ceilings.
120    #[must_use]
121    pub fn reader(&self) -> &Reader<'document> {
122        self.reader
123    }
124    /// Full selected-provider identity; output width is not an algorithm ID.
125    #[must_use]
126    pub fn identity(&self) -> &P::Identity {
127        &self.identity
128    }
129    fn failed(&self) -> bool {
130        self.failure.borrow().is_some()
131    }
132    fn refuse(&self, cause: ImprintError<E, P::Error>) -> crate::stream::Refused {
133        let mut failure = self.failure.borrow_mut();
134        if failure.is_none() {
135            *failure = Some(cause);
136        }
137        crate::stream::Refused
138    }
139    fn admit(&self) -> Result<(), crate::stream::Refused> {
140        if self.failed() {
141            return Err(crate::stream::Refused);
142        }
143        self.reader.charge(1).map_err(|cause| {
144            self.refuse(ImprintError::Processing(Error::encoding(cause, self.field)))
145        })
146    }
147    fn admit_pass(&self) -> Result<(), crate::stream::Refused> {
148        if self.failed() {
149            return Err(crate::stream::Refused);
150        }
151        self.reader.charge(1).map_err(|cause| {
152            self.refuse(ImprintError::Pass(crate::stream::StreamError::Processing(
153                Error::encoding(cause, self.field),
154            )))
155        })
156    }
157    /// Starts another candidate/intermediate hash under this SAME identity.
158    pub fn start(
159        &self,
160    ) -> Result<ImprintState<'_, 'reader, 'document, P, E>, crate::stream::Refused> {
161        self.admit()?;
162        let Some(active) = self.active.get().checked_add(1) else {
163            return Err(self.refuse(ImprintError::IncompleteInput));
164        };
165        let state = self
166            .provider
167            .start(&self.identity, &self.reader.execution())
168            .map_err(|cause| self.refuse(ImprintError::Digest(DigestError::Callback(cause))))?;
169        let Resolution::Supported(state) = state else {
170            return Err(self.refuse(ImprintError::Digest(DigestError::Unstreamable)));
171        };
172        self.active.set(active);
173        Ok(ImprintState {
174            context: self,
175            state: Some(state),
176        })
177    }
178    /// Feeds every selected state in ONE complete source pass. Empty state
179    /// lists and foreign contexts refuse before source I/O. Source errors and
180    /// ignored receiver failures remain sticky across the whole recipe.
181    pub fn stream<'context, S: crate::stream::Source<Error = E>>(
182        &'context self,
183        source: &mut S,
184        states: &mut [&mut ImprintState<'context, 'reader, 'document, P, E>],
185    ) -> Result<(), crate::stream::Refused> {
186        self.stream_with(source, states, &self.reader.execution())
187    }
188
189    /// Streams another original source using ITS producing ceiling and this
190    /// operation's current root ceilings/account. This does not use the old
191    /// source's former account or widen a narrowed current query. The format
192    /// recipe supplies the actual source reader, never a replacement parser.
193    pub fn stream_from<'context, S: crate::stream::Source<Error = E>>(
194        &'context self,
195        source: &mut S,
196        origin: &Reader<'_>,
197        states: &mut [&mut ImprintState<'context, 'reader, 'document, P, E>],
198    ) -> Result<(), crate::stream::Refused> {
199        self.stream_with(source, states, &origin.execution_query(self.reader))
200    }
201
202    fn stream_with<'context, S, W>(
203        &'context self,
204        source: &mut S,
205        states: &mut [&mut ImprintState<'context, 'reader, 'document, P, E>],
206        execution: &W,
207    ) -> Result<(), crate::stream::Refused>
208    where
209        S: crate::stream::Source<Error = E>,
210        W: structured_asn1::memory::Work,
211    {
212        self.admit()?;
213        if states.is_empty() {
214            return Err(self.refuse(ImprintError::IncompleteInput));
215        }
216        for state in &*states {
217            self.admit()?;
218            if !core::ptr::eq(&raw const state.context.owner, &raw const self.owner) {
219                return Err(self.refuse(ImprintError::ForeignInput));
220            }
221        }
222        let result = crate::stream::run(
223            source,
224            &mut Fanout { states },
225            execution,
226            self.field,
227            self.span.start,
228        );
229        match result {
230            Ok(()) => Ok(()),
231            Err(crate::stream::StreamError::Source(cause)) => Err(self.refuse(ImprintError::Pass(
232                crate::stream::StreamError::Source(cause),
233            ))),
234            Err(crate::stream::StreamError::Processing(cause)) => Err(self.refuse(
235                ImprintError::Pass(crate::stream::StreamError::Processing(cause)),
236            )),
237            // State writes already retained the original callback cause.
238            Err(crate::stream::StreamError::Callback(_)) => Err(crate::stream::Refused),
239        }
240    }
241    /// Compares one completed output with the original declared hash. Other
242    /// candidates can be completed and compared without another payload pass.
243    pub fn compare<'context>(
244        &'context self,
245        digest: &ImprintDigest<'_, '_, 'document, P, E>,
246    ) -> Result<ImprintProof<'context>, crate::stream::Refused> {
247        self.admit()?;
248        if !core::ptr::eq(&raw const digest.context.owner, &raw const self.owner) {
249            return Err(self.refuse(ImprintError::ForeignInput));
250        }
251        let result = crate::binding::matches_digest(
252            self.provider,
253            digest.finished(),
254            self.expected.bytes(),
255            &self.reader.execution(),
256            self.field,
257            self.span,
258        )
259        .map_err(|cause| self.refuse(ImprintError::Comparison(cause)))?;
260        Ok(ImprintProof {
261            owner: &self.owner,
262            agreement: match result {
263                DigestBinding::Matches => ImprintAgreement::Matches,
264                DigestBinding::Differs => ImprintAgreement::Differs,
265                DigestBinding::Malformed(_) => ImprintAgreement::MalformedHash,
266            },
267        })
268    }
269}
270
271impl<'context, 'reader, 'document, P: AsnDigest<'document>, E>
272    ImprintState<'context, 'reader, 'document, P, E>
273{
274    /// Admits and hashes one original/generated chunk. An ignored failure
275    /// cannot be repaired by subsequent writes or finalization.
276    pub fn write(&mut self, bytes: &[u8]) -> Result<(), crate::stream::Refused> {
277        self.write_with(bytes, &self.context.reader.execution())
278    }
279    /// Hashes a range from another original reader, with that origin and the
280    /// current operation admitted together. The caller's format kernel binds
281    /// `bytes` to that original source before invoking this operation.
282    pub fn write_from(
283        &mut self,
284        bytes: &[u8],
285        origin: &Reader<'_>,
286    ) -> Result<(), crate::stream::Refused> {
287        self.write_with(bytes, &origin.execution_query(self.context.reader))
288    }
289    fn write_with<W: structured_asn1::memory::Work>(
290        &mut self,
291        bytes: &[u8],
292        execution: &W,
293    ) -> Result<(), crate::stream::Refused> {
294        self.context.admit_pass()?;
295        // Empty ranges still cost a producing-source/fanout visit. A digest
296        // provider correctly charges zero byte work for them; it cannot fund
297        // this structural traversal or enforce its origin ceiling for us.
298        execution.charge_work(1).map_err(|cause| {
299            self.context
300                .refuse(ImprintError::Pass(crate::stream::StreamError::Processing(
301                    Error::resource(cause, self.context.field, self.context.span.start),
302                )))
303        })?;
304        self.context
305            .provider
306            .update(
307                self.state
308                    .as_mut()
309                    .expect("unfinished state owns its backend"),
310                bytes,
311                execution,
312            )
313            .map_err(|cause| {
314                self.context
315                    .refuse(ImprintError::Pass(crate::stream::StreamError::Callback(
316                        DigestError::Callback(cause),
317                    )))
318            })
319    }
320    /// Consumes the state exactly once. A wrong provider output width refuses
321    /// before exposing it for an intermediate tree or final comparison.
322    /// # Panics
323    /// If the internal ownership invariant is violated. The consuming public
324    /// API retains its backend state until this single completion.
325    pub fn finish(
326        mut self,
327    ) -> Result<ImprintDigest<'context, 'reader, 'document, P, E>, crate::stream::Refused> {
328        self.context.admit_pass()?;
329        let output = self
330            .context
331            .provider
332            .finish(
333                self.state
334                    .take()
335                    .expect("unfinished state owns its backend"),
336                &self.context.reader.execution(),
337            )
338            .map_err(|cause| {
339                self.context
340                    .refuse(ImprintError::Pass(crate::stream::StreamError::Callback(
341                        DigestError::Callback(cause),
342                    )))
343            })?;
344        if let Some(expected) = self.context.provider.width(&self.context.identity)
345            && output.as_ref().len() != expected
346        {
347            return Err(self.context.refuse(ImprintError::Pass(
348                crate::stream::StreamError::Callback(DigestError::OutputWidth {
349                    expected,
350                    actual: output.as_ref().len(),
351                }),
352            )));
353        }
354        Ok(ImprintDigest {
355            context: self.context,
356            output,
357        })
358    }
359}
360impl<'document, P: AsnDigest<'document>, E> Drop for ImprintState<'_, '_, 'document, P, E> {
361    fn drop(&mut self) {
362        let active = self.context.active.get();
363        debug_assert!(active > 0);
364        self.context.active.set(active - 1);
365        if self.state.is_some() {
366            self.context.abandoned.set(true);
367        }
368    }
369}
370
371struct Fanout<'states, 'state, 'context, 'reader, 'document, P: AsnDigest<'document>, E> {
372    states: &'states mut [&'state mut ImprintState<'context, 'reader, 'document, P, E>],
373}
374impl<'document, P: AsnDigest<'document>, E> crate::stream::Sink
375    for Fanout<'_, '_, '_, '_, 'document, P, E>
376{
377    type Error = crate::stream::Refused;
378    fn write<W: structured_asn1::memory::Work>(
379        &mut self,
380        bytes: &[u8],
381        execution: &W,
382    ) -> Result<(), Self::Error> {
383        for state in &mut *self.states {
384            state.write_with(bytes, execution)?;
385        }
386        Ok(())
387    }
388}
389
390/// Ordinary covered bytes use the same closed computation as derived inputs.
391pub struct RawInput<'source, S>(&'source mut S);
392impl<'source, S> RawInput<'source, S> {
393    /// Borrows a source without allocation, copying or changing its errors.
394    #[must_use]
395    pub fn new(source: &'source mut S) -> Self {
396        Self(source)
397    }
398}
399impl<P, PE, S: crate::stream::Source> ComputedInput<P, PE> for RawInput<'_, S> {
400    type Error = S::Error;
401    fn compute<'context, 'reader, 'document>(
402        &mut self,
403        context: &'context ImprintContext<'reader, 'document, P, Self::Error>,
404    ) -> Result<ImprintProof<'context>, InputFailure<Self::Error>>
405    where
406        P: AsnDigest<'document, Error = PE>,
407    {
408        let mut state = context.start()?;
409        context.stream(self.0, &mut [&mut state])?;
410        let output = state.finish()?;
411        context.compare(&output).map_err(InputFailure::Refused)
412    }
413}
414
415pub(super) fn compute<'a, P, I>(
416    imprint: &MessageImprint<'a>,
417    provider: &'a P,
418    parent: &Reader<'a>,
419    input: &mut I,
420    identity: P::Identity,
421) -> Result<ImprintAgreement, ImprintError<I::Error, P::Error>>
422where
423    P: AsnDigest<'a>,
424    I: ComputedInput<P, P::Error>,
425{
426    let field = imprint.fields().hashed;
427    let span = imprint.hashed().span();
428    // Complete declared-hash materialization precedes state/provider effects.
429    let expected = match imprint.hashed().as_one() {
430        Some(bytes) => Expected::Borrowed(bytes),
431        None => Expected::Joined(
432            imprint
433                .hashed()
434                .copy_in(parent, field, span.start)
435                .map_err(ImprintError::Processing)?,
436        ),
437    };
438    let context = ImprintContext {
439        provider,
440        identity,
441        reader: parent,
442        expected,
443        field,
444        span,
445        owner: 0,
446        active: Cell::new(0),
447        abandoned: Cell::new(false),
448        failure: RefCell::new(None),
449    };
450    let result = input.compute(&context);
451    if let Some(cause) = context.failure.borrow_mut().take() {
452        return Err(cause);
453    }
454    let proof = match result {
455        Ok(proof) => proof,
456        Err(InputFailure::Source(cause)) => {
457            return Err(ImprintError::Pass(crate::stream::StreamError::Source(
458                cause,
459            )));
460        }
461        Err(InputFailure::Refused(_)) => return Err(ImprintError::IncompleteInput),
462    };
463    if !core::ptr::eq(proof.owner, &raw const context.owner) {
464        return Err(ImprintError::ForeignInput);
465    }
466    if context.active.get() != 0 || context.abandoned.get() {
467        return Err(ImprintError::IncompleteInput);
468    }
469    Ok(proof.agreement)
470}