Skip to main content

aws_smithy_runtime_api/client/unit_of_work/
checksums.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Events that a generated client's response checksum validation reports to observers.
7//!
8//! Observers are registered in the config bag under [`SharedResponseChecksumObserver`]. Because
9//! these types are in a 1.x crate, a client and an observer built against different releases of
10//! the crates around them still find each other.
11
12use aws_smithy_types::config_bag::{Storable, StoreAppend};
13use std::fmt;
14use std::sync::Arc;
15
16/// Receives the response checksum validation events of one operation.
17///
18/// A generated client's response checksum validation calls [`on_event`](Self::on_event). Each
19/// attempt that reaches the checksum decision produces either a
20/// [`NotValidated`](ChecksumEventKind::NotValidated) event, or a
21/// [`Validating`](ChecksumEventKind::Validating) event followed by one ending per wrapped body:
22/// [`Matched`](ChecksumEventKind::Matched), [`Mismatched`](ChecksumEventKind::Mismatched),
23/// [`Errored`](ChecksumEventKind::Errored) or [`Dropped`](ChecksumEventKind::Dropped). Events are
24/// sent on the task that drives the attempt or reads the body, and `on_event` must not block.
25pub trait ObserveResponseChecksum: Send + Sync + fmt::Debug {
26    /// Receives one event.
27    fn on_event(&self, event: &ChecksumEvent<'_>);
28}
29
30/// One response checksum validation event.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct ChecksumEvent<'a> {
33    attempt: u32,
34    kind: ChecksumEventKind<'a>,
35}
36
37impl<'a> ChecksumEvent<'a> {
38    /// Creates an event that happened during `attempt`, numbered from 1.
39    pub fn new(attempt: u32, kind: ChecksumEventKind<'a>) -> Self {
40        Self { attempt, kind }
41    }
42
43    /// The attempt the event happened during, numbered from 1.
44    pub fn attempt(&self) -> u32 {
45        self.attempt
46    }
47
48    /// What happened.
49    pub fn kind(&self) -> &ChecksumEventKind<'a> {
50        &self.kind
51    }
52}
53
54/// What happened in a [`ChecksumEvent`]. Later releases may add kinds.
55#[derive(Clone, Debug, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum ChecksumEventKind<'a> {
58    /// The response will not be validated.
59    NotValidated(NotValidated),
60    /// A validator was attached to the response body.
61    Validating(Validating),
62    /// The body's data ended and its checksum matched.
63    Matched(Match<'a>),
64    /// The body's data ended and its checksum did not match. This is sent before the body
65    /// yields its mismatch error.
66    Mismatched(Mismatch<'a>),
67    /// The body yielded an error before its data ended.
68    Errored,
69    /// The body was dropped before its data ended.
70    Dropped,
71}
72
73/// The data of a [`ChecksumEventKind::NotValidated`] event.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct NotValidated {
76    reason: ChecksumSkip,
77}
78
79impl NotValidated {
80    /// Creates the data for a response that will not be validated because of `reason`.
81    pub fn new(reason: ChecksumSkip) -> Self {
82        Self { reason }
83    }
84
85    /// Why the response will not be validated.
86    pub fn reason(&self) -> ChecksumSkip {
87        self.reason
88    }
89}
90
91/// The data of a [`ChecksumEventKind::Validating`] event.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct Validating {
94    algorithm: &'static str,
95}
96
97impl Validating {
98    /// Creates the data for a body that is validated with `algorithm`.
99    pub fn new(algorithm: &'static str) -> Self {
100        Self { algorithm }
101    }
102
103    /// The lowercase name of the checksum algorithm: `"crc32"`, `"crc32c"`, `"crc64nvme"`,
104    /// `"sha1"` or `"sha256"` today. Later releases may add names.
105    pub fn algorithm(&self) -> &'static str {
106        self.algorithm
107    }
108}
109
110/// The data of a [`ChecksumEventKind::Matched`] event.
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct Match<'a> {
113    computed: &'a str,
114}
115
116impl<'a> Match<'a> {
117    /// Creates the data for a body whose checksum matched. `computed` is base64.
118    pub fn new(computed: &'a str) -> Self {
119        Self { computed }
120    }
121
122    /// The checksum computed from the body, in base64.
123    pub fn computed(&self) -> &'a str {
124        self.computed
125    }
126}
127
128/// The data of a [`ChecksumEventKind::Mismatched`] event.
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct Mismatch<'a> {
131    expected: &'a str,
132    computed: &'a str,
133}
134
135impl<'a> Mismatch<'a> {
136    /// Creates the data for a body whose checksum did not match. Both values are base64.
137    pub fn new(expected: &'a str, computed: &'a str) -> Self {
138        Self { expected, computed }
139    }
140
141    /// The checksum from the response header, in base64.
142    pub fn expected(&self) -> &'a str {
143        self.expected
144    }
145
146    /// The checksum computed from the body, in base64.
147    pub fn computed(&self) -> &'a str {
148        self.computed
149    }
150}
151
152/// Why a response's checksum will not be validated.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154#[non_exhaustive]
155pub enum ChecksumSkip {
156    /// The request did not enable checksum mode, and the client's `response_checksum_validation`
157    /// is `WhenRequired`.
158    Disabled,
159    /// The response has no checksum header for an algorithm the operation lists.
160    NoChecksum,
161    /// The checksum header holds a part-level value, which ends in `-N`.
162    PartLevelChecksum,
163    /// The checksum header is present but is not valid base64.
164    InvalidChecksumHeader,
165    /// The response's only checksum headers are for algorithms the client does not compute.
166    UnsupportedAlgorithm,
167    /// The response's status is not 2xx, such as a 404 or a 503. Its body is never validated.
168    ErrorResponse,
169}
170
171/// A shared [`ObserveResponseChecksum`], and the config-bag key that observers are registered
172/// under.
173///
174/// The key appends, so every registered observer receives every event.
175#[derive(Clone, Debug)]
176pub struct SharedResponseChecksumObserver(Arc<dyn ObserveResponseChecksum>);
177
178impl SharedResponseChecksumObserver {
179    /// Creates a new [`SharedResponseChecksumObserver`] from an observer.
180    pub fn new(observer: impl ObserveResponseChecksum + 'static) -> Self {
181        Self(Arc::new(observer))
182    }
183}
184
185impl ObserveResponseChecksum for SharedResponseChecksumObserver {
186    fn on_event(&self, event: &ChecksumEvent<'_>) {
187        self.0.on_event(event)
188    }
189}
190
191impl Storable for SharedResponseChecksumObserver {
192    type Storer = StoreAppend<Self>;
193}
194
195#[cfg(test)]
196mod tests {
197    use super::{
198        ChecksumEvent, ChecksumEventKind, ChecksumSkip, Match, Mismatch, NotValidated,
199        ObserveResponseChecksum, SharedResponseChecksumObserver, Validating,
200    };
201    use aws_smithy_types::config_bag::{ConfigBag, Layer};
202    use std::sync::{Arc, Mutex};
203
204    /// Records the events it receives.
205    #[derive(Clone, Debug, Default)]
206    struct Recorder(Arc<Mutex<Vec<String>>>);
207
208    impl Recorder {
209        fn record(&self, event: impl Into<String>) {
210            self.0.lock().unwrap().push(event.into());
211        }
212
213        fn events(&self) -> Vec<String> {
214            self.0.lock().unwrap().clone()
215        }
216    }
217
218    impl ObserveResponseChecksum for Recorder {
219        fn on_event(&self, event: &ChecksumEvent<'_>) {
220            let kind = match event.kind() {
221                ChecksumEventKind::NotValidated(data) => {
222                    format!("not_validated {:?}", data.reason())
223                }
224                ChecksumEventKind::Validating(data) => format!("validating {}", data.algorithm()),
225                ChecksumEventKind::Matched(data) => format!("matched {}", data.computed()),
226                ChecksumEventKind::Mismatched(data) => {
227                    format!("mismatched {} {}", data.expected(), data.computed())
228                }
229                ChecksumEventKind::Errored => "errored".to_string(),
230                ChecksumEventKind::Dropped => "dropped".to_string(),
231            };
232            self.record(format!("{} {kind}", event.attempt()));
233        }
234    }
235
236    #[test]
237    fn every_appended_observer_receives_every_event() {
238        let (first, second) = (Recorder::default(), Recorder::default());
239        let mut layer = Layer::new("test");
240        layer.store_append(SharedResponseChecksumObserver::new(first.clone()));
241        layer.store_append(SharedResponseChecksumObserver::new(second.clone()));
242        let cfg = ConfigBag::of_layers(vec![layer]);
243
244        let kinds = [
245            ChecksumEventKind::Validating(Validating::new("crc32")),
246            ChecksumEventKind::NotValidated(NotValidated::new(ChecksumSkip::NoChecksum)),
247            ChecksumEventKind::Matched(Match::new("computed")),
248            ChecksumEventKind::Mismatched(Mismatch::new("expected", "computed")),
249            ChecksumEventKind::Errored,
250            ChecksumEventKind::Dropped,
251        ];
252        for observer in cfg.load::<SharedResponseChecksumObserver>() {
253            for kind in &kinds {
254                observer.on_event(&ChecksumEvent::new(2, kind.clone()));
255            }
256        }
257
258        let expected = [
259            "2 validating crc32",
260            "2 not_validated NoChecksum",
261            "2 matched computed",
262            "2 mismatched expected computed",
263            "2 errored",
264            "2 dropped",
265        ];
266        assert_eq!(first.events(), expected);
267        assert_eq!(second.events(), expected);
268    }
269}