Skip to main content

structured_cms/timestamp/check/
asynchronous.rs

1//! Asynchronous acquisition inside the same scoped timestamp check pipeline.
2
3use super::{
4    CheckContext, CheckError, CheckPurpose, CheckedTimestamp, Error, ErrorCode, Execution,
5    Infallible, InfoCheckError, Information, InformationFacts, ProfileCheckError, ProfileFailure,
6    ProfileInput, Reader, SignatureProvider, SourceMap, Token, TokenExtension, TokenSignature,
7    common_holds, directory, ess, finish_information, preflight, profile, unrecognized,
8};
9use crate::timestamp::{AsyncComputedInput, AsyncInfoVisitor, AsyncRawInput, TokenInfo};
10
11struct Inspection<'a, 'doc, 'r, 'c, P, I, R, F> {
12    token: &'a Token<'doc>,
13    provider: &'a P,
14    parent: &'r Reader<'a>,
15    context: &'c CheckContext<'a, 'c>,
16    ess: &'r ess::SigningAttributes<'a>,
17    identifier: &'r directory::CertificateAgreement,
18    carried: Option<bool>,
19    input: &'r mut I,
20    recognizes: &'r mut R,
21    profile: Option<F>,
22    purpose: CheckPurpose,
23}
24
25impl<'a, 'doc: 'a, P, I, R, F, PE, RE, FE> AsyncInfoVisitor
26    for Inspection<'a, 'doc, '_, '_, P, I, R, F>
27where
28    P: for<'scope> SignatureProvider<'scope, Error = PE>,
29    I: AsyncComputedInput<P, PE>,
30    R: FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
31    F: for<'view, 'scope> FnOnce(
32        ProfileInput<'view, 'doc, 'a, 'scope>,
33    ) -> Result<(), ProfileFailure<FE>>,
34{
35    type Output = (TokenSignature<'a>, ess::Bindings, Information);
36    type Error = profile::InformationFailure<I::Error, PE, RE, FE>;
37
38    async fn visit<'data: 'scope, 'scope>(
39        &'scope mut self,
40        info: &'scope TokenInfo<'data>,
41        inside: &'scope Reader<'data>,
42        source: &'scope SourceMap,
43    ) -> Result<Self::Output, Self::Error> {
44        if info.has_faults() {
45            return Err(InfoCheckError::Nonconforming { span: info.span() }.into());
46        }
47        self.profile.take().expect("one information visit")(ProfileInput {
48            token: self.token,
49            parent: self.parent,
50            envelope: None,
51            ess: self.ess,
52            info,
53            reader: inside,
54            source,
55        })
56        .map_err(profile::InformationFailure::Profile)?;
57        let bindings = self
58            .ess
59            .bind_certificate(self.provider, self.context.certificate, self.parent)
60            .map_err(InfoCheckError::Ess)?;
61        let signature: TokenSignature<'a> = self
62            .token
63            .check_signature(self.provider, self.context.key, self.parent)
64            .map_err(InfoCheckError::Signature)?;
65        let holds = common_holds(&signature, bindings, self.identifier, self.carried);
66        let authority = info
67            .authority(
68                self.context.certificate,
69                inside,
70                self.context.certificate_parent,
71            )
72            .map_err(InfoCheckError::Authority)?;
73        let fields = self
74            .context
75            .requested
76            .as_ref()
77            .map(|request| info.matches_request_fields(request.request, inside, request.parent))
78            .transpose()
79            .map_err(InfoCheckError::RequestField)?;
80        let unknown = unrecognized(info, inside, self.recognizes)?;
81        let (imprint, requested_imprint) = if let Some(request) = &self.context.requested {
82            let (imprint, requested) = info
83                .imprint()
84                .check_requested_input_async(
85                    request.request.imprint(),
86                    self.provider,
87                    (inside, request.parent),
88                    self.input,
89                )
90                .await
91                .map_err(|error| match error {
92                    crate::timestamp::RequestedContentError::Request(source) => {
93                        InfoCheckError::RequestImprint(source)
94                    }
95                    crate::timestamp::RequestedContentError::Content(source) => {
96                        InfoCheckError::Imprint(source)
97                    }
98                })?;
99            (imprint, Some(requested))
100        } else {
101            (
102                info.imprint()
103                    .check_input_async(self.provider, inside, self.input)
104                    .await
105                    .map_err(InfoCheckError::Imprint)?,
106                None,
107            )
108        };
109        let information = finish_information(
110            info,
111            inside,
112            source,
113            self.context,
114            InformationFacts {
115                authority,
116                imprint,
117                requested_imprint,
118                requested_fields: fields,
119                unknown,
120            },
121            holds,
122            self.purpose,
123        )?;
124        Ok((signature, bindings, information))
125    }
126}
127
128impl<'doc> Token<'doc> {
129    /// Complete common timestamp checking over asynchronously acquired bytes.
130    /// The only suspension is bounded source acquisition inside scoped `TSTInfo`;
131    /// no joined reader or temporary model escapes, including on cancellation.
132    #[expect(
133        clippy::result_large_err,
134        reason = "failure retains the original carried-source mapping rather than allocating or discarding provenance"
135    )]
136    pub async fn check_async<'a, P, S, PE, RE>(
137        &'a self,
138        provider: &'a P,
139        parent: &Reader<'a>,
140        context: &CheckContext<'a, '_>,
141        covered: &mut S,
142        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
143    ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
144    where
145        'doc: 'a,
146        P: for<'scope> SignatureProvider<'scope, Error = PE>,
147        S: crate::stream::AsyncSource,
148    {
149        self.check_async_input_with_profile(
150            provider,
151            parent,
152            context,
153            &mut AsyncRawInput::new(covered),
154            recognizes,
155            |_| Ok::<(), ProfileFailure<Infallible>>(()),
156            CheckPurpose::Ordinary,
157        )
158        .await
159        .map_err(ProfileCheckError::into_common)
160    }
161
162    /// Parse-once profile checking before provider/content callbacks, followed
163    /// by the same complete asynchronous common pipeline.
164    #[expect(
165        clippy::result_large_err,
166        reason = "profile failure preserves the scoped input map and original callback cause without an error allocation"
167    )]
168    pub async fn check_async_with_profile<'a, P, I, PE, RE, FE>(
169        &'a self,
170        provider: &'a P,
171        parent: &Reader<'a>,
172        context: &CheckContext<'a, '_>,
173        covered: &mut I,
174        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
175        profile: impl for<'view, 'scope> FnOnce(
176            ProfileInput<'view, 'doc, 'a, 'scope>,
177        ) -> Result<(), ProfileFailure<FE>>,
178    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<I::Error, PE, RE, FE>>
179    where
180        'doc: 'a,
181        P: for<'scope> SignatureProvider<'scope, Error = PE>,
182        I: AsyncComputedInput<P, PE>,
183    {
184        self.check_async_input_with_profile(
185            provider,
186            parent,
187            context,
188            covered,
189            recognizes,
190            profile,
191            CheckPurpose::Ordinary,
192        )
193        .await
194    }
195
196    /// Historical asynchronous pass with the same explicit prohibition on
197    /// current-clock/live-request assertions as the resident archival API.
198    #[expect(
199        clippy::result_large_err,
200        reason = "historical checking retains identical source-qualified failures to ordinary checking"
201    )]
202    pub async fn check_historical_async_with_profile<'a, P, I, PE, RE, FE>(
203        &'a self,
204        provider: &'a P,
205        parent: &Reader<'a>,
206        context: &CheckContext<'a, '_>,
207        covered: &mut I,
208        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
209        profile: impl for<'view, 'scope> FnOnce(
210            ProfileInput<'view, 'doc, 'a, 'scope>,
211        ) -> Result<(), ProfileFailure<FE>>,
212    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<I::Error, PE, RE, FE>>
213    where
214        'doc: 'a,
215        P: for<'scope> SignatureProvider<'scope, Error = PE>,
216        I: AsyncComputedInput<P, PE>,
217    {
218        if context.requested.is_some() || context.reference.is_some() || context.window.is_some() {
219            return Err(CheckError::Processing(Error::schema(
220                ErrorCode::InvalidLocation,
221                "tstInfo.historical",
222                self.message().span().start,
223            ))
224            .into());
225        }
226        self.check_async_input_with_profile(
227            provider,
228            parent,
229            context,
230            covered,
231            recognizes,
232            profile,
233            CheckPurpose::Historical,
234        )
235        .await
236    }
237
238    #[expect(
239        clippy::result_large_err,
240        reason = "the one shared async checking pipeline owns its error source map after temporary input is dropped"
241    )]
242    #[expect(
243        clippy::too_many_arguments,
244        reason = "the shared driver receives independent provider, source, context, recognizer, format callback and historical purpose without boxing callbacks"
245    )]
246    async fn check_async_input_with_profile<'a, P, I, PE, RE, FE>(
247        &'a self,
248        provider: &'a P,
249        parent: &Reader<'a>,
250        context: &CheckContext<'a, '_>,
251        covered: &mut I,
252        mut recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
253        profile: impl for<'view, 'scope> FnOnce(
254            ProfileInput<'view, 'doc, 'a, 'scope>,
255        ) -> Result<(), ProfileFailure<FE>>,
256        purpose: CheckPurpose,
257    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<I::Error, PE, RE, FE>>
258    where
259        'doc: 'a,
260        P: for<'scope> SignatureProvider<'scope, Error = PE>,
261        I: AsyncComputedInput<P, PE>,
262    {
263        preflight(self, parent, context).map_err(CheckError::from)?;
264        let identifier =
265            directory::matches_certificate_in(self.signer().sid(), context.certificate, parent)
266                .map_err(CheckError::Identifier)?;
267        let ess =
268            ess::SigningAttributes::read(self.signer(), parent).map_err(CheckError::Processing)?;
269        let carried = context
270            .requested
271            .as_ref()
272            .map(|requested| {
273                self.matches_cert_req(
274                    requested.request,
275                    Some(context.certificate),
276                    parent,
277                    requested.parent,
278                )
279            })
280            .transpose()
281            .map_err(CheckError::RequestField)?;
282        if !self.faults().is_empty()
283            || !self.message().faults().is_empty()
284            || !self.signer().faults().is_empty()
285            || !ess.faults().is_empty()
286            || ess.v1().is_some_and(|reading| !reading.faults().is_empty())
287            || ess.v2().is_some_and(|reading| !reading.faults().is_empty())
288        {
289            return Err(CheckError::Nonconforming {
290                span: self.message().span(),
291            }
292            .into());
293        }
294        let (signature, bindings, information) = self
295            .with_info_async(
296                parent,
297                None,
298                &mut Inspection {
299                    token: self,
300                    provider,
301                    parent,
302                    context,
303                    ess: &ess,
304                    identifier: &identifier,
305                    carried,
306                    input: covered,
307                    recognizes: &mut recognizes,
308                    profile: Some(profile),
309                    purpose,
310                },
311            )
312            .await
313            .map_err(profile::information_error)?;
314        Ok(CheckedTimestamp {
315            signature,
316            identifier,
317            ess,
318            bindings,
319            authority: information.authority,
320            imprint: information.imprint,
321            requested_imprint: information.requested_imprint,
322            requested_fields: information.requested_fields,
323            carried,
324            timeliness: information.timeliness,
325            accuracy_error: information.accuracy_error,
326            unknown_critical: information.unknown,
327            evidence: information.evidence,
328        })
329    }
330}