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