use super::{Body, BodyType};
use crate::{BufWriter, Headers, HttpConfig, body::BodySource, h3::Frame};
use futures_lite::{AsyncWrite, AsyncWriteExt};
use std::{
future::poll_fn,
io::{Result, Write},
pin::Pin,
task::{Context, Poll, ready},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyFraming {
Raw,
Chunked {
keep_open: bool,
},
H3Data,
}
const MIN_CHUNK_LEN: usize = 16;
impl Body {
#[cfg(feature = "unstable")]
#[doc(hidden)]
pub async fn write_into<W: AsyncWrite + Unpin>(
self,
sink: &mut BufWriter<W>,
framing: BodyFraming,
config: &HttpConfig,
) -> Result<Option<Headers>> {
write_into(self, sink, framing, config).await
}
#[cfg(not(feature = "unstable"))]
pub(crate) async fn write_into<W: AsyncWrite + Unpin>(
self,
sink: &mut BufWriter<W>,
framing: BodyFraming,
config: &HttpConfig,
) -> Result<Option<Headers>> {
write_into(self, sink, framing, config).await
}
}
async fn write_into<W: AsyncWrite + Unpin>(
body: Body,
sink: &mut BufWriter<W>,
framing: BodyFraming,
config: &HttpConfig,
) -> Result<Option<Headers>> {
match body.0 {
BodyType::Empty => {
if framing == (BodyFraming::Chunked { keep_open: false }) {
sink.buffer_mut().extend_from_slice(b"0\r\n");
}
Ok(None)
}
BodyType::Static { content, cursor } => {
write_static(&content[cursor..], sink, framing).await?;
Ok(None)
}
BodyType::Streaming {
async_read,
progress,
len,
done,
..
} => {
write_streaming(
async_read.into_inner(),
len,
progress,
done,
sink,
framing,
config,
)
.await
}
}
}
async fn write_static<W: AsyncWrite + Unpin>(
content: &[u8],
sink: &mut BufWriter<W>,
framing: BodyFraming,
) -> Result<()> {
match framing {
BodyFraming::Raw => {
if !content.is_empty() {
sink.write_all(content).await?;
}
}
BodyFraming::Chunked { keep_open } => {
if !content.is_empty() {
write!(sink.buffer_mut(), "{:X}\r\n", content.len())?;
sink.write_all(content).await?;
sink.buffer_mut().extend_from_slice(b"\r\n");
}
if !keep_open {
sink.buffer_mut().extend_from_slice(b"0\r\n");
}
}
BodyFraming::H3Data => {
if !content.is_empty() {
write_h3_frame_header(sink.buffer_mut(), content.len() as u64);
sink.write_all(content).await?;
}
}
}
Ok(())
}
async fn write_streaming<W: AsyncWrite + Unpin>(
mut source: Pin<Box<dyn BodySource>>,
len: Option<u64>,
mut progress: u64,
mut done: bool,
sink: &mut BufWriter<W>,
framing: BodyFraming,
config: &HttpConfig,
) -> Result<Option<Headers>> {
let chunk_len = config.body_write_chunk_len.max(MIN_CHUNK_LEN);
let loops_per_yield = config.copy_loops_per_yield;
if !done
&& framing == BodyFraming::H3Data
&& let Some(len) = len
&& len > 0
{
write_h3_frame_header(sink.buffer_mut(), len);
}
poll_fn(|cx| {
poll_write_streaming(
cx,
source.as_mut(),
len,
&mut progress,
&mut done,
sink,
framing,
chunk_len,
loops_per_yield,
)
})
.await?;
Ok(source.as_mut().trailers())
}
#[allow(
clippy::too_many_arguments,
reason = "poll fn over write_streaming's locals"
)]
fn poll_write_streaming<W: AsyncWrite + Unpin>(
cx: &mut Context<'_>,
mut source: Pin<&mut dyn BodySource>,
len: Option<u64>,
progress: &mut u64,
done: &mut bool,
sink: &mut BufWriter<W>,
framing: BodyFraming,
chunk_len: usize,
loops_per_yield: usize,
) -> Poll<Result<()>> {
if *done {
return Poll::Ready(Ok(()));
}
for _ in 0..loops_per_yield {
if sink.buffer_mut().len() >= chunk_len {
ready!(Pin::new(&mut *sink).poll_flush_buf(cx))?;
}
let cap = len.map_or(chunk_len, |len| {
usize::try_from(len - *progress).map_or(chunk_len, |remaining| remaining.min(chunk_len))
});
let reserved = match framing {
BodyFraming::Raw => 0,
BodyFraming::Chunked { .. } => hex_width(cap) + 2,
BodyFraming::H3Data if len.is_some() => 0,
BodyFraming::H3Data => Frame::Data(cap as u64).encoded_len(),
};
let buffer = sink.buffer_mut();
let start = buffer.len();
buffer.resize(start + reserved + cap, 0);
match source
.as_mut()
.poll_read(cx, &mut buffer[start + reserved..])
{
Poll::Pending => {
buffer.truncate(start);
ready!(Pin::new(&mut *sink).poll_flush(cx))?;
return Poll::Pending;
}
Poll::Ready(Err(e)) => {
buffer.truncate(start);
return Poll::Ready(Err(e));
}
Poll::Ready(Ok(0)) => {
buffer.truncate(start);
*done = true;
if framing == (BodyFraming::Chunked { keep_open: false }) {
buffer.extend_from_slice(b"0\r\n");
}
return Poll::Ready(Ok(()));
}
Poll::Ready(Ok(bytes)) => {
*progress += bytes as u64;
match framing {
BodyFraming::Raw => buffer.truncate(start + bytes),
BodyFraming::H3Data if len.is_some() => buffer.truncate(start + bytes),
BodyFraming::Chunked { .. } => {
let prefix = hex_width(bytes) + 2;
let mut slot = &mut buffer[start..start + prefix];
write!(slot, "{bytes:X}\r\n")?;
if prefix < reserved {
buffer.copy_within(
start + reserved..start + reserved + bytes,
start + prefix,
);
}
buffer.truncate(start + prefix + bytes);
buffer.extend_from_slice(b"\r\n");
}
BodyFraming::H3Data => {
let frame = Frame::Data(bytes as u64);
let prefix = frame.encoded_len();
frame.encode(&mut buffer[start..start + prefix]);
if prefix < reserved {
buffer.copy_within(
start + reserved..start + reserved + bytes,
start + prefix,
);
}
buffer.truncate(start + prefix + bytes);
}
}
}
}
}
cx.waker().wake_by_ref();
Poll::Pending
}
fn write_h3_frame_header(buffer: &mut Vec<u8>, len: u64) {
let frame = Frame::Data(len);
let start = buffer.len();
buffer.resize(start + frame.encoded_len(), 0);
frame.encode(&mut buffer[start..]);
}
fn hex_width(n: usize) -> usize {
if n == 0 { 1 } else { n.ilog(16) as usize + 1 }
}
#[cfg(test)]
mod tests;