Skip to main content

cloud_sdk/transport/header/
mod.rs

1//! Bounded HTTP request and response header contracts.
2
3mod request;
4mod response;
5
6pub use request::{RequestHeader, RequestHeaders};
7pub use response::{ResponseHeader, ResponseHeaders};
8
9use core::cmp::Ordering;
10use core::fmt;
11use core::hash::{Hash, Hasher};
12
13use super::{ContentType, MediaType};
14
15/// Maximum header-name length admitted by the transport contract.
16pub const MAX_HEADER_NAME_BYTES: usize = 64;
17/// Maximum single header-value length admitted by the transport contract.
18pub const MAX_HEADER_VALUE_BYTES: usize = 1024;
19/// Maximum number of request headers.
20pub const MAX_REQUEST_HEADERS: usize = 32;
21/// Maximum encoded request-header bytes, excluding the final empty line.
22pub const MAX_REQUEST_HEADER_BYTES: usize = 8192;
23/// Maximum number of retained response headers.
24pub const MAX_RESPONSE_HEADERS: usize = 32;
25/// Maximum encoded response-header bytes, excluding the final empty line.
26pub const MAX_RESPONSE_HEADER_BYTES: usize = 8192;
27
28const HEADER_LINE_OVERHEAD: usize = 4;
29
30/// Whether a header value must be treated as sensitive.
31#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub enum HeaderSensitivity {
33    /// Ordinary metadata. Debug output remains redacted.
34    Public,
35    /// Credential, token, cookie, or other secret-bearing metadata.
36    Sensitive,
37}
38
39impl HeaderSensitivity {
40    /// Reports whether the value requires sensitive transport handling.
41    #[must_use]
42    pub(crate) const fn is_sensitive(self) -> bool {
43        matches!(self, Self::Sensitive)
44    }
45}
46
47/// Bounded HTTP header validation or capacity failure.
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum HeaderError {
50    /// Header names must not be empty.
51    EmptyName,
52    /// The header name exceeds [`MAX_HEADER_NAME_BYTES`].
53    NameTooLong,
54    /// The header name is not an HTTP token.
55    InvalidName,
56    /// The header value exceeds [`MAX_HEADER_VALUE_BYTES`].
57    ValueTooLong,
58    /// The header value contains an unsafe control or non-ASCII request byte.
59    InvalidValue,
60    /// A caller attempted to own an adapter- or protocol-owned request header.
61    ReservedRequestHeader,
62    /// Header names must be unique under ASCII case-insensitive comparison.
63    DuplicateName,
64    /// The header count exceeds the applicable request or response limit.
65    TooManyHeaders,
66    /// The encoded header block exceeds its aggregate byte limit.
67    AggregateTooLarge,
68    /// A generic `Content-Type` value failed typed media validation.
69    InvalidContentType,
70    /// Caller output cannot hold the complete encoded header block.
71    OutputTooSmall,
72}
73
74impl_static_error!(HeaderError,
75    Self::EmptyName => "HTTP header name is empty",
76    Self::NameTooLong => "HTTP header name exceeds the length limit",
77    Self::InvalidName => "HTTP header name is invalid",
78    Self::ValueTooLong => "HTTP header value exceeds the length limit",
79    Self::InvalidValue => "HTTP header value is invalid",
80    Self::ReservedRequestHeader => "HTTP request header ownership is reserved",
81    Self::DuplicateName => "HTTP header name is duplicated",
82    Self::TooManyHeaders => "HTTP header count exceeds the limit",
83    Self::AggregateTooLarge => "HTTP header block exceeds the byte limit",
84    Self::InvalidContentType => "HTTP content type is invalid",
85    Self::OutputTooSmall => "HTTP header output is too small",
86);
87
88/// Borrowed, validated HTTP header name.
89#[derive(Clone, Copy)]
90pub struct HeaderName<'a>(&'a str);
91
92impl<'a> HeaderName<'a> {
93    /// Validates an HTTP field name without normalizing its bytes.
94    pub fn new(value: &'a str) -> Result<Self, HeaderError> {
95        validate_name(value)?;
96        Ok(Self(value))
97    }
98
99    /// Returns the exact validated name.
100    #[must_use]
101    pub const fn as_str(self) -> &'a str {
102        self.0
103    }
104
105    /// Compares two names using HTTP's ASCII case-insensitive semantics.
106    #[must_use]
107    pub fn eq_ignore_ascii_case(self, other: &str) -> bool {
108        self.0.eq_ignore_ascii_case(other)
109    }
110}
111
112impl PartialEq for HeaderName<'_> {
113    fn eq(&self, other: &Self) -> bool {
114        self.0.eq_ignore_ascii_case(other.0)
115    }
116}
117
118impl Eq for HeaderName<'_> {}
119
120impl PartialOrd for HeaderName<'_> {
121    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
122        Some(self.cmp(other))
123    }
124}
125
126impl Ord for HeaderName<'_> {
127    fn cmp(&self, other: &Self) -> Ordering {
128        self.0
129            .bytes()
130            .map(|byte| byte.to_ascii_lowercase())
131            .cmp(other.0.bytes().map(|byte| byte.to_ascii_lowercase()))
132    }
133}
134
135impl Hash for HeaderName<'_> {
136    fn hash<H: Hasher>(&self, state: &mut H) {
137        for byte in self.0.bytes() {
138            state.write_u8(byte.to_ascii_lowercase());
139        }
140    }
141}
142
143impl fmt::Debug for HeaderName<'_> {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        formatter.debug_tuple("HeaderName").field(&self.0).finish()
146    }
147}
148
149/// Borrowed, validated request header value.
150///
151/// Ordinary equality is intentionally unavailable because values may contain
152/// secrets.
153///
154/// ```compile_fail
155/// use cloud_sdk::transport::HeaderValue;
156///
157/// let left = HeaderValue::new("secret").unwrap();
158/// let right = HeaderValue::new("secret").unwrap();
159/// let _ = left == right;
160/// ```
161#[derive(Clone, Copy)]
162pub struct HeaderValue<'a>(&'a str);
163
164impl<'a> HeaderValue<'a> {
165    /// Validates one canonical visible-ASCII request header value.
166    pub fn new(value: &'a str) -> Result<Self, HeaderError> {
167        validate_request_value(value)?;
168        Ok(Self(value))
169    }
170
171    /// Returns the exact validated value.
172    #[must_use]
173    pub const fn as_str(self) -> &'a str {
174        self.0
175    }
176
177    const fn validated(value: &'a str) -> Self {
178        Self(value)
179    }
180}
181
182impl fmt::Debug for HeaderValue<'_> {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        formatter.write_str("HeaderValue([redacted])")
185    }
186}
187
188fn validate_name(value: &str) -> Result<(), HeaderError> {
189    if value.is_empty() {
190        return Err(HeaderError::EmptyName);
191    }
192    if value.len() > MAX_HEADER_NAME_BYTES {
193        return Err(HeaderError::NameTooLong);
194    }
195    if !value.bytes().all(is_token_byte) {
196        return Err(HeaderError::InvalidName);
197    }
198    Ok(())
199}
200
201fn validate_request_value(value: &str) -> Result<(), HeaderError> {
202    if value.len() > MAX_HEADER_VALUE_BYTES {
203        return Err(HeaderError::ValueTooLong);
204    }
205    if !value.bytes().all(|byte| (b' '..=b'~').contains(&byte))
206        || value.starts_with(' ')
207        || value.ends_with(' ')
208    {
209        return Err(HeaderError::InvalidValue);
210    }
211    Ok(())
212}
213
214fn validate_response_value(value: &[u8]) -> Result<(), HeaderError> {
215    if value.len() > MAX_HEADER_VALUE_BYTES {
216        return Err(HeaderError::ValueTooLong);
217    }
218    if value.iter().any(|byte| *byte < b' ' || *byte == 0x7f) {
219        return Err(HeaderError::InvalidValue);
220    }
221    Ok(())
222}
223
224fn encoded_line_len(name_len: usize, value_len: usize) -> Result<usize, HeaderError> {
225    name_len
226        .checked_add(value_len)
227        .and_then(|len| len.checked_add(HEADER_LINE_OVERHEAD))
228        .ok_or(HeaderError::AggregateTooLarge)
229}
230
231fn is_reserved_request_name(name: HeaderName<'_>) -> bool {
232    const RESERVED: &[&str] = &[
233        "authorization",
234        "connection",
235        "content-length",
236        "host",
237        "keep-alive",
238        "proxy-authenticate",
239        "proxy-authorization",
240        "proxy-connection",
241        "te",
242        "trailer",
243        "transfer-encoding",
244        "upgrade",
245        "user-agent",
246    ];
247    RESERVED
248        .iter()
249        .any(|reserved| name.eq_ignore_ascii_case(reserved))
250}
251
252fn is_token_byte(byte: u8) -> bool {
253    byte.is_ascii_alphanumeric()
254        || matches!(
255            byte,
256            b'!' | b'#'
257                | b'$'
258                | b'%'
259                | b'&'
260                | b'\''
261                | b'*'
262                | b'+'
263                | b'-'
264                | b'.'
265                | b'^'
266                | b'_'
267                | b'`'
268                | b'|'
269                | b'~'
270        )
271}
272
273const fn typed_accept<'a>(media_type: MediaType<'a>) -> HeaderValue<'a> {
274    HeaderValue::validated(media_type.as_str())
275}
276
277const fn typed_content_type<'a>(content_type: ContentType<'a>) -> HeaderValue<'a> {
278    HeaderValue::validated(content_type.as_str())
279}
280
281#[cfg(test)]
282mod tests;