Skip to main content

cloud_sdk/transport/header/
response.rs

1use core::fmt;
2
3use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes, sanitize_value};
4
5use super::{
6    HeaderError, HeaderSensitivity, MAX_RESPONSE_HEADER_BYTES, MAX_RESPONSE_HEADERS,
7    encoded_line_len, validate_name, validate_response_value,
8};
9use crate::transport::retained::{ProtectedRequestId, RetainedMetadataError};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12struct HeaderRange {
13    name_start: u16,
14    name_len: u8,
15    value_start: u16,
16    value_len: u16,
17    sensitivity: HeaderSensitivity,
18}
19
20const EMPTY_RANGE: HeaderRange = HeaderRange {
21    name_start: 0,
22    name_len: 0,
23    value_start: 0,
24    value_len: 0,
25    sensitivity: HeaderSensitivity::Public,
26};
27
28/// Borrowed view into one retained response header.
29///
30/// Ordinary equality is intentionally unavailable because the value may be
31/// sensitive.
32///
33/// ```compile_fail
34/// use cloud_sdk::transport::{HeaderSensitivity, ResponseHeaders};
35///
36/// let mut storage = [0_u8; 4096];
37/// let mut headers = ResponseHeaders::new(&mut storage);
38/// headers.try_push("x-secret", b"secret", HeaderSensitivity::Sensitive).unwrap();
39/// let header = headers.get("x-secret").unwrap();
40/// let _ = header == header;
41/// ```
42#[derive(Clone, Copy)]
43pub struct ResponseHeader<'a> {
44    name: &'a str,
45    value: &'a [u8],
46    sensitivity: HeaderSensitivity,
47}
48
49impl<'a> ResponseHeader<'a> {
50    /// Returns the exact retained name.
51    #[must_use]
52    pub const fn name(self) -> &'a str {
53        self.name
54    }
55
56    /// Returns the exact retained value bytes.
57    #[must_use]
58    pub const fn value(self) -> &'a [u8] {
59        self.value
60    }
61
62    /// Returns the value sensitivity.
63    #[must_use]
64    pub const fn sensitivity(self) -> HeaderSensitivity {
65        self.sensitivity
66    }
67}
68
69impl fmt::Debug for ResponseHeader<'_> {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        formatter
72            .debug_struct("ResponseHeader")
73            .field("name", &self.name)
74            .field("value", &"[redacted]")
75            .field("sensitivity", &self.sensitivity)
76            .finish()
77    }
78}
79
80/// Caller-storage-backed fixed-capacity response-header metadata.
81///
82/// Ordinary equality is intentionally unavailable because retained values may
83/// be sensitive.
84///
85/// ```compile_fail
86/// use cloud_sdk::transport::ResponseHeaders;
87///
88/// let mut storage = [0_u8; 4096];
89/// let headers = ResponseHeaders::new(&mut storage);
90/// let _ = headers == headers;
91/// ```
92pub struct ResponseHeaders<'storage> {
93    bytes: SecretBuffer<'storage>,
94    ranges: [HeaderRange; MAX_RESPONSE_HEADERS],
95    bytes_len: usize,
96    count: usize,
97    encoded_len: usize,
98}
99
100impl<'storage> ResponseHeaders<'storage> {
101    /// Creates an empty response-header collection.
102    #[must_use]
103    pub fn new(storage: &'storage mut [u8]) -> Self {
104        sanitize_bytes(storage);
105        Self {
106            bytes: SecretBuffer::new(storage),
107            ranges: [EMPTY_RANGE; MAX_RESPONSE_HEADERS],
108            bytes_len: 0,
109            count: 0,
110            encoded_len: 0,
111        }
112    }
113
114    /// Appends one header atomically after validating every bound.
115    pub fn try_push(
116        &mut self,
117        name: &str,
118        value: &[u8],
119        sensitivity: HeaderSensitivity,
120    ) -> Result<(), HeaderError> {
121        validate_name(name)?;
122        validate_response_value(value)?;
123        if self.count >= MAX_RESPONSE_HEADERS {
124            return Err(HeaderError::TooManyHeaders);
125        }
126        if self
127            .iter()
128            .any(|header| header.name.eq_ignore_ascii_case(name))
129        {
130            return Err(HeaderError::DuplicateName);
131        }
132        let line_len = encoded_line_len(name.len(), value.len())?;
133        let encoded_len = self
134            .encoded_len
135            .checked_add(line_len)
136            .ok_or(HeaderError::AggregateTooLarge)?;
137        if encoded_len > MAX_RESPONSE_HEADER_BYTES {
138            return Err(HeaderError::AggregateTooLarge);
139        }
140        let stored_len = name
141            .len()
142            .checked_add(value.len())
143            .ok_or(HeaderError::AggregateTooLarge)?;
144        let end = self
145            .bytes_len
146            .checked_add(stored_len)
147            .ok_or(HeaderError::AggregateTooLarge)?;
148        let name_start =
149            u16::try_from(self.bytes_len).map_err(|_| HeaderError::AggregateTooLarge)?;
150        let value_start = self
151            .bytes_len
152            .checked_add(name.len())
153            .and_then(|offset| u16::try_from(offset).ok())
154            .ok_or(HeaderError::AggregateTooLarge)?;
155        let range = HeaderRange {
156            name_start,
157            name_len: u8::try_from(name.len()).map_err(|_| HeaderError::NameTooLong)?,
158            value_start,
159            value_len: u16::try_from(value.len()).map_err(|_| HeaderError::ValueTooLong)?,
160            sensitivity,
161        };
162        let next_count = self
163            .count
164            .checked_add(1)
165            .ok_or(HeaderError::TooManyHeaders)?;
166        let slot = self
167            .ranges
168            .get_mut(self.count)
169            .ok_or(HeaderError::TooManyHeaders)?;
170        let region = self
171            .bytes
172            .as_mut_slice()
173            .get_mut(self.bytes_len..end)
174            .ok_or(HeaderError::AggregateTooLarge)?;
175        let (name_out, value_out) = region.split_at_mut(name.len());
176        name_out.copy_from_slice(name.as_bytes());
177        value_out.copy_from_slice(value);
178        *slot = range;
179        self.bytes_len = end;
180        self.encoded_len = encoded_len;
181        self.count = next_count;
182        Ok(())
183    }
184
185    /// Returns the retained header count.
186    #[must_use]
187    pub const fn len(&self) -> usize {
188        self.count
189    }
190
191    /// Reports whether no headers are retained.
192    #[must_use]
193    pub const fn is_empty(&self) -> bool {
194        self.count == 0
195    }
196
197    /// Returns the aggregate encoded field-line length.
198    #[must_use]
199    pub const fn encoded_len(&self) -> usize {
200        self.encoded_len
201    }
202
203    /// Iterates in retained order.
204    pub fn iter(&self) -> impl Iterator<Item = ResponseHeader<'_>> {
205        self.ranges
206            .get(..self.count)
207            .unwrap_or_default()
208            .iter()
209            .filter_map(|range| self.view(*range))
210    }
211
212    /// Finds a retained header by ASCII case-insensitive name.
213    #[must_use]
214    pub fn get(&self, name: &str) -> Option<ResponseHeader<'_>> {
215        self.iter()
216            .find(|header| header.name.eq_ignore_ascii_case(name))
217    }
218
219    /// Creates a deliberate second cleanup-owning copy in caller storage.
220    ///
221    /// Both the source and returned collection independently clear their
222    /// complete caller-owned storage on drop.
223    pub fn retain_copy_into<'destination>(
224        &self,
225        destination: &'destination mut [u8],
226    ) -> Result<ResponseHeaders<'destination>, HeaderError> {
227        let mut retained = ResponseHeaders::new(destination);
228        for header in self.iter() {
229            retained.try_push(header.name(), header.value(), header.sensitivity())?;
230        }
231        Ok(retained)
232    }
233
234    pub(crate) fn hide_request_id(
235        &mut self,
236    ) -> Result<Option<ProtectedRequestId>, RetainedMetadataError> {
237        let found = self
238            .ranges
239            .get(..self.count)
240            .unwrap_or_default()
241            .iter()
242            .copied()
243            .enumerate()
244            .find(|(_, range)| {
245                self.view(*range)
246                    .is_some_and(|header| header.name.eq_ignore_ascii_case("x-request-id"))
247            });
248        let Some((index, range)) = found else {
249            return Ok(None);
250        };
251        let protected = ProtectedRequestId::new(range.value_start, range.value_len)?;
252        self.remove(index, range)?;
253        Ok(Some(protected))
254    }
255
256    pub(crate) fn protected_value(&self, request_id: ProtectedRequestId) -> Option<&[u8]> {
257        let start = usize::from(request_id.start());
258        let end = start.checked_add(usize::from(request_id.len()))?;
259        self.bytes.as_slice().get(start..end)
260    }
261
262    pub(crate) fn clear_protected(&mut self, request_id: ProtectedRequestId) {
263        let start = usize::from(request_id.start());
264        let end = start.saturating_add(usize::from(request_id.len()));
265        sanitize_bytes(
266            self.bytes
267                .as_mut_slice()
268                .get_mut(start..end)
269                .unwrap_or_default(),
270        );
271    }
272
273    fn remove(&mut self, index: usize, range: HeaderRange) -> Result<(), RetainedMetadataError> {
274        let name_len = usize::from(range.name_len);
275        let value_len = usize::from(range.value_len);
276        let removed_encoded_len = name_len
277            .checked_add(value_len)
278            .and_then(|length| length.checked_add(4))
279            .ok_or(RetainedMetadataError::RequestIdTooLong)?;
280        let new_encoded_len = self
281            .encoded_len
282            .checked_sub(removed_encoded_len)
283            .ok_or(RetainedMetadataError::RequestIdTooLong)?;
284        let new_count = self
285            .count
286            .checked_sub(1)
287            .ok_or(RetainedMetadataError::RequestIdTooLong)?;
288        let tail_start = index
289            .checked_add(1)
290            .ok_or(RetainedMetadataError::RequestIdTooLong)?;
291        self.ranges.copy_within(tail_start..self.count, index);
292        if let Some(last) = self.ranges.get_mut(new_count) {
293            clear_range(last);
294        }
295        self.count = new_count;
296        self.encoded_len = new_encoded_len;
297        Ok(())
298    }
299
300    pub(crate) fn clear(&mut self) {
301        sanitize_bytes(self.bytes.as_mut_slice());
302        for range in &mut self.ranges {
303            clear_range(range);
304        }
305        sanitize_value(&mut self.bytes_len);
306        sanitize_value(&mut self.count);
307        sanitize_value(&mut self.encoded_len);
308    }
309
310    fn view(&self, range: HeaderRange) -> Option<ResponseHeader<'_>> {
311        let name_start = usize::from(range.name_start);
312        let name_end = name_start.checked_add(usize::from(range.name_len))?;
313        let value_start = usize::from(range.value_start);
314        let value_end = value_start.checked_add(usize::from(range.value_len))?;
315        let name = self
316            .bytes
317            .as_slice()
318            .get(name_start..name_end)
319            .and_then(|bytes| core::str::from_utf8(bytes).ok())?;
320        let value = self.bytes.as_slice().get(value_start..value_end)?;
321        Some(ResponseHeader {
322            name,
323            value,
324            sensitivity: range.sensitivity,
325        })
326    }
327}
328
329fn clear_range(range: &mut HeaderRange) {
330    sanitize_value(&mut range.name_start);
331    sanitize_value(&mut range.name_len);
332    sanitize_value(&mut range.value_start);
333    sanitize_value(&mut range.value_len);
334    range.sensitivity = HeaderSensitivity::Public;
335}
336
337impl Drop for ResponseHeaders<'_> {
338    fn drop(&mut self) {
339        self.clear();
340    }
341}
342
343impl fmt::Debug for ResponseHeaders<'_> {
344    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345        formatter
346            .debug_struct("ResponseHeaders")
347            .field("count", &self.count)
348            .field("encoded_len", &self.encoded_len)
349            .field("values", &"[redacted]")
350            .finish()
351    }
352}
353
354#[cfg(test)]
355mod cleanup_tests {
356    use super::{HeaderSensitivity, ResponseHeaders};
357
358    #[test]
359    fn complete_header_storage_and_ranges_clear() {
360        let mut storage = [0xa5_u8; 128];
361        let mut headers = ResponseHeaders::new(&mut storage);
362        assert!(
363            headers
364                .try_push(
365                    "x-request-id",
366                    b"sensitive-id",
367                    HeaderSensitivity::Sensitive
368                )
369                .is_ok()
370        );
371        headers.clear();
372        assert!(headers.bytes.as_slice().iter().all(|byte| *byte == 0));
373        assert!(headers.ranges.iter().all(|range| {
374            range.name_start == 0
375                && range.name_len == 0
376                && range.value_start == 0
377                && range.value_len == 0
378                && range.sensitivity == HeaderSensitivity::Public
379        }));
380        assert_eq!(
381            (headers.bytes_len, headers.count, headers.encoded_len),
382            (0, 0, 0)
383        );
384    }
385
386    #[test]
387    fn hiding_request_id_preserves_stable_storage_and_removes_visibility() {
388        let mut storage = [0xa5_u8; 128];
389        let mut headers = ResponseHeaders::new(&mut storage);
390        assert!(
391            headers
392                .try_push("date", b"1", HeaderSensitivity::Public)
393                .is_ok()
394        );
395        assert!(
396            headers
397                .try_push(
398                    "x-request-id",
399                    b"sensitive-id",
400                    HeaderSensitivity::Sensitive
401                )
402                .is_ok()
403        );
404        assert!(
405            headers
406                .try_push("x-public", b"ok", HeaderSensitivity::Public)
407                .is_ok()
408        );
409
410        let pointer = headers.bytes.as_slice().as_ptr();
411        let protected = headers.hide_request_id();
412        assert!(matches!(protected, Ok(Some(_))));
413        assert_eq!(headers.len(), 2);
414        assert_eq!(headers.encoded_len(), 23);
415        assert!(headers.get("x-request-id").is_none());
416        let mut retained = headers.iter();
417        assert!(
418            retained
419                .next()
420                .is_some_and(|header| { header.name() == "date" && header.value() == b"1" })
421        );
422        assert!(
423            retained
424                .next()
425                .is_some_and(|header| { header.name() == "x-public" && header.value() == b"ok" })
426        );
427        assert!(retained.next().is_none());
428        drop(retained);
429        assert_eq!(headers.bytes.as_slice().as_ptr(), pointer);
430        assert_eq!(headers.bytes_len, 39);
431        let Ok(Some(protected)) = protected else {
432            return;
433        };
434        let mut snapshot_storage = [0xa5_u8; 128];
435        let Ok(snapshot) = headers.retain_copy_into(&mut snapshot_storage) else {
436            return;
437        };
438        assert_eq!(snapshot.len(), 2);
439        assert!(
440            snapshot
441                .bytes
442                .as_slice()
443                .windows(b"sensitive-id".len())
444                .all(|window| window != b"sensitive-id")
445        );
446        assert_eq!(
447            headers.protected_value(protected),
448            Some(b"sensitive-id".as_slice())
449        );
450        headers.clear_protected(protected);
451        assert!(
452            headers
453                .bytes
454                .as_slice()
455                .windows(b"sensitive-id".len())
456                .all(|window| window != b"sensitive-id")
457        );
458    }
459}