//! Case-insensitive HTTP header map with insertion-order preservation.
//!
//! [`HeaderMap`] stores header names in a case-insensitive manner while
//! preserving the insertion order of distinct header names. Multiple
//! values for the same header (e.g. `Set-Cookie`) are grouped under a
//! single entry via [`HeaderMap::append`].
//!
//! Header values are backed by [`bytes::Bytes`] to enable zero-copy
//! slicing when parsing directly from a network buffer.
use bytes::Bytes;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::iter::FromIterator;
// ---------------------------------------------------------------------------
// HeaderName
// ---------------------------------------------------------------------------
/// A case-insensitive HTTP header name.
///
/// Two `HeaderName` values are considered equal regardless of ASCII
/// casing (`content-type` == `Content-Type` == `CONTENT-TYPE`).
/// The original casing supplied at construction time is preserved for
/// display purposes.
#[derive(Debug, Clone)]
pub struct HeaderName {
/// The original-casing representation (used for `Display`).
original: String,
/// Lower-cased representation (used for `Hash` / `Eq`).
lower: String,
}
impl HeaderName {
/// Create a `HeaderName` from a string (compatibility constructor).
#[inline]
pub fn new(name: &str) -> Self {
Self::from_string(name.to_owned())
}
/// Create a `HeaderName` from an owned `String`.
#[inline]
pub fn from_string(name: String) -> Self {
let lower = name.to_ascii_lowercase();
Self {
original: name,
lower,
}
}
/// Return the original-casing name (compatibility alias).
#[inline]
pub fn original(&self) -> &str {
&self.original
}
/// Create a `HeaderName` from a string slice.
#[inline]
pub fn from_static(name: &'static str) -> Self {
Self {
lower: name.to_ascii_lowercase(),
original: name.to_owned(),
}
}
/// Create a `HeaderName` without copying the input string.
///
/// Returns an error if the name contains characters that are illegal
/// in HTTP header field-names (anything outside the range `0x21-0x7E`
/// excluding the delimiters `()<>@,;:\"/[]?={}`).
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, InvalidHeaderName> {
if !bytes.iter().all(|&b| is_valid_header_name_byte(b)) {
return Err(InvalidHeaderName);
}
let original = String::from_utf8_lossy(bytes).into_owned();
Ok(Self::from_string(original))
}
/// Return the header name as a lowercase `&str`.
#[inline]
pub fn as_str(&self) -> &str {
&self.lower
}
/// Return the original-casing representation.
#[inline]
pub fn as_original(&self) -> &str {
&self.original
}
/// Consume the `HeaderName` and return the original-casing `String`.
#[inline]
pub fn into_string(self) -> String {
self.original
}
/// Consume the `HeaderName` and return the lowercase `String`.
#[inline]
pub fn into_lower(self) -> String {
self.lower
}
}
/// Error returned when a header name contains invalid characters.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidHeaderName;
impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid HTTP header name")
}
}
impl std::error::Error for InvalidHeaderName {}
impl fmt::Display for HeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.original)
}
}
impl PartialEq for HeaderName {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.lower == other.lower
}
}
impl Eq for HeaderName {}
impl Hash for HeaderName {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.lower.hash(state);
}
}
impl From<&str> for HeaderName {
fn from(s: &str) -> Self {
Self::from_string(s.to_owned())
}
}
impl From<String> for HeaderName {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
// ---------------------------------------------------------------------------
// HeaderValue
// ---------------------------------------------------------------------------
/// A single HTTP header value backed by [`Bytes`].
///
/// The value is stored as raw bytes and is assumed to be valid
/// ISO-8859-1 (the default encoding for HTTP/1.1 header values).
/// UTF-8 content is the common case and is handled efficiently.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HeaderValue {
inner: Bytes,
}
impl HeaderValue {
/// Create a value from a string (compatibility constructor).
#[inline]
pub fn new(value: &str) -> Self {
Self::from_string(value.to_owned())
}
/// Create a value from an owned `String` (zero-allocation if the
/// caller no longer needs the `String`).
#[inline]
pub fn from_string(value: String) -> Self {
Self {
inner: Bytes::from(value),
}
}
/// Return the value as a string slice (lossy).
#[inline]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.inner).unwrap_or("")
}
/// Parse the value as a given type.
pub fn parse<T: std::str::FromStr>(&self) -> Result<T, T::Err> {
self.as_str().parse()
}
/// Return a lowercased version of the value.
pub fn to_lowercase(&self) -> String {
self.as_str().to_lowercase()
}
/// Create a value from a static string slice (no allocation).
#[inline]
pub fn from_static(value: &'static str) -> Self {
Self {
inner: Bytes::from_static(value.as_bytes()),
}
}
/// Create a value from raw bytes.
#[inline]
pub fn from_bytes(value: impl Into<Bytes>) -> Self {
Self {
inner: value.into(),
}
}
/// Return the value as a byte slice.
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.inner
}
/// Try to interpret the value as a UTF-8 string.
#[inline]
pub fn to_str(&self) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(&self.inner)
}
/// Consume and return the inner [`Bytes`].
#[inline]
pub fn into_bytes(self) -> Bytes {
self.inner
}
/// Return `true` if the value is empty.
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Return the length of the value in bytes.
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
}
impl fmt::Display for HeaderValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match std::str::from_utf8(&self.inner) {
Ok(s) => f.write_str(s),
// Lossy: render replacement chars for non-UTF-8 bytes.
Err(_) => {
for &b in &self.inner {
if b.is_ascii_graphic() || b == b' ' {
f.write_str(std::str::from_utf8(&[b]).unwrap())?;
} else {
write!(f, "\\x{:02x}", b)?;
}
}
Ok(())
}
}
}
}
impl From<String> for HeaderValue {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
impl From<&String> for HeaderValue {
fn from(s: &String) -> Self {
Self::from_string(s.clone())
}
}
impl From<&str> for HeaderValue {
fn from(s: &str) -> Self {
Self::from_string(s.to_owned())
}
}
impl PartialEq<&str> for HeaderValue {
fn eq(&self, other: &&str) -> bool {
self.as_str().eq_ignore_ascii_case(other)
}
}
impl From<Bytes> for HeaderValue {
fn from(b: Bytes) -> Self {
Self { inner: b }
}
}
impl From<Vec<u8>> for HeaderValue {
fn from(v: Vec<u8>) -> Self {
Self {
inner: Bytes::from(v),
}
}
}
impl<'a> From<&'a [u8]> for HeaderValue {
fn from(s: &'a [u8]) -> Self {
Self {
inner: Bytes::copy_from_slice(s),
}
}
}
// ---------------------------------------------------------------------------
// HeaderMap
// ---------------------------------------------------------------------------
/// An ordered, case-insensitive map of HTTP header names to values.
///
/// Insertion order of distinct header names is preserved. Multiple
/// values for the same name (added via [`append`](HeaderMap::append))
/// are grouped together under a single entry.
///
/// # Examples
///
/// ```
/// use veld::http::headers::{HeaderMap, HeaderName, HeaderValue};
///
/// let mut map = HeaderMap::new();
/// map.insert("Content-Type", "text/html");
/// map.append("Set-Cookie", "a=1");
/// map.append("Set-Cookie", "b=2");
///
/// assert_eq!(map.get("content-type").unwrap().to_str().unwrap(), "text/html");
/// assert_eq!(map.get_all("set-cookie").len(), 2);
/// ```
#[derive(Debug, Clone)]
pub struct HeaderMap {
/// Ordered list of (name, values) pairs.
///
/// HTTP messages carry few headers, so a linear scan with a
/// case-insensitive compare is faster than a hash map here -- and,
/// crucially, lookups need not allocate a `HeaderName` for the key,
/// which removes several heap allocations from every request.
entries: Vec<(HeaderName, Vec<HeaderValue>)>,
}
impl HeaderMap {
/// Create an empty `HeaderMap`.
#[inline]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// Create an empty `HeaderMap` with the given capacity for distinct
/// header names.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
entries: Vec::with_capacity(capacity),
}
}
/// Return the number of distinct header names in the map.
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
/// Return `true` if the map contains no headers.
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
// -- insertion / removal -------------------------------------------------
/// Insert a header, replacing any existing values for the same name.
///
/// If the name already exists its position in the iteration order is
/// preserved and the old values are replaced. Returns the previous
/// values if any.
pub fn insert<N, V>(&mut self, name: N, value: V) -> Option<Vec<HeaderValue>>
where
N: Into<HeaderName>,
V: Into<HeaderValue>,
{
let name = name.into();
let value = value.into();
// Both `as_str()` values are lowercase, so `==` is a case-insensitive
// name match.
if let Some(entry) = self
.entries
.iter_mut()
.find(|(n, _)| n.as_str() == name.as_str())
{
Some(std::mem::replace(&mut entry.1, vec![value]))
} else {
self.entries.push((name, vec![value]));
None
}
}
/// Append a value to an existing header, or create a new entry.
///
/// This is the correct way to add multiple values for the same
/// header name (e.g. `Set-Cookie`).
pub fn append<N, V>(&mut self, name: N, value: V)
where
N: Into<HeaderName>,
V: Into<HeaderValue>,
{
let name = name.into();
let value = value.into();
if let Some(entry) = self
.entries
.iter_mut()
.find(|(n, _)| n.as_str() == name.as_str())
{
entry.1.push(value);
} else {
self.entries.push((name, vec![value]));
}
}
/// Get the first value for the given header name, or `None` if the
/// name is not present.
pub fn get<N>(&self, name: &N) -> Option<&HeaderValue>
where
N: ?Sized + AsRef<str>,
{
let name = name.as_ref();
self.entries
.iter()
.find(|(n, _)| n.as_str().eq_ignore_ascii_case(name))
.and_then(|(_, v)| v.first())
}
/// Get all values for the given header name.
///
/// Returns an empty slice if the name is not present.
pub fn get_all<N>(&self, name: &N) -> &[HeaderValue]
where
N: ?Sized + AsRef<str>,
{
let name = name.as_ref();
self.entries
.iter()
.find(|(n, _)| n.as_str().eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_slice())
.unwrap_or(&[])
}
/// Return `true` if the map contains the given header name.
pub fn contains<N>(&self, name: &N) -> bool
where
N: ?Sized + AsRef<str>,
{
let name = name.as_ref();
self.entries
.iter()
.any(|(n, _)| n.as_str().eq_ignore_ascii_case(name))
}
/// Remove a header and all its values. Returns the values if the
/// header was present.
///
/// Subsequent entries preserve their relative iteration order.
pub fn remove<N>(&mut self, name: &N) -> Option<Vec<HeaderValue>>
where
N: ?Sized + AsRef<str>,
{
let name = name.as_ref();
if let Some(pos) = self
.entries
.iter()
.position(|(n, _)| n.as_str().eq_ignore_ascii_case(name))
{
Some(self.entries.remove(pos).1)
} else {
None
}
}
/// Remove all headers from the map.
#[inline]
pub fn clear(&mut self) {
self.entries.clear();
}
// -- iteration -----------------------------------------------------------
/// Return an iterator over `(&HeaderName, &Vec<HeaderValue>)` pairs
/// in insertion order.
#[inline]
pub fn iter(&self) -> Iter<'_> {
Iter {
inner: self.entries.iter(),
}
}
/// Return a mutable iterator over `(&HeaderName, &mut Vec<HeaderValue>)`
/// pairs in insertion order.
#[inline]
pub fn iter_mut(&mut self) -> IterMut<'_> {
IterMut {
inner: self.entries.iter_mut(),
}
}
/// Return an iterator over header names in insertion order.
#[inline]
pub fn keys(&self) -> Keys<'_> {
Keys {
inner: self.entries.iter(),
}
}
// -- conversion helpers --------------------------------------------------
/// Return the total number of individual header values (counting
/// all values for multi-valued headers).
#[inline]
pub fn values_len(&self) -> usize {
self.entries.iter().map(|(_, v)| v.len()).sum()
}
/// Serialize headers to HTTP wire format ("Name: value\r\n" for each).
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(self.serialized_len());
self.write_to(&mut buf);
buf
}
/// Calculate the exact byte length of the serialized header block.
///
/// Each header contributes: `name.len() + 2 + value.len() + 2` bytes
/// (the `2`s are for `: ` and `\r\n`).
pub fn serialized_len(&self) -> usize {
self.entries
.iter()
.map(|(name, values)| {
let name_len = name.original().len();
values
.iter()
.map(|v| name_len + 2 + v.len() + 2)
.sum::<usize>()
})
.sum()
}
/// Write headers directly into the provided buffer in HTTP wire format.
///
/// This avoids the intermediate `Vec<u8>` allocation that
/// [`to_bytes`](Self::to_bytes) creates when the caller already has a
/// buffer to write into (e.g. response serialization).
pub fn write_to(&self, buf: &mut Vec<u8>) {
for (name, values) in &self.entries {
for value in values {
buf.extend_from_slice(name.original().as_bytes());
buf.extend_from_slice(b": ");
buf.extend_from_slice(value.as_bytes());
buf.extend_from_slice(b"\r\n");
}
}
}
}
impl Default for HeaderMap {
#[inline]
fn default() -> Self {
Self::new()
}
}
// -- Display: render all headers as "Name: value\r\n" ------------------------
impl fmt::Display for HeaderMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (name, values) in &self.entries {
for value in values {
writeln!(f, "{}: {}", name, value)?;
}
}
Ok(())
}
}
// -- FromIterator ------------------------------------------------------------
impl FromIterator<(HeaderName, HeaderValue)> for HeaderMap {
/// Build a `HeaderMap` from an iterator of `(name, value)` pairs.
///
/// Duplicate names cause values to be appended (equivalent to
/// calling [`append`](HeaderMap::append) for each pair).
fn from_iter<I: IntoIterator<Item = (HeaderName, HeaderValue)>>(iter: I) -> Self {
let mut map = HeaderMap::new();
for (name, value) in iter {
map.append(name, value);
}
map
}
}
impl FromIterator<(String, String)> for HeaderMap {
/// Convenience: build from `(String, String)` pairs.
fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
let mut map = HeaderMap::new();
for (name, value) in iter {
map.append(name, value);
}
map
}
}
impl FromIterator<(String, Bytes)> for HeaderMap {
/// Convenience: build from `(String, Bytes)` pairs.
fn from_iter<I: IntoIterator<Item = (String, Bytes)>>(iter: I) -> Self {
let mut map = HeaderMap::new();
for (name, value) in iter {
map.append(
HeaderName::from_string(name),
HeaderValue::from_bytes(value),
);
}
map
}
}
// -- IntoIterator ------------------------------------------------------------
/// Owned iterator over `(HeaderName, Vec<HeaderValue>)` pairs.
pub struct IntoIter {
inner: std::vec::IntoIter<(HeaderName, Vec<HeaderValue>)>,
}
impl Iterator for IntoIter {
type Item = (HeaderName, Vec<HeaderValue>);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl ExactSizeIterator for IntoIter {}
impl IntoIterator for HeaderMap {
type Item = (HeaderName, Vec<HeaderValue>);
type IntoIter = IntoIter;
#[inline]
fn into_iter(self) -> IntoIter {
IntoIter {
inner: self.entries.into_iter(),
}
}
}
/// Borrowing iterator over `(&HeaderName, &Vec<HeaderValue>)` pairs.
pub struct Iter<'a> {
inner: std::slice::Iter<'a, (HeaderName, Vec<HeaderValue>)>,
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a HeaderName, &'a Vec<HeaderValue>);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(n, v)| (n, v))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> ExactSizeIterator for Iter<'a> {}
impl<'a> IntoIterator for &'a HeaderMap {
type Item = (&'a HeaderName, &'a Vec<HeaderValue>);
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Iter<'a> {
self.iter()
}
}
/// Mutable borrowing iterator.
pub struct IterMut<'a> {
inner: std::slice::IterMut<'a, (HeaderName, Vec<HeaderValue>)>,
}
impl<'a> Iterator for IterMut<'a> {
type Item = (&'a HeaderName, &'a mut Vec<HeaderValue>);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(n, v)| (&*n, v))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> ExactSizeIterator for IterMut<'a> {}
/// Borrowing iterator over header names.
pub struct Keys<'a> {
inner: std::slice::Iter<'a, (HeaderName, Vec<HeaderValue>)>,
}
impl<'a> Iterator for Keys<'a> {
type Item = &'a HeaderName;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(n, _)| n)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> ExactSizeIterator for Keys<'a> {}
// ---------------------------------------------------------------------------
// Header name validation
// ---------------------------------------------------------------------------
/// Return `true` if the byte is a valid HTTP header field-name character.
///
/// Per RFC 9110 section 5.1: `!`, `#`, `$`, `%`, `&`, `'`, `*`, `+`,
/// `-`, `.`, `^`, `` ` ``, `|`, `~`, and digits / alpha.
fn is_valid_header_name_byte(b: u8) -> bool {
matches!(b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' |
b'-' | b'.' | b'^' | b'`' | b'|' | b'~' |
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'
)
}
// ---------------------------------------------------------------------------
// Well-known header name constants
// ---------------------------------------------------------------------------
macro_rules! header_constants {
($($name:ident => $value:literal),+ $(,)?) => {
impl HeaderName {
$(
pub const $name: &'static str = $value;
)+
}
/// Convenience free functions that return a pre-built `HeaderName`
/// for each well-known header. These are **not** const; they
/// allocate. For a zero-alloc comparison, compare against the
/// `&str` constants on `HeaderName` instead.
pub mod header_names {
use super::HeaderName;
$(
#[doc = concat!("`", $value, "`")]
#[inline]
#[allow(non_snake_case)] // names mirror the SCREAMING_CASE header constants
pub fn $name() -> HeaderName {
HeaderName::from_static($value)
}
)+
}
};
}
header_constants! {
// -- General --
CACHE_CONTROL => "Cache-Control",
CONNECTION => "Connection",
DATE => "Date",
PRAGMA => "Pragma",
TRAILER => "Trailer",
TRANSFER_ENCODING => "Transfer-Encoding",
UPGRADE => "Upgrade",
VIA => "Via",
WARNING => "Warning",
// -- Request --
ACCEPT => "Accept",
ACCEPT_CHARSET => "Accept-Charset",
ACCEPT_ENCODING => "Accept-Encoding",
ACCEPT_LANGUAGE => "Accept-Language",
AUTHORIZATION => "Authorization",
COOKIE => "Cookie",
EXPECT => "Expect",
FROM => "From",
HOST => "Host",
IF_MATCH => "If-Match",
IF_MODIFIED_SINCE => "If-Modified-Since",
IF_NONE_MATCH => "If-None-Match",
IF_RANGE => "If-Range",
IF_UNMODIFIED_SINCE => "If-Unmodified-Since",
MAX_FORWARDS => "Max-Forwards",
PROXY_AUTHORIZATION => "Proxy-Authorization",
RANGE => "Range",
REFERER => "Referer",
TE => "TE",
USER_AGENT => "User-Agent",
// -- Response --
ACCEPT_RANGES => "Accept-Ranges",
AGE => "Age",
ALLOW => "Allow",
CONTENT_ENCODING => "Content-Encoding",
CONTENT_LANGUAGE => "Content-Language",
CONTENT_LENGTH => "Content-Length",
CONTENT_LOCATION => "Content-Location",
CONTENT_RANGE => "Content-Range",
CONTENT_TYPE => "Content-Type",
ETAG => "ETag",
EXPIRES => "Expires",
LAST_MODIFIED => "Last-Modified",
LOCATION => "Location",
PROXY_AUTHENTICATE => "Proxy-Authenticate",
RETRY_AFTER => "Retry-After",
SERVER => "Server",
SET_COOKIE => "Set-Cookie",
VARY => "Vary",
WWW_AUTHENTICATE => "WWW-Authenticate",
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_name_case_insensitive_eq() {
let a = HeaderName::from("Content-Type");
let b = HeaderName::from("content-type");
let c = HeaderName::from("CONTENT-TYPE");
assert_eq!(a, b);
assert_eq!(b, c);
assert_eq!(a, c);
}
#[test]
fn header_name_case_insensitive_hash() {
use std::collections::hash_map::DefaultHasher;
let a = HeaderName::from("Content-Type");
let b = HeaderName::from("content-type");
let mut ha = DefaultHasher::new();
let mut hb = DefaultHasher::new();
a.hash(&mut ha);
b.hash(&mut hb);
assert_eq!(ha.finish(), hb.finish());
}
#[test]
fn header_name_display_preserves_casing() {
let name = HeaderName::from("X-Custom-Header");
assert_eq!(name.to_string(), "X-Custom-Header");
assert_eq!(name.as_str(), "x-custom-header");
}
#[test]
fn header_name_from_static() {
let name = HeaderName::from_static("Host");
assert_eq!(name.as_original(), "Host");
assert_eq!(name.as_str(), "host");
}
#[test]
fn header_name_try_from_bytes_valid() {
let name = HeaderName::try_from_bytes(b"Accept-Encoding").unwrap();
assert_eq!(name.as_str(), "accept-encoding");
}
#[test]
fn header_name_try_from_bytes_invalid() {
// Space is not allowed in header names.
assert!(HeaderName::try_from_bytes(b"Bad Name").is_err());
}
// -- HeaderValue ---------------------------------------------------------
#[test]
fn header_value_display_utf8() {
let val = HeaderValue::from("text/html; charset=utf-8");
assert_eq!(val.to_string(), "text/html; charset=utf-8");
}
#[test]
fn header_value_display_non_utf8() {
let val = HeaderValue::from_bytes(vec![0xFF, 0xFE]);
let s = val.to_string();
assert!(s.contains("ff"));
assert!(s.contains("fe"));
}
#[test]
fn header_value_from_string() {
let val = HeaderValue::from_string("hello".to_owned());
assert_eq!(val.to_str().unwrap(), "hello");
assert_eq!(val.len(), 5);
assert!(!val.is_empty());
}
#[test]
fn header_value_empty() {
let val = HeaderValue::from_static("");
assert!(val.is_empty());
assert_eq!(val.len(), 0);
}
#[test]
fn header_value_from_bytes_zero_copy() {
let original = Bytes::from("shared buffer");
let val = HeaderValue::from(original.clone());
// Same underlying allocation.
assert_eq!(val.as_bytes().as_ptr(), original.as_ptr());
}
// -- HeaderMap -----------------------------------------------------------
#[test]
fn map_insert_and_get() {
let mut map = HeaderMap::new();
map.insert("Content-Type", "text/html");
map.insert("Host", "example.com");
assert_eq!(map.len(), 2);
assert_eq!(
map.get("content-type").unwrap().to_str().unwrap(),
"text/html"
);
assert_eq!(map.get("HOST").unwrap().to_str().unwrap(), "example.com");
}
#[test]
fn map_insert_replaces() {
let mut map = HeaderMap::new();
map.insert("X-Foo", "old");
map.insert("x-foo", "new");
assert_eq!(map.len(), 1);
assert_eq!(map.get("x-foo").unwrap().to_str().unwrap(), "new");
}
#[test]
fn map_insert_returns_old_values() {
let mut map = HeaderMap::new();
map.insert("Set-Cookie", "a=1");
let old = map.insert("set-cookie", "b=2");
assert!(old.is_some());
let old = old.unwrap();
assert_eq!(old.len(), 1);
assert_eq!(old[0].to_str().unwrap(), "a=1");
}
#[test]
fn map_append_multi_value() {
let mut map = HeaderMap::new();
map.append("Set-Cookie", "a=1");
map.append("Set-Cookie", "b=2");
map.append("Set-Cookie", "c=3");
let all = map.get_all("set-cookie");
assert_eq!(all.len(), 3);
assert_eq!(all[0].to_str().unwrap(), "a=1");
assert_eq!(all[1].to_str().unwrap(), "b=2");
assert_eq!(all[2].to_str().unwrap(), "c=3");
}
#[test]
fn map_contains() {
let mut map = HeaderMap::new();
assert!(!map.contains("Host"));
map.insert("Host", "example.com");
assert!(map.contains("host"));
assert!(map.contains("HOST"));
}
#[test]
fn map_remove() {
let mut map = HeaderMap::new();
map.insert("A", "1");
map.insert("B", "2");
map.insert("C", "3");
let removed = map.remove("b");
assert!(removed.is_some());
assert_eq!(map.len(), 2);
assert!(!map.contains("b"));
// Remaining entries are still accessible.
assert_eq!(map.get("a").unwrap().to_str().unwrap(), "1");
assert_eq!(map.get("c").unwrap().to_str().unwrap(), "3");
}
#[test]
fn map_remove_nonexistent() {
let mut map = HeaderMap::new();
assert!(map.remove("nope").is_none());
}
#[test]
fn map_get_all_empty_slice() {
let map = HeaderMap::new();
assert!(map.get_all("anything").is_empty());
}
#[test]
fn map_insertion_order_preserved() {
let mut map = HeaderMap::new();
map.insert("Z-Last", "z");
map.insert("A-First", "a");
map.insert("M-Middle", "m");
let names: Vec<_> = map.keys().map(|k| k.as_original().to_owned()).collect();
assert_eq!(names, vec!["Z-Last", "A-First", "M-Middle"]);
}
#[test]
fn map_clear() {
let mut map = HeaderMap::new();
map.insert("A", "1");
map.insert("B", "2");
map.clear();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn map_values_len() {
let mut map = HeaderMap::new();
map.insert("A", "1");
map.append("B", "2");
map.append("B", "3");
map.append("B", "4");
assert_eq!(map.values_len(), 4);
}
#[test]
fn map_default() {
let map: HeaderMap = HeaderMap::default();
assert!(map.is_empty());
}
// -- Display -------------------------------------------------------------
#[test]
fn map_display_format() {
let mut map = HeaderMap::new();
map.insert("Content-Type", "text/html");
map.insert("Content-Length", "42");
let rendered = map.to_string();
// Both lines must be present with trailing newlines.
assert!(rendered.contains("Content-Type: text/html\n"));
assert!(rendered.contains("Content-Length: 42\n"));
}
#[test]
fn map_display_multi_value() {
let mut map = HeaderMap::new();
map.append("Set-Cookie", "a=1");
map.append("Set-Cookie", "b=2");
let rendered = map.to_string();
assert!(rendered.contains("Set-Cookie: a=1\n"));
assert!(rendered.contains("Set-Cookie: b=2\n"));
}
// -- FromIterator --------------------------------------------------------
#[test]
fn map_from_iter_headername_value() {
let pairs = vec![
(HeaderName::from("A"), HeaderValue::from("1")),
(HeaderName::from("B"), HeaderValue::from("2")),
];
let map: HeaderMap = pairs.into_iter().collect();
assert_eq!(map.len(), 2);
assert_eq!(map.get("a").unwrap().to_str().unwrap(), "1");
}
#[test]
fn map_from_iter_string_string() {
let pairs = vec![
("Content-Type".to_owned(), "text/plain".to_owned()),
("Host".to_owned(), "example.com".to_owned()),
];
let map: HeaderMap = pairs.into_iter().collect();
assert_eq!(map.len(), 2);
}
#[test]
fn map_from_iter_dedup_appends() {
let pairs = vec![
("Set-Cookie".to_owned(), "a=1".to_owned()),
("Set-Cookie".to_owned(), "b=2".to_owned()),
];
let map: HeaderMap = pairs.into_iter().collect();
assert_eq!(map.len(), 1);
assert_eq!(map.get_all("set-cookie").len(), 2);
}
// -- IntoIterator --------------------------------------------------------
#[test]
fn map_into_iter_owned() {
let mut map = HeaderMap::new();
map.insert("A", "1");
map.insert("B", "2");
let items: Vec<_> = map.into_iter().collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0].0.as_str(), "a");
assert_eq!(items[0].1[0].to_str().unwrap(), "1");
assert_eq!(items[1].0.as_str(), "b");
}
#[test]
fn map_iter_borrowed() {
let mut map = HeaderMap::new();
map.insert("A", "1");
map.insert("B", "2");
let mut names = Vec::new();
for (name, _values) in &map {
names.push(name.as_original().to_owned());
}
assert_eq!(names, vec!["A", "B"]);
}
#[test]
fn map_iter_mut() {
let mut map = HeaderMap::new();
map.insert("A", "old");
for (_name, values) in map.iter_mut() {
values.clear();
values.push(HeaderValue::from("new"));
}
assert_eq!(map.get("a").unwrap().to_str().unwrap(), "new");
}
// -- Well-known constants ------------------------------------------------
#[test]
fn well_known_constants() {
assert_eq!(HeaderName::CONTENT_TYPE, "Content-Type");
assert_eq!(HeaderName::HOST, "Host");
assert_eq!(HeaderName::SET_COOKIE, "Set-Cookie");
assert_eq!(HeaderName::TRANSFER_ENCODING, "Transfer-Encoding");
assert_eq!(HeaderName::AUTHORIZATION, "Authorization");
}
// -- with_capacity -------------------------------------------------------
#[test]
fn map_with_capacity() {
let map = HeaderMap::with_capacity(32);
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
// -- Mixed insert/append/remove interaction ------------------------------
#[test]
fn map_mixed_operations() {
let mut map = HeaderMap::new();
map.insert("Host", "example.com");
map.append("Set-Cookie", "a=1");
map.append("Set-Cookie", "b=2");
map.insert("Content-Length", "100");
// Remove one header.
map.remove("content-length");
assert_eq!(map.len(), 2);
// Replace Set-Cookie entirely.
map.insert("set-cookie", "c=3");
assert_eq!(map.get_all("Set-Cookie").len(), 1);
assert_eq!(map.get("Set-Cookie").unwrap().to_str().unwrap(), "c=3");
// Host still present.
assert_eq!(map.get("HOST").unwrap().to_str().unwrap(), "example.com");
}
// -- HeaderValue edge cases ----------------------------------------------
#[test]
fn header_value_as_bytes() {
let val = HeaderValue::from("raw bytes");
assert_eq!(val.as_bytes(), b"raw bytes");
}
#[test]
fn header_value_into_bytes() {
let val = HeaderValue::from("take ownership");
let b: Bytes = val.into_bytes();
assert_eq!(&b[..], b"take ownership");
}
#[test]
fn header_name_into_lower() {
let name = HeaderName::from("X-Forwarded-For");
assert_eq!(name.into_lower(), "x-forwarded-for");
}
#[test]
fn header_name_into_string() {
let name = HeaderName::from("X-Forwarded-For");
assert_eq!(name.into_string(), "X-Forwarded-For");
}
}