// Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::str::FromStr; use thiserror::Error; #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] pub struct Header { name: String, /// The raw header field value as bytes. /// /// HTTP allows field values to contain any visible ASCII characters and /// arbitrary 0x80–0xFF bytes (`obs-text`). Unlike field *names*, field /// values are not guaranteed to be valid UTF-8. /// /// See also . value: Vec, } impl Header { pub fn new(name: N, value: V) -> Self where N: Into, V: Into>, { Self { name: name.into(), value: value.into(), } } #[must_use] pub fn is_allowed_for_response(&self) -> bool { !matches!( self.name.as_str(), "connection" | "host" | "keep-alive" | "proxy-connection" | "te" | "transfer-encoding" | "upgrade" ) } #[must_use] pub fn name(&self) -> &str { &self.name } #[must_use] pub fn value(&self) -> &[u8] { &self.value } /// Try to interpret the header value as UTF-8. /// /// # Errors /// /// Returns an error if the value contains invalid UTF-8. pub fn value_utf8(&self) -> Result<&str, std::str::Utf8Error> { std::str::from_utf8(&self.value) } } impl, U: AsRef<[u8]>> PartialEq<(T, U)> for Header { fn eq(&self, other: &(T, U)) -> bool { self.name == other.0.as_ref() && self.value == other.1.as_ref() } } pub trait HeadersExt<'h> { fn contains_header, U: AsRef<[u8]>>(self, name: T, value: U) -> bool; fn find_header + 'h>(self, name: T) -> Option<&'h Header>; } impl<'h, H> HeadersExt<'h> for H where H: IntoIterator + 'h, { fn contains_header, U: AsRef<[u8]>>(self, name: T, value: U) -> bool { let (name, value) = (name.as_ref(), value.as_ref()); self.into_iter().any(|h| h == &(name, value)) } fn find_header + 'h>(self, name: T) -> Option<&'h Header> { let name = name.as_ref(); self.into_iter().find(|h| h.name == name) } } #[derive(Debug, PartialEq, Eq, Error)] pub enum FromStrError { #[error("Header string missing colon")] MissingColon, #[error("Header string missing name")] MissingName, } impl FromStr for Header { type Err = FromStrError; fn from_str(s: &str) -> Result { let (seperator, _) = s .match_indices(':') // Pseudo-header starts with a ':'. Skip it. .find(|(i, _)| *i != 0) .ok_or(FromStrError::MissingColon)?; let name = s[..seperator].trim().to_ascii_lowercase(); if name.is_empty() { return Err(FromStrError::MissingName); } let value = s[seperator + 1..].trim(); Ok(Self::new(name, value)) } } #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { use std::str::from_utf8; use super::*; #[test] fn from_str_valid() { let header = Header::from_str("Content-Type: text/html").unwrap(); assert_eq!(header.name(), "content-type"); assert_eq!(header.value(), b"text/html"); let header = Header::from_str("Content-Type:").unwrap(); assert_eq!(header.name(), "content-type"); assert_eq!(header.value(), b""); } #[test] fn from_str_pseudo_header() { let header = Header::from_str(":scheme: https").unwrap(); assert_eq!(header.name(), ":scheme"); assert_eq!(header.value(), b"https"); } #[test] fn from_str_pseudo_header_with_value_with_colon() { let header = Header::from_str(":some: he:ader").unwrap(); assert_eq!(header.name(), ":some"); assert_eq!(header.value(), b"he:ader"); } #[test] fn from_str_errors() { assert_eq!(Header::from_str("").err(), Some(FromStrError::MissingColon)); assert_eq!( Header::from_str(" : text/html").err(), Some(FromStrError::MissingName) ); } #[test] fn non_utf8_header_value() { // Create a header with non-UTF-8 bytes in the value let non_utf8_bytes: Vec = vec![0xFF, 0xFE, 0xFD, 0x80, 0x81]; let header = Header::new("custom-header", non_utf8_bytes.as_slice()); assert_eq!(header.name(), "custom-header"); assert_eq!(header.value(), non_utf8_bytes.as_slice()); // Verify that the value is indeed not valid UTF-8 assert!(from_utf8(header.value()).is_err()); // Test the value_utf8() helper method assert!(header.value_utf8().is_err()); } #[test] fn non_ascii_header_value() { // Create a header with non-ASCII but valid UTF-8 bytes (emoji: rocket + star) let emoji_bytes = b"\xF0\x9F\x9A\x80\xF0\x9F\x8C\x9F"; let header = Header::new("emoji-header", emoji_bytes.as_slice()); assert_eq!(header.name(), "emoji-header"); assert_eq!(header.value(), emoji_bytes.as_slice()); // Verify we can convert back to UTF-8 let emoji_str = from_utf8(header.value()).unwrap(); assert_eq!(emoji_str, from_utf8(emoji_bytes).unwrap()); // Test the value_utf8() helper method assert_eq!(header.value_utf8().unwrap(), emoji_str); } #[test] fn value_utf8_method() { // Test value_utf8() with valid UTF-8 let header = Header::new("content-type", "text/html"); assert_eq!(header.value_utf8().unwrap(), "text/html"); // Test value_utf8() with bytes let header2 = Header::new("test", b"value"); assert_eq!(header2.value_utf8().unwrap(), "value"); } #[test] fn header_comparison_with_bytes() { let header = Header::new("test", b"value"); assert_eq!(header, ("test", b"value".as_ref())); assert_ne!(header, ("test", b"other".as_ref())); assert_ne!(header, ("other", b"value".as_ref())); } #[test] fn is_allowed_for_response() { assert!(Header::new("content-type", "text/html").is_allowed_for_response()); for name in ["connection", "host", "keep-alive", "transfer-encoding"] { assert!(!Header::new(name, "x").is_allowed_for_response()); } } #[test] fn headers_ext() { let headers = [ Header::new("content-type", "text/html"), Header::new("x-custom", "value"), ]; assert!(headers.iter().contains_header("content-type", "text/html")); assert!(!headers.iter().contains_header("content-type", "other")); assert!(!headers.iter().contains_header("missing", "value")); assert_eq!( headers.iter().find_header("x-custom").unwrap().name(), "x-custom" ); assert!(headers.iter().find_header("missing").is_none()); } }