Skip to main content

cratestack_client_rust/
codec.rs

1use cratestack_codec_cbor::CborCodec;
2#[cfg(feature = "codec-json")]
3use cratestack_codec_json::JsonCodec;
4use cratestack_core::{CratestackCodec, CratestackError};
5use serde::de::DeserializeOwned;
6
7pub(crate) const CBOR_SEQUENCE_CONTENT_TYPE: &str = "application/cbor-seq";
8
9pub trait HttpClientCodec: CratestackCodec {
10    fn accept_header_value(&self) -> &'static str;
11
12    fn sequence_accept_header_value(&self) -> &'static str;
13
14    /// The response payload types this codec reads **inside a COSE
15    /// envelope**, as a `Cratestack-Payload-Accept` value: one to eight
16    /// lowercase `type/subtype` entries joined by `", "`, in order of
17    /// preference. The default is [`CONTENT_TYPE`](cratestack_core::CratestackCodec::CONTENT_TYPE)
18    /// alone, which suits a codec that writes and reads the same type;
19    /// override it for an asymmetric one (a form in, JSON out). A sealed
20    /// call sends the header only when this is not `application/cbor`, and
21    /// the client refuses any answer sealed under a type it does not list.
22    fn payload_accept(&self) -> &'static str {
23        Self::CONTENT_TYPE
24    }
25
26    fn decode_response<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CratestackError>
27    where
28        T: DeserializeOwned;
29
30    fn decode_sequence_response<T>(
31        &self,
32        content_type: &str,
33        body: &[u8],
34    ) -> Result<Vec<T>, CratestackError>
35    where
36        T: DeserializeOwned;
37}
38
39impl HttpClientCodec for CborCodec {
40    fn accept_header_value(&self) -> &'static str {
41        // With `codec-json` disabled the client cannot actually
42        // decode a JSON response — drop it from the Accept header
43        // so a server with content negotiation chooses CBOR (or
44        // fails on no acceptable representation) rather than
45        // sending JSON the client will then error on.
46        #[cfg(feature = "codec-json")]
47        {
48            "application/cbor, application/json"
49        }
50        #[cfg(not(feature = "codec-json"))]
51        {
52            "application/cbor"
53        }
54    }
55
56    fn sequence_accept_header_value(&self) -> &'static str {
57        #[cfg(feature = "codec-json")]
58        {
59            "application/cbor-seq, application/cbor, application/json"
60        }
61        #[cfg(not(feature = "codec-json"))]
62        {
63            "application/cbor-seq, application/cbor"
64        }
65    }
66
67    fn decode_response<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CratestackError>
68    where
69        T: DeserializeOwned,
70    {
71        if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
72            self.decode(body)
73        } else {
74            #[cfg(feature = "codec-json")]
75            if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
76                return JsonCodec.decode(body);
77            }
78            Err(CratestackError::Codec(format!(
79                "unsupported response Content-Type {content_type}"
80            )))
81        }
82    }
83
84    fn decode_sequence_response<T>(
85        &self,
86        content_type: &str,
87        body: &[u8],
88    ) -> Result<Vec<T>, CratestackError>
89    where
90        T: DeserializeOwned,
91    {
92        if media_type_matches(content_type, CBOR_SEQUENCE_CONTENT_TYPE) {
93            decode_cbor_sequence(body)
94        } else if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
95            self.decode(body)
96        } else {
97            #[cfg(feature = "codec-json")]
98            if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
99                return JsonCodec.decode(body);
100            }
101            Err(CratestackError::Codec(format!(
102                "unsupported response Content-Type {content_type}"
103            )))
104        }
105    }
106}
107
108#[cfg(feature = "codec-json")]
109impl HttpClientCodec for JsonCodec {
110    fn accept_header_value(&self) -> &'static str {
111        "application/json, application/cbor"
112    }
113
114    fn sequence_accept_header_value(&self) -> &'static str {
115        "application/cbor-seq, application/json, application/cbor"
116    }
117
118    fn decode_response<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CratestackError>
119    where
120        T: DeserializeOwned,
121    {
122        if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
123            self.decode(body)
124        } else if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
125            CborCodec.decode(body)
126        } else {
127            Err(CratestackError::Codec(format!(
128                "unsupported response Content-Type {content_type}"
129            )))
130        }
131    }
132
133    fn decode_sequence_response<T>(
134        &self,
135        content_type: &str,
136        body: &[u8],
137    ) -> Result<Vec<T>, CratestackError>
138    where
139        T: DeserializeOwned,
140    {
141        if media_type_matches(content_type, CBOR_SEQUENCE_CONTENT_TYPE) {
142            decode_cbor_sequence(body)
143        } else if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
144            self.decode(body)
145        } else if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
146            CborCodec.decode(body)
147        } else {
148            Err(CratestackError::Codec(format!(
149                "unsupported response Content-Type {content_type}"
150            )))
151        }
152    }
153}
154
155pub(crate) fn media_type_matches(candidate: &str, expected: &str) -> bool {
156    candidate.split(';').next().unwrap_or(candidate).trim() == expected
157}
158
159pub(crate) fn decode_cbor_sequence<T>(bytes: &[u8]) -> Result<Vec<T>, CratestackError>
160where
161    T: DeserializeOwned,
162{
163    let mut values = Vec::new();
164    let mut offset = 0usize;
165    while offset < bytes.len() {
166        // One explicit depth check per item (cratestack#1315), before serde
167        // recurses into it.
168        cratestack_codec_cbor::check_depth(
169            &bytes[offset..],
170            cratestack_codec_cbor::DEFAULT_MAX_DECODE_DEPTH,
171        )?;
172        let mut deserializer = minicbor_serde::Deserializer::new(&bytes[offset..]);
173        values.push(T::deserialize(&mut deserializer).map_err(|error| {
174            CratestackError::Codec(format!("failed to decode CBOR sequence body: {error}"))
175        })?);
176        let consumed = deserializer.decoder().position();
177        if consumed == 0 {
178            return Err(CratestackError::Codec(
179                "failed to decode CBOR sequence body: decoder made no progress".to_owned(),
180            ));
181        }
182        offset += consumed;
183    }
184    Ok(values)
185}