use crate::error::{FetchError, Result, TypeError};
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct Headers {
map: HashMap<String, String>,
}
impl Headers {
pub fn new() -> Self {
Self::default()
}
pub fn append(&mut self, name: &str, value: &str) -> Result<()> {
let name = self.validate_name(name)?;
let value = self.validate_value(value)?;
match self.map.get(&name) {
Some(existing) => {
self.map.insert(name, format!("{}, {}", existing, value));
}
None => {
self.map.insert(name, value);
}
}
Ok(())
}
pub fn delete(&mut self, name: &str) -> Result<()> {
let name = self.validate_name(name)?;
self.map.remove(&name);
Ok(())
}
pub fn get(&self, name: &str) -> Result<Option<String>> {
let name = self.validate_name(name)?;
Ok(self.map.get(&name).cloned())
}
pub fn get_set_cookie(&self) -> Vec<String> {
self.map
.get("set-cookie")
.map(|v| v.split(", ").map(|s| s.to_string()).collect())
.unwrap_or_default()
}
pub fn has(&self, name: &str) -> Result<bool> {
let name = self.validate_name(name)?;
Ok(self.map.contains_key(&name))
}
pub fn set(&mut self, name: &str, value: &str) -> Result<()> {
let name = self.validate_name(name)?;
let value = self.validate_value(value)?;
self.map.insert(name, value);
Ok(())
}
pub fn entries(&self) -> impl Iterator<Item = (&str, &str)> {
self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.map.keys().map(|k| k.as_str())
}
pub fn values(&self) -> impl Iterator<Item = &str> {
self.map.values().map(|v| v.as_str())
}
fn validate_name(&self, name: &str) -> Result<String> {
if name.is_empty() {
return Err(FetchError::Type(TypeError::new("Invalid header name")));
}
for byte in name.bytes() {
if !matches!(byte, b'!' | b'#'..=b'\'' | b'*' | b'+' | b'-' | b'.' | b'0'..=b'9' | b'A'..=b'Z' | b'^'..=b'z' | b'|' | b'~')
{
return Err(FetchError::Type(TypeError::new("Invalid header name")));
}
}
Ok(name.to_ascii_lowercase())
}
fn validate_value(&self, value: &str) -> Result<String> {
let trimmed = value.trim_matches(|c| c == ' ' || c == '\t');
for byte in trimmed.bytes() {
if !matches!(byte, 0x21..=0x7E | b' ' | b'\t') {
return Err(FetchError::Type(TypeError::new("Invalid header value")));
}
}
Ok(trimmed.to_string())
}
pub(crate) fn to_http_headers(&self) -> Result<http::HeaderMap> {
let mut map = http::HeaderMap::new();
for (name, value) in &self.map {
let header_name = http::header::HeaderName::from_bytes(name.as_bytes())
.map_err(|_| FetchError::Type(TypeError::new("Invalid header name")))?;
let header_value = http::header::HeaderValue::from_str(value)
.map_err(|_| FetchError::Type(TypeError::new("Invalid header value")))?;
map.insert(header_name, header_value);
}
Ok(map)
}
pub(crate) fn from_http_headers(headers: &http::HeaderMap) -> Self {
let mut map = HashMap::new();
for (name, value) in headers {
if let Ok(value_str) = value.to_str() {
map.insert(name.as_str().to_ascii_lowercase(), value_str.to_string());
}
}
Self { map }
}
}
impl<const N: usize> From<&[(&str, &str); N]> for Headers {
fn from(headers: &[(&str, &str); N]) -> Self {
let mut h = Self::new();
for (name, value) in headers {
let _ = h.set(name, value);
}
h
}
}
impl From<&[(&str, &str)]> for Headers {
fn from(headers: &[(&str, &str)]) -> Self {
let mut h = Self::new();
for (name, value) in headers {
let _ = h.set(name, value);
}
h
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_headers_basic_operations() {
let mut headers = Headers::new();
headers.set("content-type", "application/json").unwrap();
assert_eq!(
headers.get("content-type").unwrap().unwrap(),
"application/json"
);
assert_eq!(
headers.get("Content-Type").unwrap().unwrap(),
"application/json"
);
assert!(headers.has("content-type").unwrap());
assert!(!headers.has("x-nonexistent").unwrap());
headers.append("accept", "application/json").unwrap();
headers.append("accept", "text/plain").unwrap();
assert_eq!(
headers.get("accept").unwrap().unwrap(),
"application/json, text/plain"
);
headers.delete("content-type").unwrap();
assert!(!headers.has("content-type").unwrap());
}
#[test]
fn test_headers_validation() {
let mut headers = Headers::new();
assert!(headers.set("", "value").is_err());
assert!(headers.set("test\x00", "value").is_err());
assert!(headers.set("test", "value\r\n").is_err());
assert!(headers.set("x-custom", "value").is_ok());
assert!(headers.set("content-type", "application/json").is_ok());
}
#[test]
fn test_headers_iteration() {
let mut headers = Headers::new();
headers.set("a", "1").unwrap();
headers.set("b", "2").unwrap();
headers.set("c", "3").unwrap();
let entries: Vec<_> = headers.entries().collect();
assert_eq!(entries.len(), 3);
let keys: Vec<_> = headers.keys().collect();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"a"));
assert!(keys.contains(&"b"));
assert!(keys.contains(&"c"));
let values: Vec<_> = headers.values().collect();
assert_eq!(values.len(), 3);
assert!(values.contains(&"1"));
assert!(values.contains(&"2"));
assert!(values.contains(&"3"));
}
#[test]
fn test_headers_from_slice() {
let headers = Headers::from(
&[
("content-type", "application/json"),
("accept", "application/json"),
][..],
);
assert_eq!(
headers.get("content-type").unwrap().unwrap(),
"application/json"
);
assert_eq!(headers.get("accept").unwrap().unwrap(), "application/json");
}
#[test]
fn test_headers_from_array() {
let headers = Headers::from(&[
("content-type", "application/json"),
("accept", "application/json"),
]);
assert_eq!(
headers.get("content-type").unwrap().unwrap(),
"application/json"
);
assert_eq!(headers.get("accept").unwrap().unwrap(), "application/json");
}
#[test]
fn test_get_set_cookie() {
let mut headers = Headers::new();
headers
.set("set-cookie", "session=abc123, secure=true")
.unwrap();
let cookies = headers.get_set_cookie();
assert_eq!(cookies.len(), 2);
assert!(cookies.contains(&"session=abc123".to_string()));
assert!(cookies.contains(&"secure=true".to_string()));
}
}