use bytes::Bytes;
use http_body::{Body, Frame};
use http_body_util::combinators::UnsyncBoxBody;
use std::io::Write;
use std::pin::Pin;
use std::task::{Context, Poll, ready};
use wasmtime_wasi_http::p3::bindings::http::types::ErrorCode;
pub(crate) struct ZstdDecodeBody {
inner: UnsyncBoxBody<Bytes, ErrorCode>,
decoder: Option<zstd::stream::write::Decoder<'static, Vec<u8>>>,
}
impl ZstdDecodeBody {
pub(crate) fn new(inner: UnsyncBoxBody<Bytes, ErrorCode>) -> Result<Self, ErrorCode> {
let decoder = zstd::stream::write::Decoder::new(Vec::new()).map_err(decode_failed)?;
Ok(Self {
inner,
decoder: Some(decoder),
})
}
}
impl Body for ZstdDecodeBody {
type Data = Bytes;
type Error = ErrorCode;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Bytes>, ErrorCode>>> {
let this = self.get_mut();
loop {
if this.decoder.is_none() {
return Poll::Ready(None);
}
match ready!(Pin::new(&mut this.inner).poll_frame(cx)) {
Some(Ok(frame)) => {
let Ok(chunk) = frame.into_data() else {
continue;
};
let decoder = this
.decoder
.as_mut()
.expect("the None case returned before polling");
if let Err(error) = decoder.write_all(&chunk).and_then(|()| decoder.flush()) {
this.decoder = None;
return Poll::Ready(Some(Err(decode_failed(error))));
}
let decoded = std::mem::take(decoder.get_mut());
if decoded.is_empty() {
continue;
}
return Poll::Ready(Some(Ok(Frame::data(Bytes::from(decoded)))));
}
Some(Err(error)) => {
this.decoder = None;
return Poll::Ready(Some(Err(error)));
}
None => {
let mut decoder = this
.decoder
.take()
.expect("the None case returned before polling");
if let Err(error) = decoder.flush() {
return Poll::Ready(Some(Err(decode_failed(error))));
}
let decoded = decoder.into_inner();
if decoded.is_empty() {
return Poll::Ready(None);
}
return Poll::Ready(Some(Ok(Frame::data(Bytes::from(decoded)))));
}
}
}
}
}
fn decode_failed(error: std::io::Error) -> ErrorCode {
ErrorCode::InternalError(Some(format!("zstd decode failed: {error}")))
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
fn body_of(chunks: Vec<Bytes>) -> UnsyncBoxBody<Bytes, ErrorCode> {
http_body_util::StreamBody::new(futures::stream::iter(
chunks
.into_iter()
.map(|chunk| Ok::<_, ErrorCode>(Frame::data(chunk))),
))
.boxed_unsync()
}
async fn decode(chunks: Vec<Bytes>) -> Vec<u8> {
ZstdDecodeBody::new(body_of(chunks))
.unwrap()
.collect()
.await
.unwrap()
.to_bytes()
.to_vec()
}
fn incompressible(len: usize) -> Vec<u8> {
let mut state = 0x2545_f491_4f6c_dd1du64;
(0..len)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state >> 24) as u8
})
.collect()
}
#[tokio::test]
async fn decodes_a_stream_split_across_frames() {
let original = incompressible(200_000);
let compressed = zstd::encode_all(original.as_slice(), 3).unwrap();
let chunks = compressed
.chunks(1024)
.map(Bytes::copy_from_slice)
.collect::<Vec<_>>();
assert!(
chunks.len() > 100,
"the split has to exercise many frames, got {}",
chunks.len()
);
assert_eq!(decode(chunks).await, original);
}
#[tokio::test]
async fn decodes_a_stream_arriving_whole() {
let original = b"the quick brown fox".repeat(500);
let compressed = zstd::encode_all(original.as_slice(), 3).unwrap();
assert_eq!(decode(vec![Bytes::from(compressed)]).await, original);
}
#[tokio::test]
async fn an_empty_object_decodes_to_nothing() {
let compressed = zstd::encode_all(&[][..], 3).unwrap();
assert!(decode(vec![Bytes::from(compressed)]).await.is_empty());
}
#[tokio::test]
async fn trailers_do_not_reach_the_reader() {
let original = b"payload".repeat(100);
let compressed = zstd::encode_all(original.as_slice(), 3).unwrap();
let body = http_body_util::StreamBody::new(futures::stream::iter(vec![
Ok::<_, ErrorCode>(Frame::data(Bytes::from(compressed))),
Ok(Frame::trailers(hyper::HeaderMap::new())),
]))
.boxed_unsync();
let decoded = ZstdDecodeBody::new(body)
.unwrap()
.collect()
.await
.unwrap()
.to_bytes();
assert_eq!(decoded.as_ref(), original.as_slice());
}
#[tokio::test]
async fn a_truncated_stream_reports_what_arrived() {
let original = b"payload".repeat(1000);
let compressed = zstd::encode_all(original.as_slice(), 3).unwrap();
let truncated = Bytes::copy_from_slice(&compressed[..compressed.len() / 2]);
let decoded = decode(vec![truncated]).await;
assert!(decoded.len() < original.len());
assert_eq!(decoded.as_slice(), &original[..decoded.len()]);
}
#[tokio::test]
async fn a_transport_error_is_not_swallowed() {
let body = http_body_util::StreamBody::new(futures::stream::iter(vec![Err::<
Frame<Bytes>,
ErrorCode,
>(
ErrorCode::ConnectionTerminated,
)]))
.boxed_unsync();
let result = ZstdDecodeBody::new(body).unwrap().collect().await;
assert!(matches!(result, Err(ErrorCode::ConnectionTerminated)));
}
#[tokio::test]
async fn bytes_that_are_not_zstd_fail_rather_than_pass_through() {
let result = ZstdDecodeBody::new(body_of(vec![Bytes::from_static(b"not zstd at all")]))
.unwrap()
.collect()
.await;
assert!(matches!(result, Err(ErrorCode::InternalError(Some(_)))));
}
}