use armature_h1::{ByteStr, HeaderId, header as header_id};
use bytes::Bytes;
use smallvec::SmallVec;
use std::collections::HashMap;
use std::fmt;
pub const INLINE_HEADERS: usize = 12;
enum Needle<'a> {
Known(HeaderId),
Custom(&'a str),
}
impl<'a> Needle<'a> {
#[inline]
fn new(name: &'a str) -> Self {
match HeaderId::from_bytes(name.as_bytes()) {
Some(id) => Needle::Known(id),
None => Needle::Custom(name),
}
}
#[inline]
fn matches(&self, id: &HeaderId) -> bool {
match self {
Needle::Known(known) => known == id,
Needle::Custom(name) => id.as_str().eq_ignore_ascii_case(name),
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Header {
pub id: HeaderId,
pub value: Bytes,
}
impl Header {
#[inline]
pub fn new(name: impl AsRef<str>, value: impl HeaderValueInput) -> Self {
Self {
id: header_id::intern(name.as_ref()),
value: value.into_value(),
}
}
#[inline]
pub fn name(&self) -> &str {
self.id.as_str()
}
#[inline]
pub fn value_str(&self) -> Option<&str> {
std::str::from_utf8(&self.value).ok()
}
}
impl fmt::Debug for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.value_str() {
Some(v) => write!(f, "{}: {}", self.name(), v),
None => write!(f, "{}: <{} non-utf8 bytes>", self.name(), self.value.len()),
}
}
}
pub trait HeaderValueInput {
fn into_value(self) -> Bytes;
}
impl HeaderValueInput for Bytes {
#[inline]
fn into_value(self) -> Bytes {
self
}
}
impl HeaderValueInput for &str {
#[inline]
fn into_value(self) -> Bytes {
Bytes::copy_from_slice(self.as_bytes())
}
}
impl HeaderValueInput for &String {
#[inline]
fn into_value(self) -> Bytes {
Bytes::copy_from_slice(self.as_bytes())
}
}
impl HeaderValueInput for String {
#[inline]
fn into_value(self) -> Bytes {
Bytes::from(self.into_bytes())
}
}
impl HeaderValueInput for &[u8] {
#[inline]
fn into_value(self) -> Bytes {
Bytes::copy_from_slice(self)
}
}
impl HeaderValueInput for Vec<u8> {
#[inline]
fn into_value(self) -> Bytes {
Bytes::from(self)
}
}
impl HeaderValueInput for ByteStr {
#[inline]
fn into_value(self) -> Bytes {
self.into_bytes()
}
}
impl HeaderValueInput for std::borrow::Cow<'_, str> {
#[inline]
fn into_value(self) -> Bytes {
match self {
std::borrow::Cow::Borrowed(s) => Bytes::copy_from_slice(s.as_bytes()),
std::borrow::Cow::Owned(s) => Bytes::from(s.into_bytes()),
}
}
}
#[derive(Clone, Default)]
pub struct HeaderMap {
inner: SmallVec<[Header; INLINE_HEADERS]>,
}
impl HeaderMap {
#[inline]
pub const fn new() -> Self {
Self {
inner: SmallVec::new_const(),
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: SmallVec::with_capacity(capacity),
}
}
#[inline]
pub fn is_inline(&self) -> bool {
!self.inner.spilled()
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
pub fn get(&self, name: &str) -> Option<&str> {
self.get_bytes(name)
.and_then(|v| std::str::from_utf8(v).ok())
}
#[inline]
pub fn get_bytes(&self, name: &str) -> Option<&Bytes> {
let needle = Needle::new(name);
self.inner
.iter()
.find(|h| needle.matches(&h.id))
.map(|h| &h.value)
}
#[inline]
pub fn get_unique(&self, name: &str) -> Result<Option<&Bytes>, DuplicateField> {
let needle = Needle::new(name);
self.unique_where(|h| needle.matches(&h.id))
.map(|found| found.map(|h| &h.value))
}
#[inline]
pub fn get_id(&self, id: &HeaderId) -> Option<&Bytes> {
self.inner.iter().find(|h| &h.id == id).map(|h| &h.value)
}
#[inline]
pub fn get_ignore_case(&self, name: &str) -> Option<&str> {
self.get(name)
}
#[inline]
pub fn contains(&self, name: &str) -> bool {
self.get_bytes(name).is_some()
}
#[inline]
pub fn contains_key(&self, name: &str) -> bool {
self.contains(name)
}
#[inline]
pub fn insert(&mut self, name: impl AsRef<str>, value: impl HeaderValueInput) -> Option<Bytes> {
self.insert_id(header_id::intern(name.as_ref()), value.into_value())
}
#[inline]
pub fn insert_id(&mut self, id: HeaderId, value: Bytes) -> Option<Bytes> {
let mut replaced = None;
self.inner.retain_mut(|h| {
if h.id != id {
return true;
}
match replaced {
None => {
replaced = Some(std::mem::replace(&mut h.value, value.clone()));
true
}
Some(_) => false,
}
});
if replaced.is_none() {
self.inner.push(Header { id, value });
}
replaced
}
#[inline]
pub fn append_id(&mut self, id: HeaderId, value: Bytes) {
self.inner.push(Header { id, value });
}
#[inline]
pub fn append(&mut self, name: impl AsRef<str>, value: impl HeaderValueInput) {
self.inner.push(Header {
id: header_id::intern(name.as_ref()),
value: value.into_value(),
});
}
#[inline]
pub fn remove(&mut self, name: &str) -> Option<Bytes> {
let needle = Needle::new(name);
let mut removed = None;
self.inner.retain_mut(|h| {
if !needle.matches(&h.id) {
return true;
}
if removed.is_none() {
removed = Some(h.value.clone());
}
false
});
removed
}
#[inline]
pub fn remove_all(&mut self, name: &str) -> usize {
let needle = Needle::new(name);
let before = self.inner.len();
self.inner.retain(|h| !needle.matches(&h.id));
before - self.inner.len()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.inner
.iter()
.filter_map(|h| h.value_str().map(|v| (h.name(), v)))
}
#[inline]
pub fn iter_raw(&self) -> impl Iterator<Item = (&HeaderId, &Bytes)> {
self.inner.iter().map(|h| (&h.id, &h.value))
}
#[inline]
pub fn names(&self) -> impl Iterator<Item = &str> {
self.inner.iter().map(|h| h.name())
}
#[inline]
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.names()
}
#[inline]
pub fn values(&self) -> impl Iterator<Item = &str> {
self.inner.iter().filter_map(|h| h.value_str())
}
#[inline]
pub fn get_all(&self, name: &str) -> Vec<&str> {
let needle = Needle::new(name);
self.inner
.iter()
.filter(|h| needle.matches(&h.id))
.filter_map(|h| h.value_str())
.collect()
}
#[inline]
pub fn clear(&mut self) {
self.inner.clear();
}
#[inline]
pub fn extend<I, K, V>(&mut self, iter: I)
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: HeaderValueInput,
{
for (k, v) in iter {
self.insert(k, v);
}
}
#[inline]
pub fn to_hash_map(&self) -> HashMap<String, String> {
self.iter()
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect()
}
#[inline]
pub fn from_hash_map(map: HashMap<String, String>) -> Self {
let mut headers = Self::with_capacity(map.len());
for (k, v) in map {
headers.insert(k, v);
}
headers
}
#[inline]
pub fn content_type(&self) -> Option<&str> {
self.str_of(&HeaderId::ContentType)
}
#[inline]
pub fn content_length(&self) -> Option<usize> {
self.unique_str_of(&HeaderId::ContentLength)?.parse().ok()
}
#[inline]
pub fn accept(&self) -> Option<&str> {
self.str_of(&HeaderId::Accept)
}
#[inline]
pub fn authorization(&self) -> Option<&str> {
self.unique_str_of(&HeaderId::Authorization)
}
#[inline]
pub fn user_agent(&self) -> Option<&str> {
self.str_of(&HeaderId::UserAgent)
}
#[inline]
pub fn host(&self) -> Option<&str> {
self.unique_str_of(&HeaderId::Host)
}
#[inline]
pub fn cookie(&self) -> Option<&str> {
self.str_of(&HeaderId::Cookie)
}
#[inline]
pub fn is_keep_alive(&self) -> bool {
self.str_of(&HeaderId::Connection)
.map(|v| v.eq_ignore_ascii_case("keep-alive"))
.unwrap_or(true) }
#[inline]
pub fn is_chunked(&self) -> bool {
self.str_of(&HeaderId::TransferEncoding)
.map(|v| v.contains("chunked"))
.unwrap_or(false)
}
#[inline]
pub fn set_content_type(&mut self, value: impl HeaderValueInput) {
self.insert("content-type", value);
}
#[inline]
pub fn set_content_length(&mut self, len: usize) {
self.insert("content-length", len.to_string());
}
#[inline]
fn str_of(&self, id: &HeaderId) -> Option<&str> {
self.get_id(id).and_then(|v| std::str::from_utf8(v).ok())
}
#[inline]
fn unique_str_of(&self, id: &HeaderId) -> Option<&str> {
self.unique_where(|h| &h.id == id)
.ok()
.flatten()
.and_then(Header::value_str)
}
#[inline]
fn unique_where(
&self,
mut pred: impl FnMut(&Header) -> bool,
) -> Result<Option<&Header>, DuplicateField> {
let mut first: Option<&Header> = None;
let mut count = 0usize;
for header in &self.inner {
if !pred(header) {
continue;
}
count += 1;
if first.is_none() {
first = Some(header);
}
}
match first {
Some(header) if count > 1 => Err(DuplicateField {
name: header.name().to_owned(),
count,
}),
other => Ok(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DuplicateField {
name: String,
count: usize,
}
impl DuplicateField {
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn count(&self) -> usize {
self.count
}
}
impl fmt::Display for DuplicateField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"header `{}` appeared {} times but is single-valued; no occurrence \
can be trusted over another",
self.name, self.count
)
}
}
impl std::error::Error for DuplicateField {}
impl fmt::Debug for HeaderMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map()
.entries(
self.inner
.iter()
.map(|h| (h.name(), h.value_str().unwrap_or("<non-utf8>"))),
)
.finish()
}
}
impl<K, V> FromIterator<(K, V)> for HeaderMap
where
K: AsRef<str>,
V: HeaderValueInput,
{
fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
let iter = iter.into_iter();
let (min, max) = iter.size_hint();
let mut map = HeaderMap::with_capacity(max.unwrap_or(min));
for (k, v) in iter {
map.insert(k, v);
}
map
}
}
impl Extend<(String, String)> for HeaderMap {
fn extend<I: IntoIterator<Item = (String, String)>>(&mut self, iter: I) {
for (k, v) in iter {
self.insert(k, v);
}
}
}
fn utf8_pair(h: &Header) -> Option<(&str, &str)> {
h.value_str().map(|v| (h.name(), v))
}
fn owned_utf8_pair(h: Header) -> Option<(String, String)> {
let name = h.name().to_owned();
String::from_utf8(h.value.to_vec())
.ok()
.map(|value| (name, value))
}
impl<'a> IntoIterator for &'a HeaderMap {
type Item = (&'a str, &'a str);
type IntoIter = std::iter::FilterMap<
std::slice::Iter<'a, Header>,
fn(&'a Header) -> Option<(&'a str, &'a str)>,
>;
fn into_iter(self) -> Self::IntoIter {
self.inner.iter().filter_map(utf8_pair as _)
}
}
impl IntoIterator for HeaderMap {
type Item = (String, String);
type IntoIter = std::iter::FilterMap<
smallvec::IntoIter<[Header; INLINE_HEADERS]>,
fn(Header) -> Option<(String, String)>,
>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter().filter_map(owned_utf8_pair as _)
}
}
impl std::ops::Index<&str> for HeaderMap {
type Output = str;
fn index(&self, name: &str) -> &Self::Output {
self.get(name).expect("header not found")
}
}
impl From<HashMap<String, String>> for HeaderMap {
fn from(map: HashMap<String, String>) -> Self {
Self::from_hash_map(map)
}
}
impl From<HeaderMap> for HashMap<String, String> {
fn from(map: HeaderMap) -> Self {
map.to_hash_map()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_is_inline() {
let headers = HeaderMap::new();
assert!(headers.is_inline());
assert!(headers.is_empty());
}
#[test]
fn get_returns_str_and_well_known_names_are_interned() {
let mut h = HeaderMap::new();
h.insert("Content-Type", "application/json");
h.insert("X-Tenant-Id", "acme".to_string());
assert_eq!(h.get("content-type"), Some("application/json"));
assert_eq!(h.get("CONTENT-TYPE"), Some("application/json"));
assert_eq!(h.get("x-tenant-id"), Some("acme"));
assert_eq!(h.get("absent"), None);
assert_eq!(
h.get_id(&HeaderId::ContentType).map(|b| &b[..]),
Some(&b"application/json"[..])
);
}
#[test]
fn custom_names_stay_case_insensitive_through_the_borrowed_needle() {
let mut h = HeaderMap::new();
h.insert("X-Request-ID", "abc123");
h.append("x-request-id", "def456");
assert_eq!(h.get("x-request-id"), Some("abc123"));
assert_eq!(h.get("X-REQUEST-ID"), Some("abc123"));
assert!(h.contains("X-Request-Id"));
assert_eq!(h.get_all("X-Request-Id"), vec!["abc123", "def456"]);
h.insert("Content-Type", "text/plain");
assert_eq!(h.get("x-content-type"), None);
assert_eq!(h.remove_all("X-Request-ID"), 2);
assert_eq!(h.get("x-request-id"), None);
}
#[test]
fn non_utf8_value_is_invisible_to_get_but_reachable_as_bytes() {
let mut h = HeaderMap::new();
h.insert("x-raw", Bytes::from_static(&[0xff, 0x00]));
assert_eq!(h.get("x-raw"), None);
assert_eq!(h.get_bytes("x-raw").map(|b| b.len()), Some(2));
assert_eq!(h.len(), 1);
assert_eq!(h.iter().count(), 0);
assert_eq!(h.iter_raw().count(), 1);
}
#[test]
fn test_insert_and_get() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
headers.insert("Accept", "text/html");
assert_eq!(headers.len(), 2);
assert_eq!(headers.get("Content-Type"), Some("application/json"));
assert_eq!(headers.get("content-type"), Some("application/json"));
}
#[test]
fn test_insert_replaces() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "text/plain");
let old = headers.insert("Content-Type", "application/json");
assert_eq!(old.as_deref(), Some(&b"text/plain"[..]));
assert_eq!(headers.len(), 1);
assert_eq!(headers.get("Content-Type"), Some("application/json"));
}
#[test]
fn test_append_duplicates() {
let mut headers = HeaderMap::new();
headers.append("Set-Cookie", "session=abc");
headers.append("Set-Cookie", "user=123");
assert_eq!(headers.len(), 2);
assert_eq!(
headers.get_all("set-cookie"),
vec!["session=abc", "user=123"]
);
}
#[test]
fn test_remove() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
headers.insert("Accept", "text/html");
let removed = headers.remove("Content-Type");
assert_eq!(removed.as_deref(), Some(&b"application/json"[..]));
assert_eq!(headers.len(), 1);
assert!(!headers.contains("Content-Type"));
}
#[test]
fn test_remove_all() {
let mut headers = HeaderMap::new();
headers.append("Set-Cookie", "a=1");
headers.append("set-cookie", "b=2");
headers.insert("Accept", "*/*");
assert_eq!(headers.remove_all("Set-Cookie"), 2);
assert_eq!(headers.len(), 1);
}
#[test]
fn test_inline_capacity() {
let mut headers = HeaderMap::new();
for i in 0..INLINE_HEADERS {
headers.insert(format!("Header-{i}"), format!("Value-{i}"));
}
assert!(headers.is_inline());
headers.insert("Extra-Header", "Extra-Value");
assert!(!headers.is_inline());
}
#[test]
fn test_iter() {
let mut headers = HeaderMap::new();
headers.insert("A", "1");
headers.insert("B", "2");
let pairs: Vec<_> = headers.iter().collect();
assert_eq!(pairs.len(), 2);
}
#[test]
fn a_parser_produced_id_equals_an_interned_one() {
use armature_h1::{Limits, parse_head};
use bytes::Bytes;
let raw = Bytes::from_static(b"GET / HTTP/1.1\r\nHost: a\r\nX-Trace-Id: abc\r\n\r\n");
let head = parse_head(&raw, &Limits::default())
.expect("parse")
.expect("complete")
.0;
let (parsed_id, value) = head
.headers
.iter()
.find(|(id, _)| id.as_str() == "x-trace-id")
.cloned()
.expect("the custom header is present in the parsed head");
assert_eq!(
parsed_id,
header_id::intern("x-trace-id"),
"a parser-produced id must equal an interned one, or the serve \
path's stored headers are unreachable by name"
);
let mut headers = HeaderMap::new();
headers.append_id(parsed_id, value);
assert_eq!(headers.get("X-Trace-Id"), Some("abc"));
assert_eq!(headers.get("x-trace-id"), Some("abc"));
assert!(headers.iter().any(|(k, _)| k == "x-trace-id"));
assert!(headers.keys().any(|k| k == "x-trace-id"));
assert!(headers.to_hash_map().contains_key("x-trace-id"));
}
#[test]
fn insert_collapses_every_occurrence_of_a_repeated_field() {
use bytes::Bytes;
let mut headers = HeaderMap::new();
let xff = header_id::intern("x-forwarded-for");
headers.append_id(xff.clone(), Bytes::from_static(b"203.0.113.7"));
headers.append_id(HeaderId::Accept, Bytes::from_static(b"text/html"));
headers.append_id(xff.clone(), Bytes::from_static(b"198.51.100.9"));
headers.append_id(HeaderId::UserAgent, Bytes::from_static(b"curl/8"));
headers.append_id(xff, Bytes::from_static(b"192.0.2.4"));
assert_eq!(headers.get_all("x-forwarded-for").len(), 3);
headers.insert("X-Forwarded-For", "10.0.0.1");
let all = headers.get_all("x-forwarded-for");
assert_eq!(
all,
vec!["10.0.0.1"],
"a replacing insert must leave exactly one occurrence; a surviving \
duplicate is a value the caller believed it had overwritten"
);
assert_eq!(
headers.get("accept"),
Some("text/html"),
"collapsing one field must not disturb the fields interleaved with it"
);
assert_eq!(headers.get("user-agent"), Some("curl/8"));
assert_eq!(
headers.len(),
3,
"one X-Forwarded-For plus the two bystanders"
);
}
#[test]
fn remove_takes_every_occurrence_not_just_the_first() {
use bytes::Bytes;
let mut headers = HeaderMap::new();
headers.append_id(HeaderId::Accept, Bytes::from_static(b"first"));
headers.append_id(HeaderId::Host, Bytes::from_static(b"example.com"));
headers.append_id(HeaderId::Accept, Bytes::from_static(b"second"));
headers.append_id(HeaderId::UserAgent, Bytes::from_static(b"curl/8"));
headers.append_id(HeaderId::Accept, Bytes::from_static(b"third"));
let removed = headers.remove("accept");
assert_eq!(
removed.as_deref(),
Some(&b"first"[..]),
"the first occurrence comes back, as before"
);
assert!(
headers.get("accept").is_none(),
"stripping a header must strip all of it, or code that removes an \
untrusted field before trusting the request keeps the attacker's \
later lines"
);
assert_eq!(
headers.get_all("accept").len(),
0,
"no occurrence of the removed field may survive"
);
assert_eq!(
headers.get("host"),
Some("example.com"),
"removing one field must not disturb the fields interleaved with it"
);
assert_eq!(headers.get("user-agent"), Some("curl/8"));
assert_eq!(headers.len(), 2);
}
#[test]
fn get_unique_refuses_to_choose_between_duplicate_occurrences() {
let mut headers = HeaderMap::new();
headers.append("X-Authenticated-User", "admin");
assert_eq!(
headers
.get_unique("x-authenticated-user")
.expect("one occurrence is not a duplicate")
.map(|v| &v[..]),
Some(&b"admin"[..])
);
assert_eq!(
headers
.get_unique("absent")
.expect("absent is not a duplicate"),
None,
"an absent field is Ok(None), not an error"
);
headers.append("x-authenticated-user", "alice");
assert_eq!(headers.get("X-Authenticated-User"), Some("admin"));
let err = headers
.get_unique("X-Authenticated-User")
.expect_err("two occurrences of a single-valued field must be an error");
assert_eq!(err.name(), "x-authenticated-user");
assert_eq!(err.count(), 2);
assert!(
err.to_string().contains("x-authenticated-user"),
"the message must name the field, or a rejection log cannot say which"
);
}
#[test]
fn security_relevant_accessors_fail_closed_on_a_duplicated_field() {
let mut headers = HeaderMap::new();
headers.append("Authorization", "Bearer client-chosen");
headers.append("Content-Length", "5");
headers.append("Host", "example.com");
assert_eq!(headers.authorization(), Some("Bearer client-chosen"));
assert_eq!(headers.content_length(), Some(5));
assert_eq!(headers.host(), Some("example.com"));
headers.append("authorization", "Bearer proxy-issued");
headers.append("content-length", "500");
headers.append("host", "internal.example");
assert_eq!(
headers.authorization(),
None,
"a duplicated Authorization must read as no credential, not as the \
first line the client happened to send"
);
assert_eq!(
headers.content_length(),
None,
"two Content-Length lines are the request-smuggling shape; neither \
length may be reported as the framing"
);
assert_eq!(
headers.host(),
None,
"two Host lines leave no single authority to route or cache under"
);
assert_eq!(headers.get("authorization"), Some("Bearer client-chosen"));
assert_eq!(headers.get_all("host").len(), 2);
}
#[test]
fn insert_id_collapses_every_occurrence_like_insert() {
use bytes::Bytes;
let mut by_id = HeaderMap::new();
assert_eq!(
by_id.insert_id(HeaderId::Accept, Bytes::from_static(b"first")),
None,
"the first insert replaces nothing"
);
let replaced = by_id.insert_id(HeaderId::Accept, Bytes::from_static(b"second"));
assert_eq!(replaced.as_deref(), Some(&b"first"[..]));
assert_eq!(by_id.len(), 1, "replacing must not grow the map");
let mut by_name = HeaderMap::new();
by_name.insert("Accept", "first");
let replaced_by_name = by_name.insert("Accept", "second");
assert_eq!(
replaced_by_name.as_deref(),
Some(&b"first"[..]),
"insert and insert_id must return the same displaced value"
);
assert_eq!(by_id.get("accept"), by_name.get("accept"));
assert_eq!(by_id.len(), by_name.len());
let mut repeated = HeaderMap::new();
for value in [&b"a"[..], b"b", b"c"] {
repeated.append_id(HeaderId::Accept, Bytes::copy_from_slice(value));
}
repeated.insert_id(HeaderId::Accept, Bytes::from_static(b"final"));
assert_eq!(
repeated.get_all("accept"),
vec!["final"],
"insert_id must collapse every occurrence, as insert and \
http::HeaderMap::insert do"
);
}
#[test]
fn iter_yields_lowercased_names_for_custom_headers() {
let mut h = HeaderMap::new();
h.insert("X-A", "1");
assert_eq!(h.iter().collect::<Vec<_>>(), vec![("x-a", "1")]);
}
#[test]
fn test_common_accessors() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
headers.insert("Content-Length", "100");
headers.insert("Connection", "keep-alive");
headers.insert("Transfer-Encoding", "chunked");
assert_eq!(headers.content_type(), Some("application/json"));
assert_eq!(headers.content_length(), Some(100));
assert!(headers.is_keep_alive());
assert!(headers.is_chunked());
}
#[test]
fn test_from_hash_map() {
let mut map = HashMap::new();
map.insert("Content-Type".to_string(), "application/json".to_string());
map.insert("Accept".to_string(), "text/html".to_string());
let headers = HeaderMap::from_hash_map(map);
assert_eq!(headers.len(), 2);
assert!(headers.contains("Content-Type"));
}
#[test]
fn test_to_hash_map_normalizes_names_to_lowercase() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
let map = headers.to_hash_map();
assert_eq!(
map.get("content-type").map(String::as_str),
Some("application/json")
);
assert_eq!(map.get("Content-Type"), None);
}
#[test]
fn test_from_iterator() {
let headers: HeaderMap = [
("Content-Type", "application/json"),
("Accept", "text/html"),
]
.into_iter()
.collect();
assert_eq!(headers.len(), 2);
}
#[test]
fn test_indexing() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
assert_eq!(&headers["Content-Type"], "application/json");
}
#[test]
fn test_contains_key() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
assert!(headers.contains_key("Content-Type"));
assert!(headers.contains_key("content-type"));
assert!(!headers.contains_key("Accept"));
}
#[test]
fn test_keys() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
headers.insert("Accept", "text/html");
let keys: Vec<_> = headers.keys().collect();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"content-type"));
assert!(keys.contains(&"accept"));
}
#[test]
fn test_values() {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
headers.insert("Accept", "text/html");
let values: Vec<_> = headers.values().collect();
assert_eq!(values.len(), 2);
assert!(values.contains(&"application/json"));
assert!(values.contains(&"text/html"));
}
#[test]
fn test_is_empty() {
let mut headers = HeaderMap::new();
assert!(headers.is_empty());
headers.insert("Content-Type", "application/json");
assert!(!headers.is_empty());
}
#[test]
fn test_default() {
let headers = HeaderMap::default();
assert!(headers.is_empty());
assert!(headers.is_inline());
}
#[test]
fn test_extend_trait() {
let mut headers = HeaderMap::new();
headers.insert("Existing", "1");
let extra: Vec<(String, String)> = vec![
("Content-Type".to_string(), "application/json".to_string()),
("Accept".to_string(), "text/html".to_string()),
];
Extend::extend(&mut headers, extra);
assert_eq!(headers.len(), 3);
assert_eq!(headers.get("Content-Type"), Some("application/json"));
}
#[test]
fn test_into_iterator_owned() {
let mut headers = HeaderMap::new();
headers.insert("A", "1");
headers.insert("B", "2");
let collected: Vec<(String, String)> = headers.into_iter().collect();
assert_eq!(collected.len(), 2);
}
#[test]
fn test_into_iterator_ref() {
let mut headers = HeaderMap::new();
headers.insert("A", "1");
let collected: Vec<(&str, &str)> = (&headers).into_iter().collect();
assert_eq!(collected, vec![("a", "1")]);
}
#[test]
fn test_hashmap_roundtrip() {
let mut map = HashMap::new();
map.insert("Content-Type".to_string(), "application/json".to_string());
let headers: HeaderMap = map.clone().into();
assert!(headers.contains_key("content-type"));
let back: HashMap<String, String> = headers.into();
assert_eq!(back.get("content-type"), map.get("Content-Type"));
}
#[test]
fn cloning_a_value_does_not_copy_it() {
let mut headers = HeaderMap::new();
let big = Bytes::from(vec![b'x'; 4096]);
headers.insert("x-big", big.clone());
let copy = headers.clone();
assert_eq!(
copy.get_bytes("x-big").map(|b| b.as_ptr()),
Some(big.as_ptr())
);
}
}