use crate::error::Error;
use crate::limits::DecoderLimits;
use crate::trailer::is_prohibited_trailer_field;
use crate::validate::{
is_pchar_or_slash, is_query_char, is_sub_delim_byte, is_token_char, is_unreserved_byte,
is_valid_field_value, is_valid_header_name, is_valid_token,
};
use super::phase::DecodePhase;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyKind {
ContentLength(usize),
Chunked,
CloseDelimited,
None,
Tunnel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BodyProgress {
Continue,
Complete { trailers: Vec<(String, String)> },
}
#[derive(Debug)]
pub(crate) struct BodyDecoder {
trailers: Vec<(String, String)>,
body_consumed: usize,
trailer_count: usize,
}
impl Default for BodyDecoder {
fn default() -> Self {
Self::new()
}
}
impl BodyDecoder {
pub fn new() -> Self {
Self {
trailers: Vec::new(),
body_consumed: 0,
trailer_count: 0,
}
}
pub fn reset(&mut self) {
self.trailers.clear();
self.body_consumed = 0;
self.trailer_count = 0;
}
pub fn peek_body<'a>(&self, buf: &'a [u8], phase: &DecodePhase) -> Option<&'a [u8]> {
match phase {
DecodePhase::BodyContentLength { remaining } => {
if buf.is_empty() {
return None;
}
let available = buf.len().min(*remaining);
if available > 0 {
Some(&buf[..available])
} else {
None
}
}
DecodePhase::BodyChunkedSize => None,
DecodePhase::BodyChunkedData { remaining } => {
if buf.is_empty() {
return None;
}
let available = buf.len().min(*remaining);
if available > 0 {
Some(&buf[..available])
} else {
None
}
}
DecodePhase::BodyCloseDelimited => {
if buf.is_empty() {
return None;
}
Some(buf)
}
DecodePhase::BodyChunkedDataCrlf
| DecodePhase::ChunkedTrailer
| DecodePhase::Complete
| DecodePhase::StartLine
| DecodePhase::Headers
| DecodePhase::Tunnel => None,
}
}
pub fn consume_body(
&mut self,
buf: &mut Vec<u8>,
phase: &mut DecodePhase,
len: usize,
limits: &DecoderLimits,
) -> Result<BodyProgress, Error> {
match phase {
DecodePhase::BodyContentLength { remaining } => {
if len > *remaining {
return Err(Error::InvalidData(
"consume_body: len exceeds remaining".to_string(),
));
}
if len > buf.len() {
return Err(Error::InvalidData(
"consume_body: len exceeds buffer".to_string(),
));
}
buf.drain(..len);
*remaining -= len;
self.body_consumed =
self.body_consumed
.checked_add(len)
.ok_or(Error::BodyTooLarge {
size: usize::MAX,
limit: limits.max_body_size,
})?;
if *remaining == 0 {
*phase = DecodePhase::Complete;
return Ok(BodyProgress::Complete {
trailers: Vec::new(),
});
}
Ok(BodyProgress::Continue)
}
DecodePhase::BodyChunkedSize => {
self.process_chunked_size(buf, phase, limits)?;
match phase {
DecodePhase::Complete => Ok(BodyProgress::Complete {
trailers: std::mem::take(&mut self.trailers),
}),
_ => Ok(BodyProgress::Continue),
}
}
DecodePhase::BodyChunkedData { remaining } => {
if len > *remaining {
return Err(Error::InvalidData(
"consume_body: len exceeds chunk remaining".to_string(),
));
}
if len > buf.len() {
return Err(Error::InvalidData(
"consume_body: len exceeds buffer".to_string(),
));
}
buf.drain(..len);
*remaining -= len;
self.body_consumed =
self.body_consumed
.checked_add(len)
.ok_or(Error::BodyTooLarge {
size: usize::MAX,
limit: limits.max_body_size,
})?;
if *remaining == 0 {
*phase = DecodePhase::BodyChunkedDataCrlf;
if buf.len() >= 2 {
if buf[..2] != *b"\r\n" {
return Err(Error::InvalidData(
"invalid chunked encoding: expected CRLF after chunk data"
.to_string(),
));
}
buf.drain(..2);
*phase = DecodePhase::BodyChunkedSize;
}
}
Ok(BodyProgress::Continue)
}
DecodePhase::BodyChunkedDataCrlf => {
if buf.len() >= 2 {
if buf[..2] != *b"\r\n" {
return Err(Error::InvalidData(
"invalid chunked encoding: expected CRLF after chunk data".to_string(),
));
}
buf.drain(..2);
*phase = DecodePhase::BodyChunkedSize;
}
Ok(BodyProgress::Continue)
}
DecodePhase::ChunkedTrailer => {
self.process_trailers(buf, phase, limits)?;
match phase {
DecodePhase::Complete => Ok(BodyProgress::Complete {
trailers: std::mem::take(&mut self.trailers),
}),
_ => Ok(BodyProgress::Continue),
}
}
DecodePhase::BodyCloseDelimited => {
if len > buf.len() {
return Err(Error::InvalidData(
"consume_body: len exceeds buffer".to_string(),
));
}
let new_size = self
.body_consumed
.checked_add(len)
.ok_or(Error::BodyTooLarge {
size: usize::MAX,
limit: limits.max_body_size,
})?;
if new_size > limits.max_body_size {
return Err(Error::BodyTooLarge {
size: new_size,
limit: limits.max_body_size,
});
}
buf.drain(..len);
self.body_consumed = new_size;
Ok(BodyProgress::Continue)
}
DecodePhase::Complete => Ok(BodyProgress::Complete {
trailers: std::mem::take(&mut self.trailers),
}),
DecodePhase::StartLine | DecodePhase::Headers => Err(Error::InvalidData(
"consume_body called before decode_headers".to_string(),
)),
DecodePhase::Tunnel => Err(Error::InvalidData(
"consume_body cannot be used in tunnel mode, use take_remaining instead"
.to_string(),
)),
}
}
fn process_chunked_size(
&mut self,
buf: &mut Vec<u8>,
phase: &mut DecodePhase,
limits: &DecoderLimits,
) -> Result<(), Error> {
if !matches!(phase, DecodePhase::BodyChunkedSize) {
return Ok(());
}
if let Some(pos) = find_line(buf) {
if pos > limits.max_chunk_line_size {
return Err(Error::ChunkLineTooLong {
size: pos,
limit: limits.max_chunk_line_size,
});
}
let line_bytes = &buf[..pos];
let semi_pos = line_bytes.iter().position(|&b| b == b';');
let size_end = semi_pos.unwrap_or(pos);
let size_bytes = &line_bytes[..size_end];
let hex_end = size_bytes
.iter()
.position(|b| !b.is_ascii_hexdigit())
.unwrap_or(size_bytes.len());
if hex_end == 0 {
let display = String::from_utf8_lossy(size_bytes);
return Err(Error::InvalidData(format!(
"invalid chunk size: {}",
display
)));
}
let trailing = &size_bytes[hex_end..];
if !trailing.is_empty() {
if semi_pos.is_some() {
if !trailing.iter().all(|&b| b == b' ' || b == b'\t') {
let display = String::from_utf8_lossy(size_bytes);
return Err(Error::InvalidData(format!(
"invalid chunk size: {}",
display
)));
}
} else {
let display = String::from_utf8_lossy(size_bytes);
return Err(Error::InvalidData(format!(
"invalid chunk size: {}",
display
)));
}
}
let hex_bytes = &size_bytes[..hex_end];
let size_str = std::str::from_utf8(hex_bytes)
.map_err(|_| Error::InvalidData("invalid chunk size: not ASCII".to_string()))?;
let chunk_size = usize::from_str_radix(size_str, 16)
.map_err(|_| Error::InvalidData(format!("invalid chunk size: {}", size_str)))?;
if let Some(sp) = semi_pos {
validate_chunk_ext(&line_bytes[sp..])?;
}
buf.drain(..pos + 2);
if chunk_size == 0 {
*phase = DecodePhase::ChunkedTrailer;
return self.process_trailers(buf, phase, limits);
} else {
let new_size =
self.body_consumed
.checked_add(chunk_size)
.ok_or(Error::BodyTooLarge {
size: usize::MAX,
limit: limits.max_body_size,
})?;
if new_size > limits.max_body_size {
return Err(Error::BodyTooLarge {
size: new_size,
limit: limits.max_body_size,
});
}
*phase = DecodePhase::BodyChunkedData {
remaining: chunk_size,
};
}
}
Ok(())
}
fn process_trailers(
&mut self,
buf: &mut Vec<u8>,
phase: &mut DecodePhase,
limits: &DecoderLimits,
) -> Result<(), Error> {
while matches!(phase, DecodePhase::ChunkedTrailer) {
if let Some(pos) = find_line(buf) {
if pos == 0 {
buf.drain(..2);
*phase = DecodePhase::Complete;
return Ok(());
} else {
if pos > limits.max_header_line_size {
return Err(Error::HeaderLineTooLong {
size: pos,
limit: limits.max_header_line_size,
});
}
if self.trailer_count >= limits.max_headers_count {
return Err(Error::TooManyHeaders {
count: self.trailer_count + 1,
limit: limits.max_headers_count,
});
}
let line = String::from_utf8(buf[..pos].to_vec())
.map_err(|e| Error::InvalidData(format!("invalid UTF-8: {e}")))?;
buf.drain(..pos + 2);
let (name, value) = parse_header_line(&line)?;
if is_prohibited_trailer_field(&name) {
return Err(Error::InvalidData(format!(
"prohibited trailer field: {}",
name
)));
}
self.trailers.push((name, value));
self.trailer_count += 1;
}
} else {
return Ok(());
}
}
Ok(())
}
}
fn validate_chunk_ext(ext: &[u8]) -> Result<(), Error> {
let mut i = 0;
while i < ext.len() {
i = skip_bws(ext, i);
if i >= ext.len() {
break;
}
if ext[i] != b';' {
return Err(Error::InvalidData(
"invalid chunk-ext: expected ';'".to_string(),
));
}
i += 1;
i = skip_bws(ext, i);
let name_start = i;
while i < ext.len() && is_token_char(ext[i]) {
i += 1;
}
if i == name_start {
return Err(Error::InvalidData(
"invalid chunk-ext: empty or invalid name".to_string(),
));
}
i = skip_bws(ext, i);
if i < ext.len() && ext[i] == b'=' {
i += 1;
i = skip_bws(ext, i);
if i >= ext.len() {
return Err(Error::InvalidData(
"invalid chunk-ext: missing value after '='".to_string(),
));
}
if ext[i] == b'"' {
i = parse_quoted_string(ext, i)?;
} else {
let val_start = i;
while i < ext.len() && is_token_char(ext[i]) {
i += 1;
}
if i == val_start {
return Err(Error::InvalidData(
"invalid chunk-ext: empty or invalid value".to_string(),
));
}
}
}
}
Ok(())
}
fn skip_bws(data: &[u8], mut pos: usize) -> usize {
while pos < data.len() && (data[pos] == b' ' || data[pos] == b'\t') {
pos += 1;
}
pos
}
fn parse_quoted_string(data: &[u8], start: usize) -> Result<usize, Error> {
debug_assert_eq!(data[start], b'"');
let mut i = start + 1;
while i < data.len() {
let b = data[i];
if b == b'"' {
return Ok(i + 1);
}
if b == b'\\' {
i += 1;
if i >= data.len() {
return Err(Error::InvalidData(
"invalid chunk-ext: incomplete quoted-pair".to_string(),
));
}
let escaped = data[i];
if escaped == b'\t'
|| escaped == b' '
|| (0x21..=0x7E).contains(&escaped)
|| escaped >= 0x80
{
i += 1;
} else {
return Err(Error::InvalidData(
"invalid chunk-ext: invalid quoted-pair character".to_string(),
));
}
} else if is_qdtext(b) {
i += 1;
} else {
return Err(Error::InvalidData(
"invalid chunk-ext: invalid character in quoted-string".to_string(),
));
}
}
Err(Error::InvalidData(
"invalid chunk-ext: unterminated quoted-string".to_string(),
))
}
fn is_qdtext(b: u8) -> bool {
b == b'\t'
|| b == b' '
|| b == 0x21
|| (0x23..=0x5B).contains(&b)
|| (0x5D..=0x7E).contains(&b)
|| b >= 0x80
}
pub(crate) fn find_line(buf: &[u8]) -> Option<usize> {
buf.windows(2).position(|w| w == b"\r\n")
}
fn trim_ows(s: &str) -> &str {
let bytes = s.as_bytes();
let start = bytes
.iter()
.position(|&b| b != b' ' && b != b'\t')
.unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|&b| b != b' ' && b != b'\t')
.map(|i| i + 1)
.unwrap_or(0);
if start >= end {
""
} else {
&s[start..end]
}
}
pub(crate) fn parse_header_line(line: &str) -> Result<(String, String), Error> {
if line.starts_with(' ') || line.starts_with('\t') {
return Err(Error::InvalidData(
"invalid header line: obs-fold".to_string(),
));
}
if line.contains('\r') || line.contains('\n') {
return Err(Error::InvalidData(
"invalid header line: contains CR/LF".to_string(),
));
}
let (name, value) = line
.split_once(':')
.ok_or_else(|| Error::InvalidData("invalid header line: missing colon".to_string()))?;
if name.is_empty() {
return Err(Error::InvalidData(
"invalid header line: empty name".to_string(),
));
}
if name != name.trim() || name.bytes().any(|b| b == b' ' || b == b'\t') {
return Err(Error::InvalidData(
"invalid header line: invalid name whitespace".to_string(),
));
}
if !is_valid_header_name(name) {
return Err(Error::InvalidData(
"invalid header line: invalid name".to_string(),
));
}
let trimmed_value = trim_ows(value);
if !is_valid_field_value(trimmed_value) {
return Err(Error::InvalidData(
"invalid header line: invalid value (contains control characters)".to_string(),
));
}
Ok((name.to_string(), trimmed_value.to_string()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestTargetForm {
Origin,
Absolute,
Authority,
Asterisk,
}
pub(crate) fn parse_request_target_form(target: &str) -> Result<RequestTargetForm, Error> {
if target.is_empty() {
return Err(Error::InvalidData(
"invalid request-target: empty".to_string(),
));
}
if target == "*" {
return Ok(RequestTargetForm::Asterisk);
}
if target.starts_with('/') {
return validate_origin_form(target).map(|()| RequestTargetForm::Origin);
}
if target.contains("://") {
return validate_absolute_form(target);
}
if validate_authority_form(target).is_ok() {
return Ok(RequestTargetForm::Authority);
}
if let Some(_scheme_len) = detect_scheme(target) {
return validate_absolute_form(target);
}
Err(Error::InvalidData(
"invalid request-target: unrecognized form".to_string(),
))
}
fn validate_absolute_form(target: &str) -> Result<RequestTargetForm, Error> {
let scheme_len = detect_scheme(target)
.ok_or_else(|| Error::InvalidData("invalid request-target: invalid scheme".to_string()))?;
let scheme = &target[..scheme_len];
if !scheme
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
{
return Err(Error::InvalidData(
"invalid request-target: invalid scheme".to_string(),
));
}
for c in scheme.chars().skip(1) {
if !c.is_ascii_alphanumeric() && c != '+' && c != '-' && c != '.' {
return Err(Error::InvalidData(
"invalid request-target: invalid scheme".to_string(),
));
}
}
if target.contains('#') {
return Err(Error::InvalidData(
"invalid request-target: fragment not allowed".to_string(),
));
}
validate_ipv6_brackets(target)?;
let rest = &target[scheme_len + 1..];
validate_absolute_uri_parts(rest)?;
let scheme_lower = scheme.to_ascii_lowercase();
if scheme_lower == "http" || scheme_lower == "https" {
let Some(after_slashes) = rest.strip_prefix("//") else {
return Err(Error::InvalidData(
"http/https URI must contain \"://\" (RFC 9110 Section 4.2)".to_string(),
));
};
let authority_end = after_slashes
.find(['/', '?'])
.unwrap_or(after_slashes.len());
let authority = &after_slashes[..authority_end];
if authority.contains('@') {
return Err(Error::InvalidData(
"userinfo not allowed in http/https URI (RFC 9110 Section 4.2.4)".to_string(),
));
}
if authority.is_empty() || authority.starts_with(':') {
return Err(Error::InvalidData(
"empty host identifier in http/https URI (RFC 9110 Section 4.2)".to_string(),
));
}
}
Ok(RequestTargetForm::Absolute)
}
fn detect_scheme(target: &str) -> Option<usize> {
let bytes = target.as_bytes();
if bytes.is_empty() || !bytes[0].is_ascii_alphabetic() {
return None;
}
let colon_pos = bytes.iter().position(|&b| b == b':')?;
if colon_pos == 0 {
return None;
}
for &b in &bytes[1..colon_pos] {
if !b.is_ascii_alphanumeric() && b != b'+' && b != b'-' && b != b'.' {
return None;
}
}
if colon_pos + 1 >= bytes.len() {
return None;
}
Some(colon_pos)
}
fn validate_ipv6_brackets(target: &str) -> Result<(), Error> {
let open_count = target.chars().filter(|&c| c == '[').count();
let close_count = target.chars().filter(|&c| c == ']').count();
if open_count != close_count {
return Err(Error::InvalidData(
"invalid request-target: unmatched IPv6 brackets".to_string(),
));
}
let mut depth = 0i32;
for c in target.chars() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth < 0 {
return Err(Error::InvalidData(
"invalid request-target: unmatched IPv6 brackets".to_string(),
));
}
}
_ => {}
}
}
Ok(())
}
fn validate_absolute_uri_parts(rest: &str) -> Result<(), Error> {
let path_start = if let Some(after_slashes) = rest.strip_prefix("//") {
let authority_len = after_slashes
.find(['/', '?'])
.unwrap_or(after_slashes.len());
validate_authority_chars(&after_slashes[..authority_len])?;
authority_len + 2
} else {
0
};
let rest = &rest[path_start..];
let (path, query) = if let Some(pos) = rest.find('?') {
(&rest[..pos], Some(&rest[pos + 1..]))
} else {
(rest, None)
};
if !path.is_empty() {
validate_path_chars(path)?;
}
if let Some(q) = query {
validate_query_chars(q)?;
}
Ok(())
}
fn validate_authority_chars(authority: &str) -> Result<(), Error> {
let bytes = authority.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'%' {
if i + 2 >= bytes.len()
|| !bytes[i + 1].is_ascii_hexdigit()
|| !bytes[i + 2].is_ascii_hexdigit()
{
return Err(Error::InvalidData(
"invalid authority: invalid percent-encoding".to_string(),
));
}
i += 3;
} else if is_unreserved_byte(b)
|| is_sub_delim_byte(b)
|| b == b':'
|| b == b'@'
|| b == b'['
|| b == b']'
{
i += 1;
} else {
return Err(Error::InvalidData(format!(
"invalid authority: illegal character 0x{:02X}",
b
)));
}
}
Ok(())
}
fn validate_origin_form(target: &str) -> Result<(), Error> {
if !target.starts_with('/') {
return Err(Error::InvalidData(
"invalid origin-form: must start with '/'".to_string(),
));
}
if target.contains('#') {
return Err(Error::InvalidData(
"invalid request-target: fragment not allowed".to_string(),
));
}
let (path, query) = if let Some(pos) = target.find('?') {
(&target[..pos], Some(&target[pos + 1..]))
} else {
(target, None)
};
validate_path_chars(path)?;
if let Some(q) = query {
validate_query_chars(q)?;
}
Ok(())
}
fn validate_path_chars(path: &str) -> Result<(), Error> {
let bytes = path.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'%' {
if i + 2 >= bytes.len() {
return Err(Error::InvalidData(
"invalid path: incomplete percent-encoding".to_string(),
));
}
if !bytes[i + 1].is_ascii_hexdigit() || !bytes[i + 2].is_ascii_hexdigit() {
return Err(Error::InvalidData(
"invalid path: invalid percent-encoding".to_string(),
));
}
i += 3;
} else if is_pchar_or_slash(b) {
i += 1;
} else {
return Err(Error::InvalidData(format!(
"invalid path: illegal character 0x{:02X}",
b
)));
}
}
Ok(())
}
fn validate_query_chars(query: &str) -> Result<(), Error> {
let bytes = query.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'%' {
if i + 2 >= bytes.len() {
return Err(Error::InvalidData(
"invalid query: incomplete percent-encoding".to_string(),
));
}
if !bytes[i + 1].is_ascii_hexdigit() || !bytes[i + 2].is_ascii_hexdigit() {
return Err(Error::InvalidData(
"invalid query: invalid percent-encoding".to_string(),
));
}
i += 3;
} else if is_query_char(b) {
i += 1;
} else {
return Err(Error::InvalidData(format!(
"invalid query: illegal character 0x{:02X}",
b
)));
}
}
Ok(())
}
fn validate_authority_form(target: &str) -> Result<(), Error> {
let colon_pos = target
.rfind(':')
.ok_or_else(|| Error::InvalidData("invalid authority-form: missing port".to_string()))?;
let host = &target[..colon_pos];
let port_str = &target[colon_pos + 1..];
if host.is_empty() {
return Err(Error::InvalidData(
"invalid authority-form: empty host".to_string(),
));
}
if port_str.is_empty() {
return Err(Error::InvalidData(
"invalid authority-form: empty port".to_string(),
));
}
if !port_str.chars().all(|c| c.is_ascii_digit()) {
return Err(Error::InvalidData(
"invalid authority-form: port must be numeric".to_string(),
));
}
let _port: u16 = port_str
.parse()
.map_err(|_| Error::InvalidData("invalid authority-form: port out of range".to_string()))?;
crate::host::Host::parse(host)
.map_err(|_| Error::InvalidData("invalid authority-form: invalid host".to_string()))?;
Ok(())
}
pub(crate) fn validate_request_target_for_method(
method: &str,
form: &RequestTargetForm,
) -> Result<(), Error> {
match method {
"CONNECT" => {
if *form != RequestTargetForm::Authority {
return Err(Error::InvalidData(
"CONNECT method requires authority-form request-target".to_string(),
));
}
}
"OPTIONS" => {
if *form == RequestTargetForm::Authority {
return Err(Error::InvalidData(
"OPTIONS method does not allow authority-form request-target".to_string(),
));
}
}
_ => {
match form {
RequestTargetForm::Origin | RequestTargetForm::Absolute => {}
RequestTargetForm::Authority => {
return Err(Error::InvalidData(format!(
"{} method does not allow authority-form request-target",
method
)));
}
RequestTargetForm::Asterisk => {
return Err(Error::InvalidData(format!(
"{} method does not allow asterisk-form request-target",
method
)));
}
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TransferEncodingResult {
None,
Chunked,
Other,
}
pub(crate) fn parse_transfer_encoding_for_request(
headers: &[(String, String)],
) -> Result<bool, Error> {
let mut chunked_count = 0;
for (name, value) in headers {
if name.eq_ignore_ascii_case("Transfer-Encoding") {
for token in value.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
let base_coding = token.split(';').next().unwrap_or(token).trim();
if !is_valid_token(base_coding) {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: not a valid token".to_string(),
));
}
if base_coding.eq_ignore_ascii_case("chunked") {
if token.contains(';') {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: chunked does not accept parameters (RFC 9112 Section 7.1)".to_string(),
));
}
chunked_count += 1;
if chunked_count > 1 {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: duplicate chunked".to_string(),
));
}
} else {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: unsupported coding".to_string(),
));
}
}
}
}
Ok(chunked_count == 1)
}
pub(crate) fn parse_transfer_encoding_for_response(
headers: &[(String, String)],
) -> Result<TransferEncodingResult, Error> {
let mut all_tokens: Vec<String> = Vec::new();
let mut chunked_count = 0;
for (name, value) in headers {
if name.eq_ignore_ascii_case("Transfer-Encoding") {
for token in value.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
let base_coding = token.split(';').next().unwrap_or(token).trim();
if !is_valid_token(base_coding) {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: not a valid token".to_string(),
));
}
if base_coding.eq_ignore_ascii_case("chunked") {
if token.contains(';') {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: chunked does not accept parameters (RFC 9112 Section 7.1)".to_string(),
));
}
chunked_count += 1;
if chunked_count > 1 {
return Err(Error::InvalidData(
"invalid Transfer-Encoding: duplicate chunked".to_string(),
));
}
}
all_tokens.push(base_coding.to_ascii_lowercase());
}
}
}
if all_tokens.is_empty() {
return Ok(TransferEncodingResult::None);
}
if all_tokens.last().map(|s| s.as_str()) == Some("chunked") {
Ok(TransferEncodingResult::Chunked)
} else {
Ok(TransferEncodingResult::Other)
}
}
pub(crate) fn parse_content_length(headers: &[(String, String)]) -> Result<Option<usize>, Error> {
let mut value: Option<usize> = None;
for (name, raw_value) in headers {
if name.eq_ignore_ascii_case("Content-Length") {
let parsed = parse_content_length_value(raw_value)?;
if let Some(prev) = value {
if prev != parsed {
return Err(Error::InvalidData(
"invalid Content-Length: mismatched values".to_string(),
));
}
} else {
value = Some(parsed);
}
}
}
Ok(value)
}
fn parse_content_length_value(input: &str) -> Result<usize, Error> {
let mut result: Option<usize> = None;
for part in input.split(',') {
let part = part.trim();
if part.is_empty() {
return Err(Error::InvalidData(
"invalid Content-Length: empty value in list".to_string(),
));
}
if !part.chars().all(|c| c.is_ascii_digit()) {
return Err(Error::InvalidData(
"invalid Content-Length: not a number".to_string(),
));
}
let value = part
.parse::<usize>()
.map_err(|_| Error::InvalidData("invalid Content-Length: overflow".to_string()))?;
match result {
None => result = Some(value),
Some(prev) if prev != value => {
return Err(Error::InvalidData(
"invalid Content-Length: mismatched values in list".to_string(),
));
}
Some(_) => {} }
}
result.ok_or_else(|| Error::InvalidData("invalid Content-Length: empty".to_string()))
}
pub(crate) fn resolve_body_headers_for_request(
headers: &[(String, String)],
) -> Result<(bool, Option<usize>), Error> {
let transfer_encoding_chunked = parse_transfer_encoding_for_request(headers)?;
let content_length = parse_content_length(headers)?;
if transfer_encoding_chunked && content_length.is_some() {
return Err(Error::InvalidData(
"invalid message: both Transfer-Encoding and Content-Length".to_string(),
));
}
Ok((transfer_encoding_chunked, content_length))
}
pub(crate) fn resolve_body_headers_for_response(
headers: &[(String, String)],
) -> Result<(TransferEncodingResult, Option<usize>), Error> {
let te_result = parse_transfer_encoding_for_response(headers)?;
let content_length = parse_content_length(headers)?;
if te_result != TransferEncodingResult::None {
return Ok((te_result, None));
}
Ok((TransferEncodingResult::None, content_length))
}