Skip to main content

structured_asn1/
digest.rs

1//! One incremental digest execution contract for every format.
2//!
3//! A consumer implements [`Digest`] once over its own cryptographic backend.
4//! Which algorithm a document names is a separate question answered by the
5//! format that wrote it: ASN.1 identifiers by `structured-pkix`, XML methods
6//! by `structured-xades`. Both hand their resolved identity to this contract.
7
8use crate::memory::Work;
9use core::cmp::Ordering;
10
11/// Semantic resolution, distinct from failure to execute the resolver.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Resolution<T> {
14    /// Fully resolved algorithm identity, including parameters/output semantics.
15    Supported(T),
16    /// The selected provider cannot answer this mechanism.
17    Unsupported,
18    /// The original algorithm identifier violates the selected algorithm's syntax.
19    Malformed,
20}
21
22/// Incremental digest operations of one explicitly selected provider.
23///
24/// Identities carry complete algorithm semantics, not just an identifier or a
25/// byte width. Every callback admits its complete transitive work and storage
26/// in the supplied execution before it runs. Errors are a spent ceiling or a
27/// provider fault, never a finding about the document. States may borrow the
28/// provider for `'a` and carry no `Clone`, `Send` or `Sync` requirement.
29pub trait Digest<'a> {
30    /// Fully resolved digest identity.
31    type Identity;
32    /// Digest state, independently movable from its identity object.
33    type State;
34    /// Final output, inline or retaining its own admitted allocation charge.
35    type Output: AsRef<[u8]>;
36    /// Original callback failure, including that provider's resource refusal.
37    type Error;
38
39    /// Fixed output width, or `None` for an identity without one. This is O(1)
40    /// metadata access on the resolved identity, not parsing.
41    fn width(&self, identity: &Self::Identity) -> Option<usize>;
42
43    /// Total semantic order: `Equal` means the outputs can be shared.
44    fn compare<W: Work>(
45        &self,
46        left: &Self::Identity,
47        right: &Self::Identity,
48        execution: &W,
49    ) -> Result<Ordering, Self::Error>;
50
51    /// Starts one stream for this identity without retaining a reference to the
52    /// identity object. A backend without incremental capability answers
53    /// `Unsupported`; it never buffers into a one-shot fallback.
54    fn start<W: Work>(
55        &'a self,
56        identity: &Self::Identity,
57        execution: &W,
58    ) -> Result<Resolution<Self::State>, Self::Error>;
59
60    /// Admits and processes one complete chunk exactly once.
61    fn update<W: Work>(
62        &self,
63        state: &mut Self::State,
64        bytes: &[u8],
65        execution: &W,
66    ) -> Result<(), Self::Error>;
67
68    /// Consumes a completed state once; no state cloning or repeated finish.
69    fn finish<W: Work>(
70        &self,
71        state: Self::State,
72        execution: &W,
73    ) -> Result<Self::Output, Self::Error>;
74
75    /// Compares this provider's finished output with declared bytes, admitting
76    /// the whole scan and applying the backend's comparison strategy.
77    fn matches<W: Work>(
78        &self,
79        identity: &Self::Identity,
80        output: &Self::Output,
81        expected: &[u8],
82        execution: &W,
83    ) -> Result<bool, Self::Error>;
84}
85
86/// Failure of a one-shot check, apart from every completed comparison.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum OnceError<E> {
89    /// Original provider failure, including its resource refusal.
90    Provider(E),
91    /// The provider's output contradicts the fixed width of its own identity.
92    /// This is a provider contract failure, not malformed document data.
93    OutputWidth {
94        /// Width promised by the resolved identity.
95        expected: usize,
96        /// Width actually returned.
97        actual: usize,
98    },
99}
100
101/// Hashes one contiguous input and compares it with `expected`: start, one
102/// update, finish and matches over the same contract as streamed input.
103///
104/// `Unsupported` and `Malformed` are the provider's start answer, returned
105/// before any byte is hashed.
106///
107/// # Errors
108///
109/// Returns the provider's failure, or [`OnceError::OutputWidth`] when the
110/// output contradicts the identity's fixed width.
111///
112/// # Examples
113///
114/// ```
115/// # extern crate alloc;
116/// # use alloc::sync::Arc;
117/// # use core::cmp::Ordering;
118/// # use structured_asn1::digest::{self, Digest, Resolution};
119/// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError, Work};
120/// /// A one-byte additive checksum, enough to show the call sequence.
121/// struct Sum;
122/// impl Digest<'_> for Sum {
123///     type Identity = ();
124///     type State = u8;
125///     type Output = [u8; 1];
126///     type Error = MemoryError;
127///     fn width(&self, (): &()) -> Option<usize> { Some(1) }
128///     fn compare<W: Work>(&self, (): &(), (): &(), _: &W) -> Result<Ordering, MemoryError> {
129///         Ok(Ordering::Equal)
130///     }
131///     fn start<W: Work>(&self, (): &(), w: &W) -> Result<Resolution<u8>, MemoryError> {
132///         w.charge_work(1)?;
133///         Ok(Resolution::Supported(0))
134///     }
135///     fn update<W: Work>(&self, s: &mut u8, bytes: &[u8], w: &W) -> Result<(), MemoryError> {
136///         w.charge_work(bytes.len() as u64)?;
137///         *s = bytes.iter().fold(*s, |sum, byte| sum.wrapping_add(*byte));
138///         Ok(())
139///     }
140///     fn finish<W: Work>(&self, s: u8, _: &W) -> Result<[u8; 1], MemoryError> { Ok([s]) }
141///     fn matches<W: Work>(&self, (): &(), o: &[u8; 1], e: &[u8], w: &W) -> Result<bool, MemoryError> {
142///         w.charge_work(1)?;
143///         Ok(o.as_slice() == e)
144///     }
145/// }
146/// let execution = Memory::new(Arc::new(MemoryAccount::new(0, 64)));
147/// let answer = digest::matches_once(&Sum, &(), b"\x01\x02", &[3], &execution).unwrap();
148/// assert_eq!(answer, Resolution::Supported(true));
149/// ```
150pub fn matches_once<'a, D: Digest<'a> + ?Sized, W: Work>(
151    digest: &'a D,
152    identity: &D::Identity,
153    input: &[u8],
154    expected: &[u8],
155    execution: &W,
156) -> Result<Resolution<bool>, OnceError<D::Error>> {
157    let mut state = match digest
158        .start(identity, execution)
159        .map_err(OnceError::Provider)?
160    {
161        Resolution::Supported(state) => state,
162        Resolution::Unsupported => return Ok(Resolution::Unsupported),
163        Resolution::Malformed => return Ok(Resolution::Malformed),
164    };
165    digest
166        .update(&mut state, input, execution)
167        .map_err(OnceError::Provider)?;
168    let output = digest
169        .finish(state, execution)
170        .map_err(OnceError::Provider)?;
171    if let Some(expected) = digest.width(identity) {
172        let actual = output.as_ref().len();
173        if actual != expected {
174            return Err(OnceError::OutputWidth { expected, actual });
175        }
176    }
177    digest
178        .matches(identity, &output, expected, execution)
179        .map(Resolution::Supported)
180        .map_err(OnceError::Provider)
181}