use std::io::{self, BufRead, BufReader, Read};
const MAX_SIZE_LINE: usize = 1024;
enum State {
Size,
Data(u64),
DataEnd,
Done,
}
pub(crate) struct ChunkedReader<R> {
inner: BufReader<R>,
state: State,
}
impl<R: Read> ChunkedReader<R> {
pub(crate) fn new(inner: R) -> Self {
ChunkedReader {
inner: BufReader::new(inner),
state: State::Size,
}
}
fn read_line(&mut self) -> io::Result<Option<Vec<u8>>> {
let mut line = Vec::new();
let n = (&mut self.inner)
.take(MAX_SIZE_LINE as u64 + 2)
.read_until(b'\n', &mut line)?;
if n == 0 {
return Ok(None);
}
if line.last() != Some(&b'\n') {
return Err(if n > MAX_SIZE_LINE {
invalid("chunk-size line too long")
} else {
unexpected_eof("stream closed inside a chunk-size line")
});
}
line.pop();
if line.last() == Some(&b'\r') {
line.pop();
}
Ok(Some(line))
}
fn parse_size(line: &[u8]) -> io::Result<u64> {
let digits = line.split(|&b| b == b';').next().unwrap_or_default();
let digits = std::str::from_utf8(digits)
.map_err(|_| invalid("chunk size is not ASCII"))?
.trim();
u64::from_str_radix(digits, 16).map_err(|_| invalid("chunk size is not hex"))
}
fn skip_trailers(&mut self) -> io::Result<()> {
while let Some(line) = self.read_line()? {
if line.is_empty() {
break;
}
}
Ok(())
}
}
impl<R: Read> Read for ChunkedReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
loop {
match self.state {
State::Done => return Ok(0),
State::Size => {
let Some(line) = self.read_line()? else {
self.state = State::Done;
return Ok(0);
};
match Self::parse_size(&line)? {
0 => {
self.skip_trailers()?;
self.state = State::Done;
}
size => self.state = State::Data(size),
}
}
State::Data(left) => {
let want = buf.len().min(usize::try_from(left).unwrap_or(usize::MAX));
let n = self.inner.read(&mut buf[..want])?;
if n == 0 {
return Err(unexpected_eof("stream closed inside a chunk"));
}
let left = left - n as u64;
self.state = if left == 0 {
State::DataEnd
} else {
State::Data(left)
};
return Ok(n);
}
State::DataEnd => match self.read_line()? {
Some(line) if line.is_empty() => self.state = State::Size,
Some(_) => return Err(invalid("chunk payload longer than its size")),
None => return Err(unexpected_eof("stream closed before a chunk's CRLF")),
},
}
}
}
}
fn invalid(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg)
}
fn unexpected_eof(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::UnexpectedEof, msg)
}
#[cfg(test)]
mod tests {
use super::*;
struct Trickle<'a> {
data: &'a [u8],
step: usize,
}
impl Read for Trickle<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.step.min(buf.len()).min(self.data.len());
buf[..n].copy_from_slice(&self.data[..n]);
self.data = &self.data[n..];
Ok(n)
}
}
fn decode(body: &[u8]) -> io::Result<Vec<u8>> {
let mut out = Vec::new();
ChunkedReader::new(body).read_to_end(&mut out)?;
Ok(out)
}
#[test]
fn strips_framing() {
let body = b"19\r\nblink 46823 period=500ms\n\r\n4\r\nnext\r\n0\r\n\r\n";
assert_eq!(decode(body).unwrap(), b"blink 46823 period=500ms\nnext");
}
#[test]
fn decodes_byte_at_a_time() {
let body = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
let mut out = Vec::new();
ChunkedReader::new(Trickle {
data: body,
step: 1,
})
.read_to_end(&mut out)
.unwrap();
assert_eq!(out, b"hello world");
}
#[test]
fn small_caller_buffer_spans_a_chunk() {
let mut r = ChunkedReader::new(&b"a\r\n0123456789\r\n0\r\n\r\n"[..]);
let mut buf = [0u8; 4];
let mut out = Vec::new();
loop {
let n = r.read(&mut buf).unwrap();
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
}
assert_eq!(out, b"0123456789");
}
#[test]
fn payload_may_contain_crlf_and_hex_lookalikes() {
let body = b"8\r\n64\r\n\r\nff\r\n0\r\n\r\n";
assert_eq!(decode(body).unwrap(), b"64\r\n\r\nff");
}
#[test]
fn accepts_uppercase_hex_extensions_and_trailers() {
let body = b"A;name=val\r\n0123456789\r\n0\r\nX-Trailer: 1\r\n\r\n";
assert_eq!(decode(body).unwrap(), b"0123456789");
}
#[test]
fn accepts_bare_lf() {
assert_eq!(decode(b"3\nabc\n0\n\n").unwrap(), b"abc");
}
#[test]
fn close_between_chunks_is_a_clean_end() {
assert_eq!(decode(b"3\r\nabc\r\n").unwrap(), b"abc");
assert_eq!(decode(b"").unwrap(), b"");
}
#[test]
fn close_inside_a_chunk_is_an_error() {
let err = decode(b"a\r\nabc").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
let err = decode(b"3\r\nabc").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
let err = decode(b"3").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
}
#[test]
fn rejects_bad_framing() {
assert_eq!(
decode(b"zz\r\nabc\r\n").unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(
decode(b"2\r\nabc\r\n").unwrap_err().kind(),
io::ErrorKind::InvalidData
);
let long = vec![b'1'; MAX_SIZE_LINE + 10];
assert_eq!(
decode(&long).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn nothing_after_the_terminator_is_read() {
let body = b"3\r\nabc\r\n0\r\n\r\ngarbage";
assert_eq!(decode(body).unwrap(), b"abc");
}
}