mod test;
use std::str::FromStr;
use chrono::{DateTime, Utc};
use smallvec::SmallVec;
use strum_macros::{Display, EnumString};
#[derive(Debug, Display, EnumString, PartialEq)]
#[strum(serialize_all = "Train-Case", ascii_case_insensitive)]
pub enum HttpHeader {
Server,
Date,
ContentType,
ContentLength,
UserAgent,
Authorization,
Accept,
Host,
Connection,
SetCookie,
TransferEncoding,
Range,
}
#[derive(Debug, PartialEq)]
pub enum HttpHeaderValue {
Server(String),
Date(DateTime<Utc>),
ContentType(ContentType),
ContentLength(u64),
UserAgent(String),
Authorization(String),
Accept(SmallVec<[ContentType; 1]>),
Host(String),
Connection(Connection),
SetCookie(SmallVec<[(String, String); 1]>),
TransferEncoding(TransferEncoding),
Range((u16, u16)),
}
#[derive(Debug)]
pub struct HeaderEntry {
pub name: HttpHeader,
pub value: HttpHeaderValue,
}
impl HeaderEntry {
pub fn new(name: HttpHeader, value: HttpHeaderValue) -> Self {
HeaderEntry { name, value }
}
}
impl ToString for HeaderEntry {
fn to_string(&self) -> String {
let name = self.name.to_string();
let value = match &self.value {
HttpHeaderValue::ContentType(val) => val.to_string(),
HttpHeaderValue::ContentLength(val) => val.to_string(),
HttpHeaderValue::UserAgent(val) => val.clone(),
HttpHeaderValue::Authorization(val) => val.clone(),
HttpHeaderValue::Accept(vals) => vals
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(", "),
HttpHeaderValue::Host(val) => val.to_owned(),
HttpHeaderValue::Connection(val) => val.to_string(),
HttpHeaderValue::SetCookie(kv) => kv
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("; "),
HttpHeaderValue::TransferEncoding(val) => val.to_string(),
HttpHeaderValue::Range((begin, end)) => format!("bytes={}-{}", begin, end),
HttpHeaderValue::Date(date) => date.to_rfc2822(),
HttpHeaderValue::Server(server) => server.to_owned(),
};
format!("{}: {}", name, value)
}
}
impl FromStr for HeaderEntry {
type Err = String;
fn from_str(header: &str) -> Result<Self, Self::Err> {
let (name, value) = header
.split_once(':')
.map(|(name, value)| (name.trim(), value.trim()))
.ok_or_else(|| format!("Invalid header format: {}", header))?;
let header =
HttpHeader::from_str(name).map_err(|_| format!("Invalid header name: {}", name))?;
let value = match header {
HttpHeader::ContentType => {
let content_type = ContentType::from_str(value)
.map_err(|_| format!("Invalid content type: {}", value))?;
HttpHeaderValue::ContentType(content_type)
}
HttpHeader::ContentLength => {
let length = value
.parse::<u64>()
.map_err(|_| format!("Invalid content length: {}", value))?;
HttpHeaderValue::ContentLength(length)
}
HttpHeader::Connection => {
let connection = Connection::from_str(value)
.map_err(|_| format!("Invalid connection type: {}", value))?;
HttpHeaderValue::Connection(connection)
}
HttpHeader::TransferEncoding => {
let encoding = TransferEncoding::from_str(value)
.map_err(|_| format!("Invalid transfer encoding: {}", value))?;
HttpHeaderValue::TransferEncoding(encoding)
}
HttpHeader::Host => HttpHeaderValue::Host(value.to_string()),
HttpHeader::Accept => HttpHeaderValue::Accept(
value
.split(',')
.map(|x| x.trim())
.filter_map(|x| ContentType::from_str(x).ok())
.collect::<SmallVec<[ContentType; 1]>>(),
),
HttpHeader::UserAgent => HttpHeaderValue::UserAgent(value.to_string()),
HttpHeader::Authorization => HttpHeaderValue::Authorization(value.to_string()),
HttpHeader::SetCookie => HttpHeaderValue::SetCookie(parse_set_cookie(value)),
HttpHeader::Range => {
let range = parse_range_header(value);
if range.is_none() {
return Err("Invalid range header.".to_string());
}
HttpHeaderValue::Range(range.unwrap())
}
_ => return Err("Not implemented.".to_owned()),
};
Ok(HeaderEntry {
name: header,
value,
})
}
}
#[derive(Debug, Display, EnumString, PartialEq, Clone, Copy)]
pub enum ContentType {
#[strum(serialize = "application/json", serialize = "json")]
ApplicationJson,
#[strum(serialize = "application/xml", serialize = "xml")]
ApplicationXml,
#[strum(serialize = "application/octet-stream")]
ApplicationOctetStream,
#[strum(serialize = "text/html", serialize = "html")]
TextHtml,
#[strum(serialize = "text/plain", serialize = "txt")]
TextPlain,
#[strum(serialize = "text/css", serialize = "css")]
TextCss,
#[strum(
serialize = "text/javascript",
serialize = "application/javascript",
serialize = "js"
)]
TextJavascript,
#[strum(serialize = "image/png", serialize = "png")]
ImagePng,
#[strum(serialize = "image/jpeg", serialize = "jpeg")]
ImageJpeg,
#[strum(serialize = "image/gif", serialize = "gif")]
ImageGif,
#[strum(serialize = "multipart/form-data")]
MultipartFormData,
#[strum(serialize = "application/x-www-form-urlencoded")]
FormUrlEncoded,
#[strum(serialize = "*/*")]
Any,
}
#[derive(Debug, PartialEq, EnumString, Display)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum Connection {
KeepAlive,
Close,
}
#[derive(Debug, PartialEq, EnumString, Display)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum TransferEncoding {
Chunked,
}
fn parse_set_cookie(cookie_str: &str) -> SmallVec<[(String, String); 1]> {
cookie_str
.split("; ")
.filter_map(|pair| {
let parts: Vec<&str> = pair.split('=').collect();
if parts.len() >= 2 {
let key = parts[0].to_string();
let value = parts[1..].join("=");
Some((key, value))
} else {
None
}
})
.collect()
}
pub fn parse_range_header(range_str: &str) -> Option<(u16, u16)> {
if !range_str.starts_with("bytes=") {
return None;
}
let bytes_range = &range_str["bytes=".len()..];
let parts: Vec<&str> = bytes_range.split('-').collect();
if parts.len() != 2 {
return None;
}
let start = parts[0].parse::<u16>().ok()?;
let end = parts[1].parse::<u16>().ok()?;
if start > end {
return None;
}
Some((start, end))
}