Skip to main content

cloud_sdk/transport/
response.rs

1//! Sealed response-buffer admission, commitment, and cleanup.
2
3use core::fmt;
4
5use crate::rate_limit::RateLimit;
6
7use super::{ResponseContentType, ResponseHeaders, ResponseStorageSanitizer, StatusCode};
8
9/// Bounded response metadata captured by a transport.
10#[derive(Clone, Copy, Debug)]
11pub struct ResponseMetadata {
12    content_type: Option<ResponseContentType>,
13    rate_limit: Option<RateLimit>,
14    headers: ResponseHeaders,
15}
16
17impl ResponseMetadata {
18    /// Empty response metadata.
19    pub const EMPTY: Self = Self {
20        content_type: None,
21        rate_limit: None,
22        headers: ResponseHeaders::new(),
23    };
24
25    /// Adds a validated response content type.
26    #[must_use]
27    pub const fn with_content_type(mut self, content_type: ResponseContentType) -> Self {
28        self.content_type = Some(content_type);
29        self
30    }
31
32    /// Adds validated rate-limit metadata.
33    #[must_use]
34    pub const fn with_rate_limit(mut self, rate_limit: RateLimit) -> Self {
35        self.rate_limit = Some(rate_limit);
36        self
37    }
38
39    /// Adds complete bounded response headers.
40    #[must_use]
41    pub const fn with_headers(mut self, headers: ResponseHeaders) -> Self {
42        self.headers = headers;
43        self
44    }
45}
46
47/// Response-writer state violation.
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum ResponseWriterError {
50    /// The transport has already committed this response.
51    AlreadyCommitted,
52    /// The transport has not committed this response.
53    NotCommitted,
54    /// The committed initialized length exceeds the admitted body capacity.
55    InitializedLengthTooLarge,
56}
57
58impl_static_error!(ResponseWriterError,
59    Self::AlreadyCommitted => "response writer is already committed",
60    Self::NotCommitted => "response writer is not committed",
61    Self::InitializedLengthTooLarge => "response length exceeds admitted storage",
62);
63
64#[derive(Clone, Copy, Debug)]
65struct ResponseCommit {
66    status: StatusCode,
67    initialized_len: usize,
68    metadata: ResponseMetadata,
69}
70
71/// Exclusive transport access to one admitted caller-owned response prefix.
72///
73/// This handle is only obtainable from [`ResponseBuffer::writer`]. It cannot
74/// substitute an external or static body slice.
75pub struct ResponseWriter<'buffer> {
76    storage: &'buffer mut [u8],
77    admitted_len: usize,
78    commit: Option<ResponseCommit>,
79}
80
81impl ResponseWriter<'_> {
82    /// Returns the admitted response-body capacity.
83    #[must_use]
84    pub const fn body_capacity(&self) -> usize {
85        self.admitted_len
86    }
87
88    /// Returns exclusive access to the admitted prefix before commitment.
89    pub fn body_mut(&mut self) -> Result<&mut [u8], ResponseWriterError> {
90        if self.commit.is_some() {
91            return Err(ResponseWriterError::AlreadyCommitted);
92        }
93        self.storage
94            .get_mut(..self.admitted_len)
95            .ok_or(ResponseWriterError::InitializedLengthTooLarge)
96    }
97
98    /// Commits status, initialized length, and bounded metadata exactly once.
99    pub fn commit(
100        &mut self,
101        status: StatusCode,
102        initialized_len: usize,
103        metadata: ResponseMetadata,
104    ) -> Result<(), ResponseWriterError> {
105        if self.commit.is_some() {
106            return Err(ResponseWriterError::AlreadyCommitted);
107        }
108        if initialized_len > self.admitted_len {
109            return Err(ResponseWriterError::InitializedLengthTooLarge);
110        }
111        self.commit = Some(ResponseCommit {
112            status,
113            initialized_len,
114            metadata,
115        });
116        Ok(())
117    }
118
119    /// Reports whether the transport committed the response.
120    #[must_use]
121    pub const fn is_committed(&self) -> bool {
122        self.commit.is_some()
123    }
124
125    fn response(&self) -> Result<TransportResponse<'_>, ResponseWriterError> {
126        let commit = self.commit.ok_or(ResponseWriterError::NotCommitted)?;
127        let body = self
128            .storage
129            .get(..commit.initialized_len)
130            .ok_or(ResponseWriterError::InitializedLengthTooLarge)?;
131        Ok(TransportResponse::from_commit(commit, body))
132    }
133
134    fn initialized_body(&self, initialized_len: usize) -> &[u8] {
135        self.storage.get(..initialized_len).unwrap_or_default()
136    }
137}
138
139impl fmt::Debug for ResponseWriter<'_> {
140    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141        formatter
142            .debug_struct("ResponseWriter")
143            .field("storage_capacity", &self.storage.len())
144            .field("admitted_len", &self.admitted_len)
145            .field("committed", &self.commit.is_some())
146            .field("body", &"[redacted]")
147            .finish()
148    }
149}
150
151/// Cleanup-owning admission guard around one sealed [`ResponseWriter`].
152///
153/// The complete original storage is sanitized before admission and again when
154/// this guard is dropped, including bytes outside the admitted prefix.
155pub struct ResponseBuffer<'buffer, 'sanitizer, S: ResponseStorageSanitizer + ?Sized> {
156    writer: ResponseWriter<'buffer>,
157    sanitizer: &'sanitizer S,
158}
159
160impl<'buffer, 'sanitizer, S> ResponseBuffer<'buffer, 'sanitizer, S>
161where
162    S: ResponseStorageSanitizer + ?Sized,
163{
164    /// Admits at most `max_body_bytes` from caller storage and clears all
165    /// supplied storage before transport use.
166    #[must_use]
167    pub fn new(
168        storage: &'buffer mut [u8],
169        max_body_bytes: usize,
170        sanitizer: &'sanitizer S,
171    ) -> Self {
172        sanitizer.sanitize_response_storage(storage);
173        Self {
174            writer: ResponseWriter {
175                admitted_len: core::cmp::min(storage.len(), max_body_bytes),
176                storage,
177                commit: None,
178            },
179            sanitizer,
180        }
181    }
182
183    /// Returns exclusive transport access to the admitted response prefix.
184    #[must_use]
185    pub const fn writer(&mut self) -> &mut ResponseWriter<'buffer> {
186        &mut self.writer
187    }
188
189    /// Inspects a committed response without allowing its body borrow to
190    /// escape the closure.
191    pub fn with_response<R>(
192        &self,
193        inspect: impl for<'response> FnOnce(TransportResponse<'response>) -> R,
194    ) -> Result<R, ResponseWriterError> {
195        let response = self.writer.response()?;
196        Ok(inspect(response))
197    }
198
199    pub(crate) fn response(&self) -> Result<TransportResponse<'_>, ResponseWriterError> {
200        self.writer.response()
201    }
202
203    pub(crate) fn initialized_body(&self, initialized_len: usize) -> &[u8] {
204        self.writer.initialized_body(initialized_len)
205    }
206}
207
208impl<S> fmt::Debug for ResponseBuffer<'_, '_, S>
209where
210    S: ResponseStorageSanitizer + ?Sized,
211{
212    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213        formatter
214            .debug_struct("ResponseBuffer")
215            .field("writer", &self.writer)
216            .finish()
217    }
218}
219
220impl<S> Drop for ResponseBuffer<'_, '_, S>
221where
222    S: ResponseStorageSanitizer + ?Sized,
223{
224    fn drop(&mut self) {
225        self.sanitizer
226            .sanitize_response_storage(self.writer.storage);
227    }
228}
229
230/// Borrowed view of response bytes committed from an admitted writer.
231///
232/// Safe callers cannot construct a response from unrelated storage.
233///
234/// ```compile_fail
235/// use cloud_sdk::rate_limit::RateLimit;
236/// use cloud_sdk::transport::{
237///     ResponseContentType, ResponseHeaders, StatusCode, TransportResponse,
238/// };
239///
240/// let external = b"unadmitted";
241/// let _ = TransportResponse {
242///     status: StatusCode::OK,
243///     body: external,
244///     content_type: None::<ResponseContentType>,
245///     rate_limit: None::<RateLimit>,
246///     headers: ResponseHeaders::new(),
247/// };
248/// ```
249#[derive(Clone, Copy)]
250pub struct TransportResponse<'buffer> {
251    status: StatusCode,
252    body: &'buffer [u8],
253    content_type: Option<ResponseContentType>,
254    rate_limit: Option<RateLimit>,
255    headers: ResponseHeaders,
256}
257
258impl<'buffer> TransportResponse<'buffer> {
259    const fn from_commit(commit: ResponseCommit, body: &'buffer [u8]) -> Self {
260        Self {
261            status: commit.status,
262            body,
263            content_type: commit.metadata.content_type,
264            rate_limit: commit.metadata.rate_limit,
265            headers: commit.metadata.headers,
266        }
267    }
268
269    /// Returns the status code.
270    #[must_use]
271    pub const fn status(&self) -> StatusCode {
272        self.status
273    }
274
275    /// Returns the initialized response body bytes.
276    #[must_use]
277    pub const fn body(&self) -> &'buffer [u8] {
278        self.body
279    }
280
281    /// Returns the validated response content type when supplied.
282    #[must_use]
283    pub const fn content_type(&self) -> Option<ResponseContentType> {
284        self.content_type
285    }
286
287    /// Returns validated rate-limit metadata when supplied.
288    #[must_use]
289    pub const fn rate_limit(&self) -> Option<RateLimit> {
290        self.rate_limit
291    }
292
293    /// Returns complete bounded response-header metadata.
294    #[must_use]
295    pub const fn headers(&self) -> &ResponseHeaders {
296        &self.headers
297    }
298}
299
300impl fmt::Debug for TransportResponse<'_> {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        formatter
303            .debug_struct("TransportResponse")
304            .field("status", &self.status)
305            .field("body_len", &self.body.len())
306            .field("body", &"[redacted]")
307            .field("content_type", &self.content_type)
308            .field("rate_limit", &self.rate_limit)
309            .field("headers", &self.headers)
310            .finish()
311    }
312}