use std::fmt;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll, ready};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
const COMPRESSED_BUF_SIZE: usize = 8192;
const WRITE_BUF_SIZE: usize = 16384; const DEFAULT_COMPRESS_LEVEL: u32 = 1;
fn counter_delta_to_usize(before: u64, after: u64) -> usize {
let delta = after.saturating_sub(before);
usize::try_from(delta).expect("flate2 byte counter delta is bounded by the caller buffer size")
}
#[derive(Debug, Clone, Copy)]
enum DrainState {
Idle,
Draining { pos: usize, len: usize },
}
fn poll_drain_buf<S: AsyncWrite>(
mut inner: Pin<&mut S>,
cx: &mut Context<'_>,
buf: &[u8],
state: &mut DrainState,
) -> Poll<io::Result<()>> {
let DrainState::Draining { pos, len } = state else {
return Poll::Ready(Ok(()));
};
while *pos < *len {
let n = ready!(inner.as_mut().poll_write(cx, &buf[*pos..*len]))?;
if n == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"inner stream write returned 0",
)));
}
*pos += n;
}
*state = DrainState::Idle;
Poll::Ready(Ok(()))
}
fn poll_compress_drain<S: AsyncWrite>(
compressor: &mut flate2::Compress,
mut inner: Pin<&mut S>,
cx: &mut Context<'_>,
buf: &mut [u8],
state: &mut DrainState,
flush: flate2::FlushCompress,
) -> Poll<io::Result<()>> {
loop {
ready!(poll_drain_buf(inner.as_mut(), cx, buf, state))?;
let before_out = compressor.total_out();
compressor
.compress(&[], buf, flush)
.map_err(io::Error::other)?;
let produced = counter_delta_to_usize(before_out, compressor.total_out());
if produced > 0 {
*state = DrainState::Draining {
pos: 0,
len: produced,
};
}
if produced < buf.len() {
ready!(poll_drain_buf(inner.as_mut(), cx, buf, state))?;
return Poll::Ready(Ok(()));
}
}
}
fn try_decompress(
decompressor: &mut flate2::Decompress,
input: &[u8],
buf: &mut ReadBuf<'_>,
stats: &mut u64,
) -> io::Result<(usize, bool)> {
let out_slice = buf.initialize_unfilled();
if out_slice.is_empty() {
return Ok((0, true));
}
let before_in = decompressor.total_in();
let before_out = decompressor.total_out();
let decompress_status = decompressor
.decompress(input, out_slice, flate2::FlushDecompress::None)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let consumed = counter_delta_to_usize(before_in, decompressor.total_in());
let produced = counter_delta_to_usize(before_out, decompressor.total_out());
if produced > 0 {
*stats += produced as u64;
buf.advance(produced);
}
Ok((
consumed,
produced > 0 || matches!(decompress_status, flate2::Status::StreamEnd),
))
}
pin_project_lite::pin_project! {
pub struct DecompressStream<S> {
#[pin]
inner: S,
decompressor: flate2::Decompress,
compressed_buf: Box<[u8]>,
compressed_pos: usize,
compressed_len: usize,
compressor: flate2::Compress,
write_buf: Box<[u8]>,
flush_buf: Box<[u8]>,
write_drain: DrainState,
flush_drain: DrainState,
bytes_compressed_in: u64,
bytes_decompressed_out: u64,
}
}
impl<S> DecompressStream<S> {
pub fn new(inner: S) -> Self {
Self::with_level(inner, DEFAULT_COMPRESS_LEVEL)
}
pub fn with_level(inner: S, level: u32) -> Self {
let level = flate2::Compression::new(level.min(9));
Self {
inner,
decompressor: flate2::Decompress::new(false),
compressed_buf: vec![0u8; COMPRESSED_BUF_SIZE].into_boxed_slice(),
compressed_pos: 0,
compressed_len: 0,
compressor: flate2::Compress::new(level, false),
write_buf: vec![0u8; WRITE_BUF_SIZE].into_boxed_slice(),
flush_buf: vec![0u8; WRITE_BUF_SIZE].into_boxed_slice(),
write_drain: DrainState::Idle,
flush_drain: DrainState::Idle,
bytes_compressed_in: 0,
bytes_decompressed_out: 0,
}
}
pub fn into_inner(self) -> S {
self.inner
}
#[inline]
pub const fn get_ref(&self) -> &S {
&self.inner
}
#[inline]
pub const fn get_mut(&mut self) -> &mut S {
&mut self.inner
}
#[inline]
pub const fn bandwidth_stats(&self) -> (u64, u64) {
(self.bytes_compressed_in, self.bytes_decompressed_out)
}
}
impl<S: fmt::Debug> fmt::Debug for DecompressStream<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DecompressStream")
.field("inner", &self.inner)
.field("compressed_pos", &self.compressed_pos)
.field("compressed_len", &self.compressed_len)
.field("bytes_compressed_in", &self.bytes_compressed_in)
.field("bytes_decompressed_out", &self.bytes_decompressed_out)
.finish_non_exhaustive()
}
}
impl<S: AsyncRead> AsyncRead for DecompressStream<S> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut this = self.project();
loop {
if *this.compressed_pos < *this.compressed_len {
let input = &this.compressed_buf[*this.compressed_pos..*this.compressed_len];
let (consumed, done) =
try_decompress(this.decompressor, input, buf, this.bytes_decompressed_out)?;
*this.compressed_pos += consumed;
if done {
return Poll::Ready(Ok(()));
}
continue;
}
let (_, done) =
try_decompress(this.decompressor, &[], buf, this.bytes_decompressed_out)?;
if done {
return Poll::Ready(Ok(()));
}
*this.compressed_pos = 0;
*this.compressed_len = 0;
let mut read_buf = ReadBuf::new(this.compressed_buf);
ready!(this.inner.as_mut().poll_read(cx, &mut read_buf))?;
let n = read_buf.filled().len();
if n == 0 {
return Poll::Ready(Ok(()));
}
*this.compressed_len = n;
*this.bytes_compressed_in += n as u64;
}
}
}
impl<S: AsyncWrite> AsyncWrite for DecompressStream<S> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let mut this = self.project();
ready!(poll_drain_buf(
this.inner.as_mut(),
cx,
this.write_buf,
this.write_drain,
))?;
let before_in = this.compressor.total_in();
let before_out = this.compressor.total_out();
this.compressor
.compress(buf, this.write_buf, flate2::FlushCompress::None)
.map_err(io::Error::other)?;
let consumed = counter_delta_to_usize(before_in, this.compressor.total_in());
let produced = counter_delta_to_usize(before_out, this.compressor.total_out());
if produced > 0 {
*this.write_drain = DrainState::Draining {
pos: 0,
len: produced,
};
match poll_drain_buf(this.inner.as_mut(), cx, this.write_buf, this.write_drain) {
Poll::Ready(Ok(())) | Poll::Pending => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
}
}
Poll::Ready(Ok(if consumed > 0 {
consumed
} else {
buf.len().min(1)
}))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let mut this = self.project();
ready!(poll_drain_buf(
this.inner.as_mut(),
cx,
this.write_buf,
this.write_drain
))?;
ready!(poll_compress_drain(
this.compressor,
this.inner.as_mut(),
cx,
this.flush_buf,
this.flush_drain,
flate2::FlushCompress::Sync,
))?;
this.inner.poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let mut this = self.project();
ready!(poll_drain_buf(
this.inner.as_mut(),
cx,
this.write_buf,
this.write_drain
))?;
ready!(poll_compress_drain(
this.compressor,
this.inner.as_mut(),
cx,
this.flush_buf,
this.flush_drain,
flate2::FlushCompress::Finish,
))?;
this.inner.poll_shutdown(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn deflate_compress(data: &[u8]) -> Vec<u8> {
use flate2::write::DeflateEncoder;
use std::io::Write;
let mut enc = DeflateEncoder::new(Vec::new(), flate2::Compression::default());
enc.write_all(data).unwrap();
enc.finish().unwrap()
}
async fn feed_compressed(
data: &[u8],
) -> (
tokio::io::DuplexStream,
DecompressStream<tokio::io::DuplexStream>,
) {
let compressed = deflate_compress(data);
let (mut tx, rx) = tokio::io::duplex(4096);
tx.write_all(&compressed).await.unwrap();
tx.shutdown().await.unwrap();
(tx, DecompressStream::new(rx))
}
#[tokio::test]
async fn test_roundtrip_read() {
let original = b"Hello, NNTP world! COMPRESS DEFLATE test data.\r\n";
let (_tx, mut stream) = feed_compressed(original).await;
let mut output = Vec::new();
stream.read_to_end(&mut output).await.unwrap();
assert_eq!(output, original);
}
#[tokio::test]
async fn test_roundtrip_write_flush() {
use flate2::read::DeflateDecoder;
use std::io::Read;
let original = b"GROUP alt.test\r\n";
let (reader, writer_side) = tokio::io::duplex(4096);
let mut stream = DecompressStream::new(writer_side);
stream.write_all(original).await.unwrap();
stream.flush().await.unwrap();
stream.shutdown().await.unwrap();
drop(stream);
let mut compressed_output = Vec::new();
let mut reader = reader;
reader.read_to_end(&mut compressed_output).await.unwrap();
let mut decoder = DeflateDecoder::new(&compressed_output[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).unwrap();
assert_eq!(decompressed, original);
}
#[tokio::test]
async fn test_large_data_roundtrip() {
let mut original = Vec::with_capacity(1024 * 1024);
for i in 0..1024 * 64 {
original.extend_from_slice(
format!("Line {i}: Some NNTP article content here\r\n").as_bytes(),
);
}
let compressed = deflate_compress(&original);
let (mut writer, reader) = tokio::io::duplex(16384);
tokio::spawn(async move {
writer.write_all(&compressed).await.unwrap();
writer.shutdown().await.unwrap();
});
let mut stream = DecompressStream::new(reader);
let mut output = Vec::new();
stream.read_to_end(&mut output).await.unwrap();
assert_eq!(output.len(), original.len());
assert_eq!(output, original);
}
#[tokio::test]
async fn test_bandwidth_stats() {
let original = b"Test data for bandwidth tracking\r\n";
let (_tx, mut stream) = feed_compressed(original).await;
let mut output = Vec::new();
stream.read_to_end(&mut output).await.unwrap();
let (compressed_in, decompressed_out) = stream.bandwidth_stats();
assert!(compressed_in > 0);
assert_eq!(decompressed_out, original.len() as u64);
}
#[test]
fn test_debug_impl() {
let stream = DecompressStream::new(std::io::Cursor::new(Vec::<u8>::new()));
let debug_str = format!("{stream:?}");
assert!(debug_str.contains("DecompressStream"));
assert!(debug_str.contains("compressed_pos"));
assert!(debug_str.contains("bytes_compressed_in"));
}
}