Skip to main content

cloud_sdk/transport/
response.rs

1//! Sealed response-buffer admission, commitment, and cleanup.
2
3use core::fmt;
4
5use crate::operation::RequestIdPolicy;
6use crate::rate_limit::RateLimit;
7
8use super::cleanup::sanitize_response_storage;
9use super::retained::{ProtectedRequestId, RetainedMetadataError, RetainedResponseMetadata};
10use super::{ResponseContentType, ResponseHeaders, ResponseStorageSanitizer, StatusCode};
11
12/// Non-sensitive interpreted response metadata captured by a transport.
13#[derive(Debug)]
14pub struct ResponseMetadata {
15    rate_limit: Option<RateLimit>,
16}
17
18impl ResponseMetadata {
19    /// Empty response metadata.
20    pub const EMPTY: Self = Self { rate_limit: None };
21
22    /// Adds validated rate-limit metadata.
23    #[must_use]
24    pub fn with_rate_limit(mut self, rate_limit: RateLimit) -> Self {
25        self.rate_limit = Some(rate_limit);
26        self
27    }
28
29    pub(crate) const fn rate_limit(&self) -> Option<RateLimit> {
30        self.rate_limit
31    }
32}
33
34/// Response-writer state violation.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ResponseWriterError {
37    /// The transport has already committed this response.
38    AlreadyCommitted,
39    /// The transport has not committed this response.
40    NotCommitted,
41    /// The committed initialized length exceeds the admitted body capacity.
42    InitializedLengthTooLarge,
43}
44
45impl_static_error!(ResponseWriterError,
46    Self::AlreadyCommitted => "response writer is already committed",
47    Self::NotCommitted => "response writer is not committed",
48    Self::InitializedLengthTooLarge => "response length exceeds admitted storage",
49);
50
51struct ResponseCommit {
52    status: StatusCode,
53    initialized_len: usize,
54    metadata: ResponseMetadata,
55}
56
57/// Exclusive transport access to one admitted caller-owned response prefix.
58///
59/// This handle is only obtainable from [`ResponseBuffer::writer`]. It cannot
60/// substitute an external or static body slice.
61pub struct ResponseWriter<'buffer> {
62    storage: &'buffer mut [u8],
63    admitted_len: usize,
64    headers: ResponseHeaders<'buffer>,
65    request_id: Option<ProtectedRequestId>,
66    commit: Option<ResponseCommit>,
67}
68
69impl<'buffer> ResponseWriter<'buffer> {
70    /// Returns the admitted response-body capacity.
71    #[must_use]
72    pub const fn body_capacity(&self) -> usize {
73        self.admitted_len
74    }
75
76    /// Returns exclusive access to the admitted prefix before commitment.
77    pub fn body_mut(&mut self) -> Result<&mut [u8], ResponseWriterError> {
78        if self.commit.is_some() {
79            return Err(ResponseWriterError::AlreadyCommitted);
80        }
81        self.storage
82            .get_mut(..self.admitted_len)
83            .ok_or(ResponseWriterError::InitializedLengthTooLarge)
84    }
85
86    /// Returns stable caller-owned response-header storage before commitment.
87    pub fn headers_mut(&mut self) -> Result<&mut ResponseHeaders<'buffer>, ResponseWriterError> {
88        if self.commit.is_some() {
89            return Err(ResponseWriterError::AlreadyCommitted);
90        }
91        Ok(&mut self.headers)
92    }
93
94    /// Returns the response headers captured so far.
95    pub fn headers(&self) -> &ResponseHeaders<'buffer> {
96        &self.headers
97    }
98
99    /// Commits status, initialized length, and bounded metadata exactly once.
100    pub fn commit(
101        &mut self,
102        status: StatusCode,
103        initialized_len: usize,
104        metadata: ResponseMetadata,
105    ) -> Result<(), ResponseWriterError> {
106        if self.commit.is_some() {
107            return Err(ResponseWriterError::AlreadyCommitted);
108        }
109        if initialized_len > self.admitted_len {
110            return Err(ResponseWriterError::InitializedLengthTooLarge);
111        }
112        self.commit = Some(ResponseCommit {
113            status,
114            initialized_len,
115            metadata,
116        });
117        Ok(())
118    }
119
120    /// Reports whether the transport committed the response.
121    #[must_use]
122    pub const fn is_committed(&self) -> bool {
123        self.commit.is_some()
124    }
125
126    fn response(&self) -> Result<TransportResponse<'_, 'buffer>, ResponseWriterError> {
127        let commit = self
128            .commit
129            .as_ref()
130            .ok_or(ResponseWriterError::NotCommitted)?;
131        let body = self
132            .storage
133            .get(..commit.initialized_len)
134            .ok_or(ResponseWriterError::InitializedLengthTooLarge)?;
135        Ok(TransportResponse::from_commit(
136            commit,
137            body,
138            &self.headers,
139            self.request_id,
140        ))
141    }
142
143    fn apply_request_id_policy(
144        &mut self,
145        policy: RequestIdPolicy,
146    ) -> Result<(), RetainedMetadataError> {
147        let request_id = self.headers.hide_request_id()?;
148        match policy {
149            RequestIdPolicy::Discard => {
150                if let Some(request_id) = request_id {
151                    self.headers.clear_protected(request_id);
152                }
153            }
154            RequestIdPolicy::Protected | RequestIdPolicy::Retain => {
155                self.request_id = request_id;
156            }
157        }
158        Ok(())
159    }
160
161    fn request_id(&self) -> Option<&[u8]> {
162        self.request_id
163            .and_then(|request_id| self.headers.protected_value(request_id))
164    }
165
166    fn retain_request_id<'destination>(
167        &mut self,
168        destination: &'destination mut [u8],
169        retention_limit: usize,
170    ) -> Result<RetainedResponseMetadata<'destination>, RetainedMetadataError> {
171        let mut retained = RetainedResponseMetadata::empty_for_core(destination);
172        let Some(request_id) = self.request_id.take() else {
173            return Ok(retained);
174        };
175        let result = {
176            let source = self
177                .headers
178                .protected_value(request_id)
179                .ok_or(RetainedMetadataError::RequestIdTooLong)?;
180            if source.len() > retention_limit {
181                Err(RetainedMetadataError::RetentionLimitExceeded)
182            } else {
183                retained.write_request_id(source)
184            }
185        };
186        self.headers.clear_protected(request_id);
187        result.map(|()| retained)
188    }
189
190    fn initialized_body(&self, initialized_len: usize) -> &[u8] {
191        self.storage.get(..initialized_len).unwrap_or_default()
192    }
193}
194
195impl fmt::Debug for ResponseWriter<'_> {
196    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197        formatter
198            .debug_struct("ResponseWriter")
199            .field("storage_capacity", &self.storage.len())
200            .field("admitted_len", &self.admitted_len)
201            .field("committed", &self.commit.is_some())
202            .field("body", &"[redacted]")
203            .finish()
204    }
205}
206
207/// Cleanup-owning admission guard around one sealed [`ResponseWriter`].
208///
209/// Core volatile-clears the complete original storage before admission and on
210/// drop. [`Self::with_additive_sanitizer`] can add a platform operation without
211/// replacing either mandatory clear.
212pub struct ResponseBuffer<'buffer> {
213    writer: ResponseWriter<'buffer>,
214    additive: Option<&'buffer dyn ResponseStorageSanitizer>,
215}
216
217impl<'buffer> ResponseBuffer<'buffer> {
218    /// Admits at most `max_body_bytes` and clears all supplied storage.
219    #[must_use]
220    pub fn new(
221        storage: &'buffer mut [u8],
222        max_body_bytes: usize,
223        header_storage: &'buffer mut [u8],
224    ) -> Self {
225        Self::construct(storage, max_body_bytes, header_storage, None)
226    }
227
228    /// Adds a platform cleanup hook between mandatory core clears.
229    #[must_use]
230    pub fn with_additive_sanitizer(
231        storage: &'buffer mut [u8],
232        max_body_bytes: usize,
233        header_storage: &'buffer mut [u8],
234        additive: &'buffer dyn ResponseStorageSanitizer,
235    ) -> Self {
236        Self::construct(storage, max_body_bytes, header_storage, Some(additive))
237    }
238
239    fn construct(
240        storage: &'buffer mut [u8],
241        max_body_bytes: usize,
242        header_storage: &'buffer mut [u8],
243        additive: Option<&'buffer dyn ResponseStorageSanitizer>,
244    ) -> Self {
245        let headers = ResponseHeaders::new(header_storage);
246        sanitize_response_storage(storage, additive);
247        Self {
248            writer: ResponseWriter {
249                admitted_len: core::cmp::min(storage.len(), max_body_bytes),
250                storage,
251                headers,
252                request_id: None,
253                commit: None,
254            },
255            additive,
256        }
257    }
258
259    /// Returns exclusive transport access to the admitted response prefix.
260    #[must_use]
261    pub const fn writer(&mut self) -> &mut ResponseWriter<'buffer> {
262        &mut self.writer
263    }
264
265    /// Inspects a committed response without allowing its body to escape.
266    pub fn with_response<R>(
267        &self,
268        inspect: impl for<'response> FnOnce(TransportResponse<'response, 'buffer>) -> R,
269    ) -> Result<R, ResponseWriterError> {
270        let response = self.writer.response()?;
271        Ok(inspect(response))
272    }
273
274    pub(crate) fn response(&self) -> Result<TransportResponse<'_, 'buffer>, ResponseWriterError> {
275        self.writer.response()
276    }
277
278    pub(crate) fn apply_request_id_policy(
279        &mut self,
280        policy: RequestIdPolicy,
281    ) -> Result<(), RetainedMetadataError> {
282        self.writer.apply_request_id_policy(policy)
283    }
284
285    pub(crate) fn request_id(&self) -> Option<&[u8]> {
286        self.writer.request_id()
287    }
288
289    pub(crate) fn retain_request_id<'destination>(
290        &mut self,
291        destination: &'destination mut [u8],
292        retention_limit: usize,
293    ) -> Result<RetainedResponseMetadata<'destination>, RetainedMetadataError> {
294        self.writer.retain_request_id(destination, retention_limit)
295    }
296
297    pub(crate) fn initialized_body(&self, initialized_len: usize) -> &[u8] {
298        self.writer.initialized_body(initialized_len)
299    }
300}
301
302impl fmt::Debug for ResponseBuffer<'_> {
303    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304        formatter
305            .debug_struct("ResponseBuffer")
306            .field("writer", &self.writer)
307            .field("additive", &self.additive.is_some())
308            .finish()
309    }
310}
311
312impl Drop for ResponseBuffer<'_> {
313    fn drop(&mut self) {
314        sanitize_response_storage(self.writer.storage, self.additive);
315    }
316}
317
318/// Borrowed view committed from an admitted writer.
319///
320/// Safe callers cannot construct a response from unrelated storage.
321///
322/// ```compile_fail
323/// use cloud_sdk::transport::{StatusCode, TransportResponse};
324///
325/// let external = b"unadmitted";
326/// let _ = TransportResponse {
327///     status: StatusCode::OK,
328///     body: external,
329///     metadata: unreachable!(),
330/// };
331/// ```
332#[derive(Clone, Copy)]
333pub struct TransportResponse<'response, 'storage> {
334    status: StatusCode,
335    body: &'response [u8],
336    metadata: &'response ResponseMetadata,
337    headers: &'response ResponseHeaders<'storage>,
338    request_id: Option<ProtectedRequestId>,
339}
340
341impl<'response, 'storage> TransportResponse<'response, 'storage> {
342    fn from_commit(
343        commit: &'response ResponseCommit,
344        body: &'response [u8],
345        headers: &'response ResponseHeaders<'storage>,
346        request_id: Option<ProtectedRequestId>,
347    ) -> Self {
348        Self {
349            status: commit.status,
350            body,
351            metadata: &commit.metadata,
352            headers,
353            request_id,
354        }
355    }
356
357    /// Returns the status code.
358    #[must_use]
359    pub const fn status(&self) -> StatusCode {
360        self.status
361    }
362
363    /// Returns initialized response body bytes.
364    #[must_use]
365    pub const fn body(&self) -> &'response [u8] {
366        self.body
367    }
368
369    /// Returns the validated response content type when supplied.
370    ///
371    /// A present malformed value is an error, never an absent header.
372    pub fn content_type(
373        &self,
374    ) -> Result<Option<ResponseContentType<'response>>, super::ContentTypeError> {
375        let Some(header) = self.headers.get("content-type") else {
376            return Ok(None);
377        };
378        let value =
379            core::str::from_utf8(header.value()).map_err(|_| super::ContentTypeError::Invalid)?;
380        ResponseContentType::new(value).map(Some)
381    }
382
383    /// Returns validated rate-limit metadata when supplied.
384    #[must_use]
385    pub const fn rate_limit(&self) -> Option<RateLimit> {
386        self.metadata.rate_limit()
387    }
388
389    /// Returns complete bounded response-header metadata.
390    #[must_use]
391    pub const fn headers(&self) -> &'response ResponseHeaders<'storage> {
392        self.headers
393    }
394
395    /// Runs a closure with a protected provider request identifier.
396    pub fn with_request_id<R>(&self, inspect: impl FnOnce(Option<&[u8]>) -> R) -> R {
397        inspect(
398            self.request_id
399                .and_then(|request_id| self.headers.protected_value(request_id)),
400        )
401    }
402}
403
404impl fmt::Debug for TransportResponse<'_, '_> {
405    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
406        formatter
407            .debug_struct("TransportResponse")
408            .field("status", &self.status)
409            .field("body_len", &self.body.len())
410            .field("body", &"[redacted]")
411            .field("metadata", &self.metadata)
412            .field("headers", &self.headers)
413            .field("request_id", &"[redacted]")
414            .finish()
415    }
416}