use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt};
use super::FetchError;
const MAX_SIZE_LINE_BYTES: u64 = 1024;
pub(super) async fn read_body<R: AsyncBufRead + Unpin>(
mut reader: R,
limit: usize,
) -> Result<Vec<u8>, FetchError> {
let mut body = Vec::new();
loop {
let size = parse_size(&read_line(&mut reader).await?)?;
if size == 0 {
return Ok(body);
}
if size > (limit - body.len()) as u64 {
return Err(FetchError::TooLarge { limit });
}
let read = (&mut reader).take(size).read_to_end(&mut body).await?;
if (read as u64) < size {
return Err(malformed("chunked body ended inside a chunk"));
}
if !read_line(&mut reader).await?.is_empty() {
return Err(malformed("chunk data ran past its declared size"));
}
}
}
async fn read_line<R: AsyncBufRead + Unpin>(reader: &mut R) -> Result<String, FetchError> {
let mut line = String::new();
(&mut *reader)
.take(MAX_SIZE_LINE_BYTES)
.read_line(&mut line)
.await?;
let Some(line) = line.strip_suffix('\n') else {
return Err(malformed(
"chunked body ended early, or a chunk line ran too long",
));
};
Ok(line.strip_suffix('\r').unwrap_or(line).to_owned())
}
fn parse_size(line: &str) -> Result<u64, FetchError> {
let digits = line.split(';').next().unwrap_or(line).trim_end();
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(malformed("chunk size was not a hex number"));
}
u64::from_str_radix(digits, 16).map_err(|_overflow| malformed("chunk size was too large"))
}
fn malformed(reason: &str) -> FetchError {
FetchError::Transport(std::io::Error::other(reason))
}