Skip to main content

watermelon_proto/headers/
value.rs

1use alloc::string::String;
2use core::{
3    fmt::{self, Display},
4    str::FromStr,
5};
6
7use bytes::Bytes;
8use bytestring::ByteString;
9
10/// A string that can be used to represent an header value
11///
12/// `HeaderValue` contains raw bytes that are guaranteed [^1] to
13/// contain a valid header value that meets the following requirements:
14///
15/// * The value does not contain `\r` or `\n` bytes
16///
17/// The value may be empty and may contain arbitrary non-UTF-8 bytes:
18/// the NATS server relays header values verbatim, and other clients
19/// (like `nats.go`) are able to produce such values.
20///
21/// `HeaderValue` can be constructed from [`HeaderValue::from_static`],
22/// [`HeaderValue::from_bytes`], or any of the `TryFrom` implementations.
23///
24/// [^1]: Because [`HeaderValue::from_dangerous_value`] is safe to call,
25///       unsafe code must not assume any of the above invariants.
26#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
27pub struct HeaderValue {
28    inner: Bytes,
29}
30
31impl fmt::Debug for HeaderValue {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.debug_tuple("HeaderValue").field(&self.inner).finish()
34    }
35}
36
37impl HeaderValue {
38    /// Construct `HeaderValue` from a static string
39    ///
40    /// # Panics
41    ///
42    /// Will panic if `value` isn't a valid `HeaderValue`
43    #[must_use]
44    pub fn from_static(value: &'static str) -> Self {
45        Self::try_from(ByteString::from_static(value)).expect("invalid HeaderValue")
46    }
47
48    /// Construct a `HeaderValue` from raw bytes, validating the value
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if `value` contains `\r` or `\n`.
53    pub fn from_bytes(value: &[u8]) -> Result<Self, HeaderValueValidateError> {
54        validate_header_value_bytes(value)?;
55        Ok(Self::from_dangerous_value(Bytes::copy_from_slice(value)))
56    }
57
58    /// Construct a `HeaderValue` from a `Bytes`, without checking invariants
59    ///
60    /// This method bypasses invariants checks implemented by [`HeaderValue::from_static`],
61    /// [`HeaderValue::from_bytes`], and all `TryFrom` implementations.
62    ///
63    /// # Security
64    ///
65    /// While calling this method can eliminate the runtime performance cost of
66    /// checking the value, constructing `HeaderValue` with an invalid value and
67    /// then calling the NATS server with it can cause serious security issues.
68    /// When in doubt use [`HeaderValue::from_static`], [`HeaderValue::from_bytes`],
69    /// or any of the `TryFrom` implementations.
70    #[must_use]
71    #[expect(
72        clippy::missing_panics_doc,
73        reason = "The header validation is only made in debug"
74    )]
75    pub fn from_dangerous_value(value: Bytes) -> Self {
76        if cfg!(debug_assertions) {
77            validate_header_value_bytes(&value).expect("invalid HeaderValue");
78        }
79        Self { inner: value }
80    }
81
82    /// Try to view this header value as a `&str`
83    ///
84    /// # Errors
85    ///
86    /// Returns [`HeaderValueToStrError`] if the value contains bytes that are
87    /// not valid UTF-8.
88    pub fn to_str(&self) -> Result<&str, HeaderValueToStrError> {
89        core::str::from_utf8(&self.inner).map_err(|_| HeaderValueToStrError { _priv: () })
90    }
91
92    /// Returns the raw bytes of the header value
93    #[must_use]
94    pub fn as_bytes(&self) -> &[u8] {
95        &self.inner
96    }
97}
98
99impl Display for HeaderValue {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        if let Ok(s) = self.to_str() {
102            f.write_str(s)
103        } else {
104            // Fallback: show escaped bytes
105            for &b in &self.inner {
106                if b.is_ascii_graphic() || b == b' ' {
107                    write!(f, "{}", b as char)?;
108                } else {
109                    write!(f, "\\x{b:02x}")?;
110                }
111            }
112            Ok(())
113        }
114    }
115}
116
117impl TryFrom<ByteString> for HeaderValue {
118    type Error = HeaderValueValidateError;
119
120    fn try_from(value: ByteString) -> Result<Self, Self::Error> {
121        validate_header_value_bytes(value.as_bytes())?;
122        Ok(Self::from_dangerous_value(value.into_bytes()))
123    }
124}
125
126impl FromStr for HeaderValue {
127    type Err = HeaderValueValidateError;
128
129    fn from_str(value: &str) -> Result<Self, Self::Err> {
130        validate_header_value_bytes(value.as_bytes())?;
131        Ok(Self::from_dangerous_value(Bytes::copy_from_slice(
132            value.as_bytes(),
133        )))
134    }
135}
136
137impl TryFrom<String> for HeaderValue {
138    type Error = HeaderValueValidateError;
139
140    fn try_from(value: String) -> Result<Self, Self::Error> {
141        validate_header_value_bytes(value.as_bytes())?;
142        Ok(Self::from_dangerous_value(value.into()))
143    }
144}
145
146impl AsRef<[u8]> for HeaderValue {
147    fn as_ref(&self) -> &[u8] {
148        self.as_bytes()
149    }
150}
151
152/// An error returned when a header value contains non-UTF-8 bytes
153#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
154#[error("header value contains non-UTF-8 bytes")]
155pub struct HeaderValueToStrError {
156    _priv: (),
157}
158
159/// An error encountered while validating [`HeaderValue`]
160#[derive(Debug, thiserror::Error)]
161pub enum HeaderValueValidateError {
162    /// The value contains `\r` or `\n`
163    #[error("HeaderValue contained an illegal character")]
164    IllegalCharacter,
165}
166
167fn validate_header_value_bytes(value: &[u8]) -> Result<(), HeaderValueValidateError> {
168    // The NATS server treats the header block as an opaque blob and relays
169    // header values verbatim: any bytes — including obs-text (0x80–0xFF),
170    // invalid UTF-8 and empty values — pass through. It also doesn't enforce
171    // per-field length limits; the header block as a whole only counts
172    // towards the maximum payload size.
173    // `\r` and `\n` must still be rejected as they would break the
174    // line-based framing of the header block itself.
175    for &b in value {
176        if b == b'\r' || b == b'\n' {
177            return Err(HeaderValueValidateError::IllegalCharacter);
178        }
179    }
180
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186    use alloc::string::ToString;
187
188    use claims::assert_matches;
189
190    use super::{HeaderValue, HeaderValueValidateError};
191
192    #[test]
193    fn valid_header_values() {
194        let values = [
195            "value",
196            "value with spaces",
197            "value\twith\ttabs",
198            "non-àscíì ütf-8",
199        ];
200        for value in values {
201            let header_value: HeaderValue = value.parse().unwrap();
202            assert_eq!(header_value.as_bytes(), value.as_bytes());
203            assert_eq!(header_value.to_str().unwrap(), value);
204            assert_eq!(header_value.to_string(), value);
205        }
206    }
207
208    #[test]
209    fn empty_header_value() {
210        let header_value = HeaderValue::from_bytes(b"").unwrap();
211        assert_eq!(header_value.as_bytes(), b"");
212        assert_eq!(header_value.to_str().unwrap(), "");
213    }
214
215    #[test]
216    fn non_utf8_header_value() {
217        let header_value = HeaderValue::from_bytes(b"\x00\x80\xffabc").unwrap();
218        assert_eq!(header_value.as_bytes(), b"\x00\x80\xffabc");
219        assert!(header_value.to_str().is_err());
220        assert_eq!(header_value.to_string(), "\\x00\\x80\\xffabc");
221    }
222
223    #[test]
224    fn invalid_header_values() {
225        let values: [&[u8]; 5] = [b"\r", b"\n", b"value\r", b"value\nvalue", b"value\r\n"];
226        for value in values {
227            assert_matches!(
228                HeaderValue::from_bytes(value),
229                Err(HeaderValueValidateError::IllegalCharacter),
230                "{value:?}"
231            );
232        }
233    }
234}