watermelon_proto/headers/
value.rs1use alloc::string::String;
2use core::{
3 fmt::{self, Display},
4 str::FromStr,
5};
6
7use bytes::Bytes;
8use bytestring::ByteString;
9
10#[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 #[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 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 #[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 pub fn to_str(&self) -> Result<&str, HeaderValueToStrError> {
89 core::str::from_utf8(&self.inner).map_err(|_| HeaderValueToStrError { _priv: () })
90 }
91
92 #[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 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#[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#[derive(Debug, thiserror::Error)]
161pub enum HeaderValueValidateError {
162 #[error("HeaderValue contained an illegal character")]
164 IllegalCharacter,
165}
166
167fn validate_header_value_bytes(value: &[u8]) -> Result<(), HeaderValueValidateError> {
168 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}