Skip to main content

net_wire/http1/
chunked.rs

1//! Chunked HTTP/1 body views.
2
3use super::error::Http1ParseError;
4use super::fields::{Http1Fields, LineEndError, find_line_end, is_token};
5
6/// A validated nonzero HTTP/1 chunk.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct Http1Chunk<'a> {
9    size: usize,
10    data: &'a [u8],
11    extensions: &'a [u8],
12}
13
14impl<'a> Http1Chunk<'a> {
15    /// Returns the decoded chunk size.
16    pub const fn size(self) -> usize {
17        self.size
18    }
19
20    /// Returns the raw chunk data.
21    pub const fn data(self) -> &'a [u8] {
22        self.data
23    }
24
25    /// Returns the raw extension suffix, including its first semicolon and any optional whitespace.
26    pub const fn extensions(self) -> &'a [u8] {
27        self.extensions
28    }
29}
30
31/// A complete, exactly bounded chunked HTTP/1 body.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct Http1ChunkedBody<'a> {
34    bytes: &'a [u8],
35    chunks_end: usize,
36    decoded_len: usize,
37    last_chunk_extensions: &'a [u8],
38    trailers: Http1Fields<'a>,
39}
40
41impl<'a> Http1ChunkedBody<'a> {
42    /// Parses one complete chunked body through its trailer terminator.
43    ///
44    /// Framing and routing fields that are unambiguously forbidden in trailers are rejected.
45    /// Callers remain responsible for field-specific semantic restrictions on other trailers.
46    pub fn parse(input: &'a [u8]) -> Result<Self, Http1ParseError> {
47        let mut offset = 0;
48        let mut decoded_len = 0usize;
49        loop {
50            let line_end = chunk_line_end(input, offset)?;
51            let line = &input[offset..line_end];
52            let parsed = parse_chunk_line(line)?;
53            let after_line = line_end + 2;
54            if parsed.size == 0 {
55                let trailer_start = after_line;
56                let mut trailer_offset = trailer_start;
57                loop {
58                    let trailer_end = trailer_line_end(input, trailer_offset)?;
59                    if trailer_end == trailer_offset {
60                        let trailers =
61                            Http1Fields::parse(&input[trailer_start..trailer_offset], true)?;
62                        return Ok(Self {
63                            bytes: &input[..trailer_end + 2],
64                            chunks_end: offset,
65                            decoded_len,
66                            last_chunk_extensions: parsed.extensions,
67                            trailers,
68                        });
69                    }
70                    trailer_offset = trailer_end + 2;
71                }
72            }
73
74            decoded_len = decoded_len
75                .checked_add(parsed.size)
76                .ok_or(Http1ParseError::DecodedSizeOverflow)?;
77            let data_end = after_line
78                .checked_add(parsed.size)
79                .ok_or(Http1ParseError::ChunkSizeOverflow)?;
80            let required = data_end
81                .checked_add(2)
82                .ok_or(Http1ParseError::ChunkSizeOverflow)?;
83            if input.len() < required {
84                return Err(Http1ParseError::Incomplete {
85                    required,
86                    available: input.len(),
87                });
88            }
89            if &input[data_end..required] != b"\r\n" {
90                return Err(Http1ParseError::InvalidChunkTerminator);
91            }
92            offset = required;
93        }
94    }
95
96    /// Returns the complete represented chunked-body encoding.
97    pub const fn as_bytes(self) -> &'a [u8] {
98        self.bytes
99    }
100
101    /// Returns the sum of decoded nonzero chunk sizes.
102    pub const fn decoded_len(self) -> usize {
103        self.decoded_len
104    }
105
106    /// Returns raw final-chunk extension bytes, including the first semicolon and optional whitespace.
107    pub const fn last_chunk_extensions(self) -> &'a [u8] {
108        self.last_chunk_extensions
109    }
110
111    /// Returns ordered trailer fields that passed structural and wire-framing restrictions.
112    pub const fn trailers(self) -> Http1Fields<'a> {
113        self.trailers
114    }
115
116    /// Iterates validated nonzero chunks in wire order.
117    pub fn chunks(self) -> Http1Chunks<'a> {
118        Http1Chunks {
119            bytes: &self.bytes[..self.chunks_end],
120            offset: 0,
121        }
122    }
123}
124
125/// Iterator over validated nonzero HTTP/1 chunks.
126#[derive(Clone, Debug)]
127pub struct Http1Chunks<'a> {
128    bytes: &'a [u8],
129    offset: usize,
130}
131
132impl<'a> Iterator for Http1Chunks<'a> {
133    type Item = Http1Chunk<'a>;
134
135    fn next(&mut self) -> Option<Self::Item> {
136        if self.offset == self.bytes.len() {
137            return None;
138        }
139        let line_end = find_line_end(self.bytes, self.offset).ok()?;
140        let parsed = parse_chunk_line(&self.bytes[self.offset..line_end]).ok()?;
141        let data_start = line_end + 2;
142        let data_end = data_start + parsed.size;
143        self.offset = data_end + 2;
144        Some(Http1Chunk {
145            size: parsed.size,
146            data: &self.bytes[data_start..data_end],
147            extensions: parsed.extensions,
148        })
149    }
150}
151
152struct ParsedChunkLine<'a> {
153    size: usize,
154    extensions: &'a [u8],
155}
156
157fn parse_chunk_line(line: &[u8]) -> Result<ParsedChunkLine<'_>, Http1ParseError> {
158    let mut offset = 0;
159    let mut size = 0usize;
160    while let Some(digit) = line.get(offset).and_then(|byte| hex_value(*byte)) {
161        size = size
162            .checked_mul(16)
163            .and_then(|size| size.checked_add(usize::from(digit)))
164            .ok_or(Http1ParseError::ChunkSizeOverflow)?;
165        offset += 1;
166    }
167    if offset == 0 {
168        return Err(Http1ParseError::InvalidChunkSize);
169    }
170    let extension_start = offset;
171    validate_chunk_extensions(&line[extension_start..])?;
172    Ok(ParsedChunkLine {
173        size,
174        extensions: &line[extension_start..],
175    })
176}
177
178fn validate_chunk_extensions(bytes: &[u8]) -> Result<(), Http1ParseError> {
179    if bytes.is_empty() {
180        return Ok(());
181    }
182    let mut offset = 0;
183    skip_ows(bytes, &mut offset);
184    if offset == bytes.len() {
185        return Err(Http1ParseError::InvalidChunkSize);
186    }
187    while offset < bytes.len() {
188        if bytes[offset] != b';' {
189            return Err(Http1ParseError::InvalidChunkSize);
190        }
191        offset += 1;
192        skip_ows(bytes, &mut offset);
193        parse_token(bytes, &mut offset)?;
194        skip_ows(bytes, &mut offset);
195        if bytes.get(offset) == Some(&b'=') {
196            offset += 1;
197            skip_ows(bytes, &mut offset);
198            parse_token_or_quoted(bytes, &mut offset)?;
199            skip_ows(bytes, &mut offset);
200        }
201    }
202    Ok(())
203}
204
205fn parse_token(bytes: &[u8], offset: &mut usize) -> Result<(), Http1ParseError> {
206    let start = *offset;
207    while bytes.get(*offset).is_some_and(|byte| is_token(*byte)) {
208        *offset += 1;
209    }
210    if *offset == start {
211        return Err(Http1ParseError::InvalidChunkSize);
212    }
213    Ok(())
214}
215
216fn parse_token_or_quoted(bytes: &[u8], offset: &mut usize) -> Result<(), Http1ParseError> {
217    if bytes.get(*offset) != Some(&b'"') {
218        return parse_token(bytes, offset);
219    }
220    *offset += 1;
221    loop {
222        let byte = *bytes
223            .get(*offset)
224            .ok_or(Http1ParseError::InvalidChunkSize)?;
225        match byte {
226            b'"' => {
227                *offset += 1;
228                return Ok(());
229            }
230            b'\\' => {
231                *offset += 1;
232                let escaped = *bytes
233                    .get(*offset)
234                    .ok_or(Http1ParseError::InvalidChunkSize)?;
235                if !matches!(escaped, b'\t' | b' ' | 0x21..=0x7e | 0x80..=0xff) {
236                    return Err(Http1ParseError::InvalidChunkSize);
237                }
238                *offset += 1;
239            }
240            b'\t' | b' ' | 0x21 | 0x23..=0x5b | 0x5d..=0x7e | 0x80..=0xff => *offset += 1,
241            _ => return Err(Http1ParseError::InvalidChunkSize),
242        }
243    }
244}
245
246fn skip_ows(bytes: &[u8], offset: &mut usize) {
247    while matches!(bytes.get(*offset), Some(b' ' | b'\t')) {
248        *offset += 1;
249    }
250}
251
252fn chunk_line_end(input: &[u8], offset: usize) -> Result<usize, Http1ParseError> {
253    match find_line_end(input, offset) {
254        Ok(end) => Ok(end),
255        Err(LineEndError::Incomplete) => Err(Http1ParseError::Incomplete {
256            required: input.len().saturating_add(1),
257            available: input.len(),
258        }),
259        Err(LineEndError::Malformed) => Err(Http1ParseError::InvalidChunkSize),
260    }
261}
262
263fn trailer_line_end(input: &[u8], offset: usize) -> Result<usize, Http1ParseError> {
264    match find_line_end(input, offset) {
265        Ok(end) => Ok(end),
266        Err(LineEndError::Incomplete) => Err(Http1ParseError::Incomplete {
267            required: input.len().saturating_add(1),
268            available: input.len(),
269        }),
270        Err(LineEndError::Malformed) => Err(Http1ParseError::InvalidTrailer),
271    }
272}
273
274const fn hex_value(byte: u8) -> Option<u8> {
275    match byte {
276        b'0'..=b'9' => Some(byte - b'0'),
277        b'a'..=b'f' => Some(byte - b'a' + 10),
278        b'A'..=b'F' => Some(byte - b'A' + 10),
279        _ => None,
280    }
281}