Skip to main content

go_http/parse/
chunk.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Chunked transfer encoding reader/writer — port of Go's `internal/chunked`.
4use std::io::{self, Read, Write};
5
6use super::ParseError;
7use crate::header::Header;
8
9// ---------------------------------------------------------------------------
10// ChunkedReader
11// ---------------------------------------------------------------------------
12
13/// Decodes an HTTP/1.1 chunked body from the inner reader.
14/// Trailers (if any) are populated into `trailers` after EOF is reached.
15pub struct ChunkedReader<R: Read> {
16    inner:    R,
17    /// Bytes remaining in the current chunk (0 means we need a new chunk-size line).
18    remaining: u64,
19    /// Set true once the terminal zero-length chunk has been read.
20    done:      bool,
21    /// Populated with trailer headers after the terminal chunk is consumed.
22    pub trailers: Header,
23}
24
25impl<R: Read> ChunkedReader<R> {
26    pub fn new(inner: R) -> Self {
27        Self {
28            inner,
29            remaining: 0,
30            done: false,
31            trailers: Header::new(),
32        }
33    }
34
35    /// Consume the inner reader, returning it (useful after the body is fully read).
36    pub fn into_inner(self) -> R {
37        self.inner
38    }
39
40    fn read_chunk_size(&mut self) -> Result<u64, ParseError> {
41        let mut line = Vec::with_capacity(32);
42        let mut buf = [0u8; 1];
43        loop {
44            self.inner
45                .read_exact(&mut buf)
46                .map_err(|_| ParseError::UnexpectedEof)?;
47            if buf[0] == b'\n' {
48                break;
49            }
50            if buf[0] != b'\r' {
51                line.push(buf[0]);
52            }
53        }
54        // Strip chunk extensions (anything after ';').
55        let s = std::str::from_utf8(&line)
56            .map_err(|_| ParseError::InvalidChunkSize)?
57            .splitn(2, ';')
58            .next()
59            .unwrap_or("")
60            .trim();
61        u64::from_str_radix(s, 16).map_err(|_| ParseError::InvalidChunkSize)
62    }
63
64    fn consume_crlf(&mut self) -> Result<(), ParseError> {
65        let mut buf = [0u8; 2];
66        self.inner
67            .read_exact(&mut buf)
68            .map_err(|_| ParseError::UnexpectedEof)?;
69        if buf != *b"\r\n" {
70            return Err(ParseError::Other("expected CRLF after chunk data".into()));
71        }
72        Ok(())
73    }
74}
75
76impl<R: Read> Read for ChunkedReader<R> {
77    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
78        if self.done {
79            return Ok(0);
80        }
81
82        loop {
83            if self.remaining > 0 {
84                let to_read = buf.len().min(self.remaining as usize);
85                let n = self.inner.read(&mut buf[..to_read])?;
86                if n == 0 {
87                    return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "chunked body truncated"));
88                }
89                self.remaining -= n as u64;
90                if self.remaining == 0 {
91                    // Consume the CRLF that follows the chunk data.
92                    self.consume_crlf()
93                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
94                }
95                return Ok(n);
96            }
97
98            // Need next chunk size.
99            let size = self.read_chunk_size()
100                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
101
102            if size == 0 {
103                // Terminal chunk — read optional trailers.
104                self.trailers = super::read_headers(&mut self.inner, 4096)
105                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
106                self.done = true;
107                return Ok(0);
108            }
109
110            self.remaining = size;
111        }
112    }
113}
114
115// ---------------------------------------------------------------------------
116// ChunkedWriter
117// ---------------------------------------------------------------------------
118
119/// Encodes data written to it into HTTP/1.1 chunked transfer encoding.
120/// Call `finish()` to write the terminal `0\r\n\r\n`.
121pub struct ChunkedWriter<W: Write> {
122    inner: W,
123}
124
125impl<W: Write> ChunkedWriter<W> {
126    pub fn new(inner: W) -> Self {
127        Self { inner }
128    }
129
130    /// Write the terminal zero chunk and flush.
131    pub fn finish(mut self) -> io::Result<W> {
132        self.inner.write_all(b"0\r\n\r\n")?;
133        self.inner.flush()?;
134        Ok(self.inner)
135    }
136}
137
138impl<W: Write> Write for ChunkedWriter<W> {
139    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
140        if buf.is_empty() {
141            return Ok(0);
142        }
143        write!(self.inner, "{:X}\r\n", buf.len())?;
144        self.inner.write_all(buf)?;
145        self.inner.write_all(b"\r\n")?;
146        Ok(buf.len())
147    }
148
149    fn flush(&mut self) -> io::Result<()> {
150        self.inner.flush()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use std::io::{Cursor, Read};
158
159    #[test]
160    fn round_trip() {
161        let mut enc = Vec::new();
162        {
163            let mut w = ChunkedWriter::new(&mut enc);
164            w.write_all(b"Hello").unwrap();
165            w.write_all(b", World!").unwrap();
166            w.finish().unwrap();
167        }
168
169        let mut r = ChunkedReader::new(Cursor::new(enc));
170        let mut out = String::new();
171        r.read_to_string(&mut out).unwrap();
172        assert_eq!(out, "Hello, World!");
173    }
174
175    #[test]
176    fn empty_body() {
177        // Terminal chunk only.
178        let raw = b"0\r\n\r\n";
179        let mut r = ChunkedReader::new(Cursor::new(raw.as_ref()));
180        let mut out = Vec::new();
181        r.read_to_end(&mut out).unwrap();
182        assert!(out.is_empty());
183    }
184
185    #[test]
186    fn chunk_extensions_ignored() {
187        // Chunk size line with extension after ';'.
188        let raw = b"5;ext=foo\r\nHello\r\n0\r\n\r\n";
189        let mut r = ChunkedReader::new(Cursor::new(raw.as_ref()));
190        let mut out = String::new();
191        r.read_to_string(&mut out).unwrap();
192        assert_eq!(out, "Hello");
193    }
194}