use alloc::string::String;
use core::{
fmt::{self, Display},
str::FromStr,
};
use bytes::Bytes;
use bytestring::ByteString;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct HeaderValue {
inner: Bytes,
}
impl fmt::Debug for HeaderValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("HeaderValue").field(&self.inner).finish()
}
}
impl HeaderValue {
#[must_use]
pub fn from_static(value: &'static str) -> Self {
Self::try_from(ByteString::from_static(value)).expect("invalid HeaderValue")
}
pub fn from_bytes(value: &[u8]) -> Result<Self, HeaderValueValidateError> {
validate_header_value_bytes(value)?;
Ok(Self::from_dangerous_value(Bytes::copy_from_slice(value)))
}
#[must_use]
#[expect(
clippy::missing_panics_doc,
reason = "The header validation is only made in debug"
)]
pub fn from_dangerous_value(value: Bytes) -> Self {
if cfg!(debug_assertions) {
validate_header_value_bytes(&value).expect("invalid HeaderValue");
}
Self { inner: value }
}
pub fn to_str(&self) -> Result<&str, HeaderValueToStrError> {
core::str::from_utf8(&self.inner).map_err(|_| HeaderValueToStrError { _priv: () })
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.inner
}
}
impl Display for HeaderValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Ok(s) = self.to_str() {
f.write_str(s)
} else {
for &b in &self.inner {
if b.is_ascii_graphic() || b == b' ' {
write!(f, "{}", b as char)?;
} else {
write!(f, "\\x{b:02x}")?;
}
}
Ok(())
}
}
}
impl TryFrom<ByteString> for HeaderValue {
type Error = HeaderValueValidateError;
fn try_from(value: ByteString) -> Result<Self, Self::Error> {
validate_header_value_bytes(value.as_bytes())?;
Ok(Self::from_dangerous_value(value.into_bytes()))
}
}
impl FromStr for HeaderValue {
type Err = HeaderValueValidateError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
validate_header_value_bytes(value.as_bytes())?;
Ok(Self::from_dangerous_value(Bytes::copy_from_slice(
value.as_bytes(),
)))
}
}
impl TryFrom<String> for HeaderValue {
type Error = HeaderValueValidateError;
fn try_from(value: String) -> Result<Self, Self::Error> {
validate_header_value_bytes(value.as_bytes())?;
Ok(Self::from_dangerous_value(value.into()))
}
}
impl AsRef<[u8]> for HeaderValue {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("header value contains non-UTF-8 bytes")]
pub struct HeaderValueToStrError {
_priv: (),
}
#[derive(Debug, thiserror::Error)]
pub enum HeaderValueValidateError {
#[error("HeaderValue contained an illegal character")]
IllegalCharacter,
}
fn validate_header_value_bytes(value: &[u8]) -> Result<(), HeaderValueValidateError> {
for &b in value {
if b == b'\r' || b == b'\n' {
return Err(HeaderValueValidateError::IllegalCharacter);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use claims::assert_matches;
use super::{HeaderValue, HeaderValueValidateError};
#[test]
fn valid_header_values() {
let values = [
"value",
"value with spaces",
"value\twith\ttabs",
"non-àscíì ütf-8",
];
for value in values {
let header_value: HeaderValue = value.parse().unwrap();
assert_eq!(header_value.as_bytes(), value.as_bytes());
assert_eq!(header_value.to_str().unwrap(), value);
assert_eq!(header_value.to_string(), value);
}
}
#[test]
fn empty_header_value() {
let header_value = HeaderValue::from_bytes(b"").unwrap();
assert_eq!(header_value.as_bytes(), b"");
assert_eq!(header_value.to_str().unwrap(), "");
}
#[test]
fn non_utf8_header_value() {
let header_value = HeaderValue::from_bytes(b"\x00\x80\xffabc").unwrap();
assert_eq!(header_value.as_bytes(), b"\x00\x80\xffabc");
assert!(header_value.to_str().is_err());
assert_eq!(header_value.to_string(), "\\x00\\x80\\xffabc");
}
#[test]
fn invalid_header_values() {
let values: [&[u8]; 5] = [b"\r", b"\n", b"value\r", b"value\nvalue", b"value\r\n"];
for value in values {
assert_matches!(
HeaderValue::from_bytes(value),
Err(HeaderValueValidateError::IllegalCharacter),
"{value:?}"
);
}
}
}