#[cfg(feature = "compression")]
use hotaru_lib::compression;
#[derive(Debug, Clone, PartialEq)]
pub enum TransferCoding {
Chunked,
Other(Box<str>),
}
impl TransferCoding {
pub fn from_string(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"chunked" => TransferCoding::Chunked,
other => TransferCoding::Other(other.into()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Chunked => "chunked",
Self::Other(s) => s,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ContentCoding {
Gzip,
Deflate,
Compress,
Brotli,
Zstd,
Other(Box<str>),
}
impl ContentCoding {
pub fn from_string(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"gzip" => ContentCoding::Gzip,
"deflate" => ContentCoding::Deflate,
"compress" => ContentCoding::Compress,
"br" => ContentCoding::Brotli,
"zstd" => ContentCoding::Zstd,
other => ContentCoding::Other(other.into()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Gzip => "gzip",
Self::Deflate => "deflate",
Self::Compress => "compress",
Self::Brotli => "br",
Self::Zstd => "zstd",
Self::Other(s) => s,
}
}
pub fn decode_compressed(encoding: &ContentCoding, data: &[u8]) -> std::io::Result<Vec<u8>> {
match encoding {
#[cfg(feature = "compression")]
ContentCoding::Gzip => compression::decompress_gzip(data),
#[cfg(feature = "compression")]
ContentCoding::Deflate => compression::decompress_deflate(data),
#[cfg(feature = "compression")]
ContentCoding::Brotli => compression::decompress_brotli(data),
#[cfg(feature = "compression")]
ContentCoding::Zstd => compression::decompress_zstd(data),
#[cfg(not(feature = "compression"))]
ContentCoding::Gzip
| ContentCoding::Deflate
| ContentCoding::Brotli
| ContentCoding::Zstd => Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"compression feature not enabled",
)),
ContentCoding::Compress => Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"compress encoding not supported",
)),
_ => Ok(data.to_vec()), }
}
pub fn encode_compressed(encoding: &ContentCoding, data: &[u8]) -> std::io::Result<Vec<u8>> {
match encoding {
#[cfg(feature = "compression")]
ContentCoding::Gzip => compression::compress_gzip(data),
#[cfg(feature = "compression")]
ContentCoding::Deflate => compression::compress_deflate(data),
#[cfg(feature = "compression")]
ContentCoding::Brotli => compression::compress_brotli(data),
#[cfg(feature = "compression")]
ContentCoding::Zstd => compression::compress_zstd(data, 1),
#[cfg(not(feature = "compression"))]
ContentCoding::Gzip
| ContentCoding::Deflate
| ContentCoding::Brotli
| ContentCoding::Zstd => Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"compression feature not enabled",
)),
ContentCoding::Compress => Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"compress encoding not supported",
)),
_ => Ok(data.to_vec()), }
}
}
#[derive(Debug, Clone, Default)]
pub struct TransferCodings {
codings: Vec<TransferCoding>,
}
impl TransferCodings {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, coding: TransferCoding) -> Result<(), &'static str> {
if matches!(coding, TransferCoding::Chunked) {
if self
.codings
.iter()
.any(|c| matches!(c, TransferCoding::Chunked))
{
return Err("chunked can only appear once");
}
} else if self
.codings
.last()
.is_some_and(|c| matches!(c, TransferCoding::Chunked))
{
return Err("no coding can follow chunked");
}
self.codings.push(coding);
Ok(())
}
pub fn is_chunked(&self) -> bool {
self.codings
.iter()
.any(|c| matches!(c, TransferCoding::Chunked))
}
pub fn is_identity(&self) -> bool {
self.codings.is_empty()
}
pub fn to_header(&self) -> String {
self.codings
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(", ")
}
}
#[derive(Debug, Clone, Default)]
pub struct ContentCodings {
codings: Vec<ContentCoding>,
}
impl ContentCodings {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, coding: ContentCoding) {
self.codings.push(coding);
}
pub fn is_identity(&self) -> bool {
self.codings.is_empty()
}
pub fn to_header(&self) -> String {
self.codings
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(", ")
}
pub fn decode_compressed(&self, data: Vec<u8>) -> std::io::Result<Vec<u8>> {
if self.is_identity() {
return Ok(data);
}
let mut result = data;
for coding in self.codings.iter().rev() {
result = ContentCoding::decode_compressed(coding, &result)?;
}
Ok(result)
}
pub fn encode_compressed(&self, data: Vec<u8>) -> std::io::Result<Vec<u8>> {
if self.is_identity() {
return Ok(data);
}
let mut result = data;
for coding in &self.codings {
result = ContentCoding::encode_compressed(coding, &result)?;
}
Ok(result)
}
}
#[derive(Debug, Clone, Default)]
pub struct HttpEncoding {
transfer: TransferCodings,
content: ContentCodings,
}
impl HttpEncoding {
pub fn from_headers(transfer_header: Option<String>, content_header: Option<String>) -> Self {
let mut transfer = TransferCodings::new();
let mut content = ContentCodings::new();
if let Some(header) = transfer_header {
for part in header.split(',') {
if !part.trim().is_empty() {
let coding = TransferCoding::from_string(part);
if let Err(e) = transfer.push(coding) {
eprintln!("[WARN] Invalid Transfer-Encoding: {}", e);
}
}
}
}
if let Some(header) = content_header {
for part in header.split(',') {
if !part.trim().is_empty() {
content.push(ContentCoding::from_string(part));
}
}
}
Self { transfer, content }
}
pub fn to_headers(&self) -> (Option<String>, Option<String>) {
let transfer = if !self.transfer.is_identity() {
Some(self.transfer.to_header())
} else {
None
};
let content = if !self.content.is_identity() {
Some(self.content.to_header())
} else {
None
};
(transfer, content)
}
pub fn transfer(&self) -> &TransferCodings {
&self.transfer
}
pub fn content(&self) -> &ContentCodings {
&self.content
}
}