Skip to main content

structured_xades/
digest.rs

1//! Streaming XML digest checks through the caller's selected provider.
2//!
3//! Execution is the provider's [`structured_asn1::digest::Digest`]
4//! implementation; [`DigestProvider`] adds only the XML method resolution and
5//! classification. No independent URI dispatcher or implicit backend is
6//! selected by this crate.
7
8use crate::binary::{self, BinaryXml, Location, OwnedBinaryXml};
9use crate::certificate::{self, Agreement, DigestProvider};
10use crate::xml::{Access, Snapshot, Xml};
11use structured_asn1::digest::{Digest, Resolution};
12use structured_asn1::memory::{MemoryError, OwnedVec, Work};
13use structured_cms::stream::{self, Sink, Source, StreamError};
14
15mod batch;
16pub use batch::Batch;
17
18/// Original digest method/value selectors, NOT proof of their signed coverage.
19/// The enclosing property mechanism supplies its actual source-bound nodes.
20#[derive(Debug, Clone, Copy)]
21pub struct Reference<N, D, G> {
22    /// Original XML document and generation.
23    pub snapshot: Snapshot<D, G>,
24    /// Original method including untouched parameters.
25    pub method: N,
26    /// Original base64 digest-value element.
27    pub value: N,
28}
29
30/// Execution failure stays independent of completed digest agreement.
31#[derive(Debug)]
32pub enum Error<SE, PE> {
33    /// A prepared XML method was paired with another provider's output.
34    ForeignDigest,
35    /// The provider resolved an identity but cannot stream its digest state.
36    Unstreamable,
37    /// Original complete-source acquisition/replay refusal.
38    Source(SE),
39    /// Original selected-provider failure, including resource/policy refusal.
40    Provider(PE),
41    /// Driver metadata admission failed before callback execution.
42    Resource(MemoryError),
43    /// Existing stream driver refused dispatch. Its location is the virtual
44    /// input's start (zero), NOT a physical XML byte or decoded ASN.1 offset;
45    /// the outer binary error preserves the actual XML value node/generation.
46    Stream(structured_cms::Error),
47    /// Backend output contradicts its already resolved fixed width. This is
48    /// processing failure, not malformed declared bytes or a normal mismatch.
49    OutputWidth {
50        /// Fixed resolved width.
51        expected: usize,
52        /// Actual backend output width.
53        actual: usize,
54    },
55}
56
57impl<SE, PE> Error<SE, PE> {
58    /// Translates only acquisition causes, preserving provider, processing and
59    /// resource classifications without replacing their original locations.
60    pub fn map_source<E>(self, map: impl FnOnce(SE) -> E) -> Error<E, PE> {
61        match self {
62            Self::ForeignDigest => Error::ForeignDigest,
63            Self::Unstreamable => Error::Unstreamable,
64            Self::Source(cause) => Error::Source(map(cause)),
65            Self::Provider(cause) => Error::Provider(cause),
66            Self::Resource(cause) => Error::Resource(cause),
67            Self::Stream(cause) => Error::Stream(cause),
68            Self::OutputWidth { expected, actual } => Error::OutputWidth { expected, actual },
69        }
70    }
71}
72
73struct Updating<'s, 'p, P: Digest<'p>> {
74    provider: &'p P,
75    state: &'s mut P::State,
76}
77
78impl<'p, P: Digest<'p>> Sink for Updating<'_, 'p, P> {
79    type Error = P::Error;
80    fn write<W: Work>(&mut self, bytes: &[u8], execution: &W) -> Result<(), Self::Error> {
81        self.provider.update(self.state, bytes, execution)
82    }
83}
84
85/// Resolves the actual XML method, validates declared width before source I/O,
86/// then runs one exact source pass through the existing sticky stream driver.
87/// No whole-input collector, parser, implicit I/O or runtime crypto dependency
88/// is introduced. This produces digest agreement only, never signed coverage,
89/// policy-document acceptance, timestamp evidence or an attained B level.
90pub fn check<'p, X: BinaryXml, P: DigestProvider<'p, X>, S: Source, W: Work>(
91    xml: &X,
92    reference: Reference<X::Id, X::Document, X::Generation>,
93    source: &mut S,
94    provider: &'p P,
95    execution: &W,
96) -> binary::BinaryResult<Agreement, X, Error<S::Error, P::Error>> {
97    check_with(
98        xml,
99        reference,
100        provider,
101        execution,
102        |identity, expected, work| {
103            run_resolved::<P, S, W>(&identity, expected, source, provider, work)
104        },
105    )
106    .map(|reading| match reading {
107        Checked::Answer(answer) | Checked::Value(answer) => answer,
108    })
109}
110
111/// Same method/width/algorithm checks over a source that may suspend. The XML
112/// owner moves its funded decoded value into this scope rather than copying a
113/// callback-local digest into a second allocation before awaiting input.
114pub async fn check_async<'p, X: OwnedBinaryXml, P: DigestProvider<'p, X>, S, W: Work>(
115    xml: &X,
116    reference: Reference<X::Id, X::Document, X::Generation>,
117    source: &mut S,
118    provider: &'p P,
119    execution: &W,
120) -> binary::BinaryResult<Agreement, X, Error<S::Error, P::Error>>
121where
122    S: stream::AsyncSource,
123{
124    check_async_with::<true, X, P, S, W>(xml, reference, source, provider, execution).await
125}
126
127/// `XMLDSig` digest checking without selecting an `XAdES` baseline profile. The
128/// format using this result supplies its own profile requirements; selected
129/// provider algorithm/policy validation is still mandatory before source I/O.
130pub async fn check_xml_async<'p, X: OwnedBinaryXml, P: DigestProvider<'p, X>, S, W: Work>(
131    xml: &X,
132    reference: Reference<X::Id, X::Document, X::Generation>,
133    source: &mut S,
134    provider: &'p P,
135    execution: &W,
136) -> binary::BinaryResult<Agreement, X, Error<S::Error, P::Error>>
137where
138    S: stream::AsyncSource,
139{
140    check_async_with::<false, X, P, S, W>(xml, reference, source, provider, execution).await
141}
142
143/// Compares an original XML method/value with an actual already completed
144/// common digest. Full semantic identity must agree before output comparison;
145/// output width or matching bytes alone cannot establish that association.
146/// The actual output supplies its own provider. No state, output or identity
147/// is cloned, no resolver is replaced and no source is hashed again.
148/// This general XML path does not select an `XAdES` baseline algorithm policy.
149pub fn check_xml_finished<'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work>(
150    xml: &X,
151    reference: Reference<X::Id, X::Document, X::Generation>,
152    actual: structured_cms::provider::Finished<'_, P, P::Identity, P::Output>,
153    execution: &W,
154) -> binary::BinaryResult<Agreement, X, Error<core::convert::Infallible, P::Error>> {
155    let access = Access::new(xml, reference.snapshot, execution);
156    if let Some(answer) = preflight(&access, reference)? {
157        return Ok(answer);
158    }
159    let location = Location {
160        snapshot: reference.snapshot,
161        node: reference.value,
162    };
163    binary::with_octets(xml, location, execution, |expected| {
164        Ok::<_, core::convert::Infallible>(compare_finished(&access, reference, actual, expected))
165    })
166    .map_err(|error| error.map_callback(|never| match never {}))?
167}
168
169fn compare_finished<'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work>(
170    access: &Access<'_, X, W>,
171    reference: Reference<X::Id, X::Document, X::Generation>,
172    actual: structured_cms::provider::Finished<'_, P, P::Identity, P::Output>,
173    expected: &[u8],
174) -> binary::BinaryResult<Agreement, X, Error<core::convert::Infallible, P::Error>> {
175    let provider = actual.provider();
176    let identity = match resolve_method(access, reference, provider)? {
177        Resolution::Supported(identity) => identity,
178        Resolution::Unsupported => return Ok(Agreement::Unsupported),
179        Resolution::Malformed => return Ok(Agreement::MalformedMethod),
180    };
181    let identity = match certificate::resolved_digest::<false, X, P>(identity, provider) {
182        Ok(identity) => identity,
183        Err(answer) => return Ok(answer),
184    };
185    compare_resolved_finished(access, reference, provider, &identity, actual, expected)
186}
187
188fn compare_resolved_finished<'p, X: Xml, P: Digest<'p>, W: Work>(
189    access: &Access<'_, X, W>,
190    reference: Reference<X::Id, X::Document, X::Generation>,
191    provider: &P,
192    identity: &P::Identity,
193    actual: structured_cms::provider::Finished<'_, P, P::Identity, P::Output>,
194    expected: &[u8],
195) -> binary::BinaryResult<Agreement, X, Error<core::convert::Infallible, P::Error>> {
196    let width = provider.width(identity);
197    if let Some(width) = width
198        && expected.len() != width
199    {
200        return Ok(Agreement::MalformedValue);
201    }
202    let location = Location {
203        snapshot: reference.snapshot,
204        node: reference.value,
205    };
206    access
207        .query_fallible(reference.value, |_, _, work| {
208            work.charge_work(1).map_err(Error::Resource)?;
209            if provider
210                .compare(identity, actual.identity(), work)
211                .map_err(Error::Provider)?
212                != core::cmp::Ordering::Equal
213            {
214                return Ok(Agreement::Differs);
215            }
216            if let Some(width) = width
217                && actual.output().as_ref().len() != width
218            {
219                return Err(Error::OutputWidth {
220                    expected: width,
221                    actual: actual.output().as_ref().len(),
222                });
223            }
224            let agrees = provider
225                .matches(identity, actual.output(), expected, work)
226                .map_err(Error::Provider)?;
227            Ok(if agrees {
228                Agreement::Matches
229            } else {
230                Agreement::Differs
231            })
232        })
233        .map_err(|cause| match cause {
234            crate::xml::AccessError::Xml(cause) => binary::Error::Callback { location, cause },
235            crate::xml::AccessError::Changed { node } => binary::Error::Access {
236                location,
237                cause: crate::xml::AccessError::Changed { node },
238            },
239            crate::xml::AccessError::Resource { node, cause } => binary::Error::Access {
240                location,
241                cause: crate::xml::AccessError::Resource { node, cause },
242            },
243        })
244}
245
246enum PreparedValue<I> {
247    Answer(Agreement),
248    Ready { identity: I, expected: OwnedVec<u8> },
249}
250
251/// Original XML method/value admitted and resolved before asynchronous input
252/// or provider state creation. Only the declared digest bytes are retained;
253/// this is not a finished digest or evidence of signed coverage.
254pub struct Prepared<'p, P: Digest<'p>, N, D, G> {
255    provider: &'p P,
256    reference: Reference<N, D, G>,
257    value: PreparedValue<P::Identity>,
258}
259
260/// Resolves and checks an original general XML digest instruction before a
261/// separate shared all-algorithm pass. Malformed/unsupported readings remain
262/// values; XML, policy and resource refusals remain terminal original causes.
263/// Baseline algorithm restrictions belong to the enclosing profile.
264pub type PreparationResult<'p, P, X> = binary::BinaryResult<
265    Prepared<'p, P, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
266    X,
267    Error<core::convert::Infallible, <P as Digest<'p>>::Error>,
268>;
269
270/// Checks original syntax and provider policy before creating digest state.
271pub fn prepare_xml<'p, X: OwnedBinaryXml, P: DigestProvider<'p, X>, W: Work>(
272    xml: &X,
273    reference: Reference<X::Id, X::Document, X::Generation>,
274    provider: &'p P,
275    execution: &W,
276) -> PreparationResult<'p, P, X> {
277    let access = Access::new(xml, reference.snapshot, execution);
278    let value = if let Some(answer) = preflight(&access, reference)? {
279        PreparedValue::Answer(answer)
280    } else {
281        match resolve_method(&access, reference, provider)? {
282            Resolution::Unsupported => PreparedValue::Answer(Agreement::Unsupported),
283            Resolution::Malformed => PreparedValue::Answer(Agreement::MalformedMethod),
284            Resolution::Supported(identity) => {
285                match certificate::resolved_digest::<false, X, P>(identity, provider) {
286                    Err(answer) => PreparedValue::Answer(answer),
287                    Ok(identity) => {
288                        let location = Location {
289                            snapshot: reference.snapshot,
290                            node: reference.value,
291                        };
292                        let expected = binary::owned(xml, location, execution)
293                            .map_err(|cause| binary::Error::Access { location, cause })?
294                            .value;
295                        if let Some(width) = provider.width(&identity)
296                            && expected.len() != width
297                        {
298                            PreparedValue::Answer(Agreement::MalformedValue)
299                        } else {
300                            PreparedValue::Ready { identity, expected }
301                        }
302                    }
303                }
304            }
305        }
306    };
307    Ok(Prepared {
308        provider,
309        reference,
310        value,
311    })
312}
313
314impl<'p, P: Digest<'p>, N: Copy + Eq, D: Copy + Eq, G: Copy + Eq> Prepared<'p, P, N, D, G> {
315    /// Compares the actual completed shared output without another XML decode,
316    /// method resolution, hash state, source pass or identity/output clone.
317    /// Reusing a stale XML generation or another provider is processing refusal.
318    pub fn check_finished<X: Xml<Id = N, Document = D, Generation = G>, W: Work>(
319        &self,
320        xml: &X,
321        actual: structured_cms::provider::Finished<'_, P, P::Identity, P::Output>,
322        execution: &W,
323    ) -> binary::BinaryResult<Agreement, X, Error<core::convert::Infallible, P::Error>> {
324        let access = Access::new(xml, self.reference.snapshot, execution);
325        let location = Location {
326            snapshot: self.reference.snapshot,
327            node: self.reference.value,
328        };
329        access
330            .inspect(self.reference.value, |_| ())
331            .map_err(|cause| binary::Error::Access { location, cause })?;
332        if !actual.from_provider(self.provider) {
333            return Err(binary::Error::Callback {
334                location,
335                cause: Error::ForeignDigest,
336            });
337        }
338        match &self.value {
339            PreparedValue::Answer(answer) => Ok(*answer),
340            PreparedValue::Ready { identity, expected } => compare_resolved_finished(
341                &access,
342                self.reference,
343                self.provider,
344                identity,
345                actual,
346                expected,
347            ),
348        }
349    }
350}
351
352async fn check_async_with<
353    'p,
354    const BASELINE: bool,
355    X: OwnedBinaryXml,
356    P: DigestProvider<'p, X>,
357    S: stream::AsyncSource,
358    W: Work,
359>(
360    xml: &X,
361    reference: Reference<X::Id, X::Document, X::Generation>,
362    source: &mut S,
363    provider: &'p P,
364    execution: &W,
365) -> binary::BinaryResult<Agreement, X, Error<S::Error, P::Error>> {
366    let access = Access::new(xml, reference.snapshot, execution);
367    if let Some(answer) = preflight::<X, W, S::Error, P::Error>(&access, reference)? {
368        return Ok(answer);
369    }
370    let location = Location {
371        snapshot: reference.snapshot,
372        node: reference.value,
373    };
374    let expected = access
375        .query(reference.value, |xml, snapshot, work| {
376            xml.decode_owned(snapshot, reference.value, work)
377        })
378        .map_err(|cause| binary::Error::Access { location, cause })?;
379    let identity = match resolve_method(&access, reference, provider)? {
380        Resolution::Supported(identity) => identity,
381        Resolution::Unsupported => return Ok(Agreement::Unsupported),
382        Resolution::Malformed => return Ok(Agreement::MalformedMethod),
383    };
384    let identity = match certificate::resolved_digest::<BASELINE, X, P>(identity, provider) {
385        Ok(identity) => identity,
386        Err(answer) => return Ok(answer),
387    };
388    let width = provider.width(&identity);
389    if let Some(width) = width
390        && expected.len() != width
391    {
392        return Ok(Agreement::MalformedValue);
393    }
394    let state = match start_resolved::<P, W, S::Error>(&identity, provider, execution)
395        .map_err(|cause| binary::Error::Callback { location, cause })?
396    {
397        Resolution::Supported(state) => state,
398        Resolution::Unsupported => return Ok(Agreement::Unsupported),
399        Resolution::Malformed => return Ok(Agreement::MalformedMethod),
400    };
401    let mut state = state;
402    let mut updating = Updating::<P> {
403        provider,
404        state: &mut state,
405    };
406    let result = stream::run_async(source, &mut updating, execution, "xades.digestInput", 0).await;
407    stream_result(result).map_err(|cause| binary::Error::Callback { location, cause })?;
408    // External acquisition may suspend while the XML owner changes generation.
409    // Fence finalization/comparison with the same original-node query kernel;
410    // failed source/provider causes retain precedence over a later guard.
411    access
412        .query_fallible(reference.value, |_, _, work| {
413            let output = finish_resolved(width, state, provider, work)?;
414            let agrees = provider
415                .matches(&identity, &output, &expected, work)
416                .map_err(Error::Provider)?;
417            Ok(if agrees {
418                Agreement::Matches
419            } else {
420                Agreement::Differs
421            })
422        })
423        .map_err(|cause| match cause {
424            crate::xml::AccessError::Xml(cause) => binary::Error::Callback { location, cause },
425            crate::xml::AccessError::Changed { node } => binary::Error::Access {
426                location,
427                cause: crate::xml::AccessError::Changed { node },
428            },
429            crate::xml::AccessError::Resource { node, cause } => binary::Error::Access {
430                location,
431                cause: crate::xml::AccessError::Resource { node, cause },
432            },
433        })
434}
435
436pub(crate) enum Checked<T> {
437    Answer(Agreement),
438    Value(T),
439}
440
441// Both one-shot source checking and complete material-catalogue checking use
442// this one method/declared-value kernel. Memoization cannot bypass syntax,
443// width or actual resolved-algorithm classification before provider effects.
444pub(crate) fn check_with<'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work, SE, R>(
445    xml: &X,
446    reference: Reference<X::Id, X::Document, X::Generation>,
447    provider: &'p P,
448    execution: &W,
449    visit: impl FnOnce(
450        certificate::FormatDigest<P::Identity>,
451        &[u8],
452        &W,
453    ) -> Result<R, Error<SE, P::Error>>,
454) -> binary::BinaryResult<Checked<R>, X, Error<SE, P::Error>> {
455    let access = Access::new(xml, reference.snapshot, execution);
456    if let Some(answer) = preflight(&access, reference)? {
457        return Ok(Checked::Answer(answer));
458    }
459    binary::with_octets(
460        xml,
461        Location {
462            snapshot: reference.snapshot,
463            node: reference.value,
464        },
465        execution,
466        |expected| {
467            Ok::<_, core::convert::Infallible>(check_expected(
468                &access, reference, expected, provider, visit,
469            ))
470        },
471    )
472    .map_err(|error| error.map_callback(|never| match never {}))?
473}
474
475fn preflight<X: BinaryXml, W: Work, SE, PE>(
476    access: &Access<'_, X, W>,
477    reference: Reference<X::Id, X::Document, X::Generation>,
478) -> binary::BinaryResult<Option<Agreement>, X, Error<SE, PE>> {
479    certificate::preflight_method(access, reference.method).map_err(|cause| binary::Error::Access {
480        location: Location {
481            snapshot: reference.snapshot,
482            node: reference.method,
483        },
484        cause,
485    })
486}
487
488fn check_expected<'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work, SE, R>(
489    access: &Access<'_, X, W>,
490    reference: Reference<X::Id, X::Document, X::Generation>,
491    expected: &[u8],
492    provider: &'p P,
493    visit: impl FnOnce(
494        certificate::FormatDigest<P::Identity>,
495        &[u8],
496        &W,
497    ) -> Result<R, Error<SE, P::Error>>,
498) -> binary::BinaryResult<Checked<R>, X, Error<SE, P::Error>> {
499    let execution = access.execution();
500    let resolved = resolve_method(access, reference, provider)?;
501    let identity = match resolved {
502        Resolution::Supported(identity) => identity,
503        Resolution::Unsupported => return Ok(Checked::Answer(Agreement::Unsupported)),
504        Resolution::Malformed => return Ok(Checked::Answer(Agreement::MalformedMethod)),
505    };
506    let identity = match certificate::baseline_digest::<X, P>(identity, provider) {
507        certificate::Expected::Ready(identity) => identity,
508        certificate::Expected::Answer(answer) => return Ok(Checked::Answer(answer)),
509    };
510    if let Some(width) = provider.width(identity.identity())
511        && expected.len() != width
512    {
513        return Ok(Checked::Answer(Agreement::MalformedValue));
514    }
515    visit(identity, expected, execution)
516        .map(Checked::Value)
517        .map_err(|cause| binary::Error::Callback {
518            location: Location {
519                snapshot: reference.snapshot,
520                node: reference.value,
521            },
522            cause,
523        })
524}
525
526fn resolve_method<'p, X: Xml, P: DigestProvider<'p, X>, W: Work, SE>(
527    access: &Access<'_, X, W>,
528    reference: Reference<X::Id, X::Document, X::Generation>,
529    provider: &P,
530) -> binary::BinaryResult<Resolution<P::Identity>, X, Error<SE, P::Error>> {
531    let location = Location {
532        snapshot: reference.snapshot,
533        node: reference.method,
534    };
535    access
536        .query(reference.method, |_, _, _| {
537            Ok(provider.resolve(access, reference.method))
538        })
539        .map_err(|cause| binary::Error::Access { location, cause })?
540        .map_err(|cause| binary::Error::Callback {
541            location,
542            cause: Error::Provider(cause),
543        })
544}
545
546pub(crate) fn run_resolved<'p, P: Digest<'p>, S: Source, W: Work>(
547    identity: &certificate::FormatDigest<P::Identity>,
548    expected: &[u8],
549    source: &mut S,
550    provider: &'p P,
551    execution: &W,
552) -> Result<Agreement, Error<S::Error, P::Error>> {
553    let width = provider.width(identity.identity());
554    if let Some(width) = width
555        && expected.len() != width
556    {
557        return Ok(Agreement::MalformedValue);
558    }
559    let output = match compute_resolved::<P, S, W>(identity, source, provider, execution)? {
560        Resolution::Supported(output) => output,
561        Resolution::Unsupported => return Ok(Agreement::Unsupported),
562        Resolution::Malformed => return Ok(Agreement::MalformedMethod),
563    };
564    execution.charge_work(1).map_err(Error::Resource)?;
565    let agrees = provider
566        .matches(identity.identity(), &output, expected, execution)
567        .map_err(Error::Provider)?;
568    Ok(if agrees {
569        Agreement::Matches
570    } else {
571        Agreement::Differs
572    })
573}
574
575type Computation<O, SE, PE> = Result<Resolution<O>, Error<SE, PE>>;
576
577pub(crate) fn compute_resolved<'p, P: Digest<'p>, S: Source, W: Work>(
578    identity: &certificate::FormatDigest<P::Identity>,
579    source: &mut S,
580    provider: &'p P,
581    execution: &W,
582) -> Computation<P::Output, S::Error, P::Error> {
583    let width = provider.width(identity.identity());
584    let mut state =
585        match start_resolved::<P, W, S::Error>(identity.identity(), provider, execution)? {
586            Resolution::Supported(state) => state,
587            Resolution::Unsupported => return Ok(Resolution::Unsupported),
588            Resolution::Malformed => return Ok(Resolution::Malformed),
589        };
590    let mut updating = Updating::<P> {
591        provider,
592        state: &mut state,
593    };
594    stream_result(stream::run(
595        source,
596        &mut updating,
597        execution,
598        "xades.digestInput",
599        0,
600    ))?;
601    finish_resolved(width, state, provider, execution).map(Resolution::Supported)
602}
603
604fn stream_result<SE, PE>(result: Result<(), StreamError<SE, PE>>) -> Result<(), Error<SE, PE>> {
605    match result {
606        Ok(()) => {}
607        Err(StreamError::Source(cause)) => return Err(Error::Source(cause)),
608        Err(StreamError::Callback(cause)) => return Err(Error::Provider(cause)),
609        Err(StreamError::Processing(cause)) => return Err(Error::Stream(cause)),
610    }
611    Ok(())
612}
613
614fn start_resolved<'p, P: Digest<'p>, W: Work, SE>(
615    identity: &P::Identity,
616    provider: &'p P,
617    execution: &W,
618) -> Result<Resolution<P::State>, Error<SE, P::Error>> {
619    execution.charge_work(1).map_err(Error::Resource)?;
620    provider.start(identity, execution).map_err(Error::Provider)
621}
622
623fn finish_resolved<'p, P: Digest<'p>, W: Work, SE>(
624    width: Option<usize>,
625    state: P::State,
626    provider: &'p P,
627    execution: &W,
628) -> Result<P::Output, Error<SE, P::Error>> {
629    execution.charge_work(1).map_err(Error::Resource)?;
630    let output = provider.finish(state, execution).map_err(Error::Provider)?;
631    if let Some(expected) = width
632        && output.as_ref().len() != expected
633    {
634        return Err(Error::OutputWidth {
635            expected,
636            actual: output.as_ref().len(),
637        });
638    }
639    Ok(output)
640}