Skip to main content

zenith_http1/
chunked.rs

1//! HTTP/1.1 Chunked 传输编码解析
2//!
3//! 严格遵循 RFC 7230 §4.1:
4//! ```text
5//! chunked-body = *chunk
6//!                last-chunk
7//!                trailer-part
8//!                CRLF
9//! chunk = chunk-size [ chunk-extension ] CRLF
10//!         chunk-data CRLF
11//! chunk-size = 1*HEXDIG
12//! ```
13//!
14//! 防护点:
15//! - chunk-size 必须为十六进制数字(禁止符号/空格)
16//! - 单个 chunk 不得超过 max_chunk_size(防止内存爆涨)
17//! - 拒绝负值/零长度陷阱(0 必须是 last-chunk)
18//! - 处理 trailer 时重置状态
19
20use crate::types::Http1Error;
21
22/// trailer 缓冲区最大大小(与 MAX_HEADER_SIZE 典型限制一致,防止恶意超长 trailer 耗尽内存)
23const MAX_TRAILER_BUF_SIZE: usize = 8192;
24
25/// Chunked 解析状态机
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum ChunkedState {
28    /// 等待下一个 chunk-size 行
29    SizeLine,
30    /// 正在读取 chunk-data
31    ChunkData,
32    /// 等待 chunk-data 结束 CRLF
33    ChunkDataEnd,
34    /// 读取 trailer(可选)
35    Trailer,
36    /// 完成
37    Done,
38}
39
40/// Chunked 解码器
41#[derive(Debug, Clone)]
42pub struct ChunkedDecoder {
43    /// 当前状态
44    pub state: ChunkedState,
45    /// 剩余 chunk 字节数
46    pub remaining: u64,
47    /// 当前 chunk 大小
48    pub current_chunk_size: u64,
49    /// 已解码总字节数
50    pub total_decoded: u64,
51    /// 最大允许字节数
52    pub max_total: u64,
53    /// 是否已遇到 0 大小块
54    pub saw_last: bool,
55    /// 临时缓冲区(size 行累积)
56    size_buf: [u8; 64],
57    size_len: usize,
58    /// 临时缓冲区(trailer 行跨 feed 累积):H-1 修复,与 size_buf 同语义,
59    /// 防止 partial trailer line 跨 feed 丢失
60    trailer_buf: Vec<u8>,
61}
62
63impl ChunkedDecoder {
64    /// 创建解码器
65    #[inline]
66    pub fn new(max_total: u64) -> Self {
67        Self {
68            state: ChunkedState::SizeLine,
69            remaining: 0,
70            current_chunk_size: 0,
71            total_decoded: 0,
72            max_total,
73            saw_last: false,
74            size_buf: [0u8; 64],
75            size_len: 0,
76            trailer_buf: Vec::new(),
77        }
78    }
79
80    /// 重置状态
81    #[inline]
82    pub fn reset(&mut self) {
83        self.state = ChunkedState::SizeLine;
84        self.remaining = 0;
85        self.current_chunk_size = 0;
86        self.total_decoded = 0;
87        self.saw_last = false;
88        self.size_len = 0;
89        self.trailer_buf.clear();
90    }
91
92    /// 是否已完成
93    #[inline]
94    pub fn is_done(&self) -> bool {
95        matches!(self.state, ChunkedState::Done)
96    }
97
98    /// 推入数据并返回产出的解码字节
99    ///
100    /// # 参数
101    /// - `input`: 输入字节流
102    ///
103    /// # 返回
104    /// - Ok((output_vec, consumed)): 解码出的字节与消耗的输入字节数
105    /// - Err(Http1Error): 解码错误
106    pub fn feed(&mut self, input: &[u8]) -> Result<(Vec<u8>, usize), Http1Error> {
107        let mut output = Vec::with_capacity(input.len());
108
109        // 若 size_buf 中有未完成的数据,先拼接进本次输入的前部
110        if self.size_len > 0 {
111            let buffered = self.size_len;
112            // checked 算术:输入长度由对端控制,溢出 fail-closed
113            let combined_len = buffered.checked_add(input.len()).ok_or_else(|| {
114                Http1Error::ChunkedError("buffered input length overflow".into())
115            })?;
116            let mut combined = Vec::with_capacity(combined_len);
117            combined.extend_from_slice(&self.size_buf[..buffered]);
118            combined.extend_from_slice(input);
119            self.size_len = 0;
120            let consumed = self.feed_combined(&combined, &mut output)?;
121            // 只计本次输入消耗的字节数
122            let own_consumed = consumed.saturating_sub(buffered);
123            return Ok((output, own_consumed));
124        }
125
126        // H-1: 若 trailer_buf 中有未完成的 trailer 数据,先拼接进本次输入的前部
127        if !self.trailer_buf.is_empty() {
128            let buffered = self.trailer_buf.len();
129            let combined_len = buffered.checked_add(input.len()).ok_or_else(|| {
130                Http1Error::ChunkedError("buffered trailer input length overflow".into())
131            })?;
132            let mut combined = Vec::with_capacity(combined_len);
133            combined.extend_from_slice(&self.trailer_buf);
134            combined.extend_from_slice(input);
135            self.trailer_buf.clear();
136            let consumed = self.feed_combined(&combined, &mut output)?;
137            // 只计本次输入消耗的字节数
138            let own_consumed = consumed.saturating_sub(buffered);
139            return Ok((output, own_consumed));
140        }
141
142        let consumed = self.feed_combined(input, &mut output)?;
143        Ok((output, consumed))
144    }
145
146    /// 内部解析(已合并 size_buf)
147    fn feed_combined(
148        &mut self,
149        input: &[u8],
150        output: &mut Vec<u8>,
151    ) -> Result<usize, Http1Error> {
152        let mut consumed = 0;
153
154        while consumed < input.len() && !matches!(self.state, ChunkedState::Done) {
155            match self.state {
156                ChunkedState::SizeLine => {
157                    let rest = &input[consumed..];
158                    if let Some(crlf) = find_crlf(rest) {
159                        let line = &rest[..crlf];
160                        self.parse_size_line(line)?;
161                        consumed += crlf + 2;
162                    } else {
163                        let available = rest.len().min(self.size_buf.len() - self.size_len);
164                        if available > 0 {
165                            self.size_buf[self.size_len..self.size_len + available]
166                                .copy_from_slice(&rest[..available]);
167                            self.size_len += available;
168                            consumed += available;
169                        }
170                        if self.size_len >= self.size_buf.len() {
171                            return Err(Http1Error::ChunkedError(
172                                "chunk-size line too long".into(),
173                            ));
174                        }
175                        break;
176                    }
177                }
178                ChunkedState::ChunkData => {
179                    let take = (input.len() - consumed) as u64;
180                    let take = take.min(self.remaining);
181                    if take == 0 {
182                        // 需要更多数据
183                        break;
184                    }
185                    output.extend_from_slice(
186                        &input[consumed..consumed + take as usize],
187                    );
188                    consumed += take as usize;
189                    self.remaining -= take;
190                    // checked 算术:累计解码字节数溢出按超限处理(fail-closed)
191                    self.total_decoded = self.total_decoded.checked_add(take).ok_or(Http1Error::BodyTooLarge)?;
192                    if self.total_decoded > self.max_total {
193                        return Err(Http1Error::BodyTooLarge);
194                    }
195                    if self.remaining == 0 {
196                        self.state = ChunkedState::ChunkDataEnd;
197                    }
198                }
199                ChunkedState::ChunkDataEnd => {
200                    let rest = &input[consumed..];
201                    if rest.len() < 2 {
202                        break;
203                    }
204                    if rest[0] == b'\r' && rest[1] == b'\n' {
205                        consumed += 2;
206                        self.state = ChunkedState::SizeLine;
207                    } else {
208                        return Err(Http1Error::ChunkedError(
209                            "missing CRLF after chunk-data".into(),
210                        ));
211                    }
212                }
213                ChunkedState::Trailer => {
214                    let rest = &input[consumed..];
215                    // 数据不足暂不消费:空 rest 等待下次喂入补齐
216                    if rest.is_empty() {
217                        break;
218                    }
219                    // 跳过 trailer 行直到遇到空行 (CRLF at pos 0)
220                    if let Some(crlf) = find_crlf(rest) {
221                        consumed += crlf + 2;
222                        if crlf == 0 {
223                            self.state = ChunkedState::Done;
224                        }
225                    } else {
226                        // H-1: 未找到完整 CRLF,缓冲 partial trailer 行
227                        // (含单独 \r 的半个 CRLF),下次 feed() 拼接后继续扫描,
228                        // 禁止丢失跨 feed 的 partial bytes
229                        self.trailer_buf.extend_from_slice(rest);
230                        // 限制 trailer 缓冲区大小,防止恶意超长 trailer 耗尽内存
231                        if self.trailer_buf.len() > MAX_TRAILER_BUF_SIZE {
232                            return Err(Http1Error::ChunkedError(
233                                "trailer buffer exceeds maximum size".into(),
234                            ));
235                        }
236                        consumed += rest.len();
237                        break;
238                    }
239                }
240                // 循环条件已排除 Done;防御性 fail-closed,禁止 unreachable!() panic
241                ChunkedState::Done => {
242                    return Err(Http1Error::ChunkedError(
243                        "chunked decoder in terminal state".into(),
244                    ));
245                }
246            }
247        }
248
249        // 注意:输入耗尽停在 Trailer 不视为完成——终止空行(CRLF)必须真实到达,
250        // 否则截断的 chunked body 会被误判为完整(fail-closed)
251
252        Ok(consumed)
253    }
254
255    /// 解析 chunk-size 行
256    ///
257    /// 委托 [`crate::parser::parse_chunk_size`](全 crate 唯一实现:分号
258    /// chunk-ext 剥离、trim、显式 hexdigit 校验、溢出 fail-closed),
259    /// 与服务端 feed 状态机语义严格一致,消除双解析器分歧。
260    fn parse_size_line(&mut self, line: &[u8]) -> Result<(), Http1Error> {
261        let parsed = crate::parser::parse_chunk_size(line)?;
262        // usize → u64 显式形式化(32-bit 平台理论截断面 fail-closed;64-bit 无损)
263        let val = u64::try_from(parsed).map_err(|_| Http1Error::BodyTooLarge)?;
264
265        self.current_chunk_size = val;
266        if val == 0 {
267            self.saw_last = true;
268            // RFC 7230 §4.1:last-chunk 的 CRLF 之后直接进入 trailer-part
269            // (空 trailer 时下一个 CRLF 即终止空行;非空 trailer 逐行忽略)
270            self.state = ChunkedState::Trailer;
271        } else {
272            if val > self.max_total {
273                return Err(Http1Error::BodyTooLarge);
274            }
275            // checked 算术:total_decoded + val 溢出按超限处理(fail-closed)
276            if self
277                .total_decoded
278                .checked_add(val)
279                .map(|sum| sum > self.max_total)
280                .unwrap_or(true)
281            {
282                return Err(Http1Error::BodyTooLarge);
283            }
284            self.remaining = val;
285            self.state = ChunkedState::ChunkData;
286        }
287        Ok(())
288    }
289}
290
291/// 查找 CRLF 位置
292#[inline]
293fn find_crlf(buf: &[u8]) -> Option<usize> {
294    let mut i = 0;
295    while i + 1 < buf.len() {
296        if buf[i] == b'\r' && buf[i + 1] == b'\n' {
297            return Some(i);
298        }
299        i += 1;
300    }
301    None
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_simple_chunked() {
310        let mut dec = ChunkedDecoder::new(1024);
311        let input = b"5\r\nhello\r\n0\r\n\r\n";
312        let (out, consumed) = dec.feed(input).unwrap();
313        assert_eq!(out, b"hello");
314        assert_eq!(consumed, input.len());
315        assert!(dec.is_done());
316    }
317
318    #[test]
319    fn test_multiple_chunks() {
320        let mut dec = ChunkedDecoder::new(1024);
321        let input = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
322        let (out, consumed) = dec.feed(input).unwrap();
323        assert_eq!(out, b"hello world");
324        assert_eq!(consumed, input.len());
325        assert!(dec.is_done());
326    }
327
328    #[test]
329    fn test_incremental() {
330        let mut dec = ChunkedDecoder::new(1024);
331        let input = b"5\r\nhello\r\n0\r\n\r\n";
332        let (out1, c1) = dec.feed(&input[..6]).unwrap();
333        assert_eq!(c1, 6, "first feed should consume 6 bytes");
334        let (out2, _c2) = dec.feed(&input[c1..]).unwrap();
335        let mut all = out1;
336        all.extend_from_slice(&out2);
337        assert_eq!(all, b"hello");
338        assert!(dec.is_done());
339    }
340
341    #[test]
342    fn test_chunk_extension() {
343        let mut dec = ChunkedDecoder::new(1024);
344        let input = b"5;foo=bar\r\nhello\r\n0\r\n\r\n";
345        let (out, _) = dec.feed(input).unwrap();
346        assert_eq!(out, b"hello");
347        assert!(dec.is_done());
348    }
349
350    #[test]
351    fn test_invalid_hex() {
352        let mut dec = ChunkedDecoder::new(1024);
353        let input = b"ZZ\r\nx\r\n0\r\n\r\n";
354        let r = dec.feed(input);
355        assert!(r.is_err());
356    }
357
358    #[test]
359    fn test_too_large() {
360        let mut dec = ChunkedDecoder::new(2);
361        let input = b"5\r\nhello\r\n0\r\n\r\n";
362        let r = dec.feed(input);
363        assert!(r.is_err());
364    }
365
366    #[test]
367    fn test_missing_crlf() {
368        let mut dec = ChunkedDecoder::new(1024);
369        let input = b"5\r\nhello\rBAD";
370        let r = dec.feed(input);
371        assert!(r.is_err());
372    }
373
374    #[test]
375    fn test_empty_chunk_zero_size() {
376        let mut dec = ChunkedDecoder::new(1024);
377        let input = b"0\r\n\r\n";
378        let (out, consumed) = dec.feed(input).unwrap();
379        assert!(out.is_empty());
380        assert_eq!(consumed, input.len());
381        assert!(dec.is_done());
382        assert!(dec.saw_last);
383    }
384
385    #[test]
386    fn test_single_byte_chunk() {
387        let mut dec = ChunkedDecoder::new(1024);
388        let input = b"1\r\nA\r\n0\r\n\r\n";
389        let (out, _) = dec.feed(input).unwrap();
390        assert_eq!(out, b"A");
391        assert!(dec.is_done());
392    }
393
394    #[test]
395    fn test_large_hex_chunk_size() {
396        let mut dec = ChunkedDecoder::new(1024 * 1024);
397        let size = 0xFF;
398        let mut input = Vec::new();
399        input.extend_from_slice(format!("{:X}\r\n", size).as_bytes());
400        input.extend_from_slice(&vec![b'x'; size]);
401        input.extend_from_slice(b"\r\n0\r\n\r\n");
402        let (out, _) = dec.feed(&input).unwrap();
403        assert_eq!(out.len(), size);
404        assert!(dec.is_done());
405    }
406
407    #[test]
408    fn test_chunk_size_with_trailing_spaces() {
409        let mut dec = ChunkedDecoder::new(1024);
410        let input = b"5   \r\nhello\r\n0\r\n\r\n";
411        let (out, _) = dec.feed(input).unwrap();
412        assert_eq!(out, b"hello");
413        assert!(dec.is_done());
414    }
415
416    #[test]
417    fn test_chunk_size_with_tabs() {
418        let mut dec = ChunkedDecoder::new(1024);
419        let input = b"5\t\r\nhello\r\n0\r\n\r\n";
420        let (out, _) = dec.feed(input).unwrap();
421        assert_eq!(out, b"hello");
422        assert!(dec.is_done());
423    }
424
425    #[test]
426    fn test_empty_chunk_size_line() {
427        let mut dec = ChunkedDecoder::new(1024);
428        let input = b"\r\nhello\r\n0\r\n\r\n";
429        let r = dec.feed(input);
430        assert!(r.is_err());
431    }
432
433    #[test]
434    fn test_chunk_size_line_too_long() {
435        let mut dec = ChunkedDecoder::new(1024);
436        let mut long_line = vec![b'A'; 100];
437        long_line.extend_from_slice(b"\r\n");
438        let r = dec.feed(&long_line);
439        assert!(r.is_err());
440    }
441
442    #[test]
443    fn test_trailer_state_received() {
444        let mut dec = ChunkedDecoder::new(1024);
445        let input = b"5\r\nhello\r\n0\r\n\r\n";
446        let (out, _) = dec.feed(input).unwrap();
447        assert_eq!(out, b"hello");
448        assert!(dec.is_done());
449    }
450
451    #[test]
452    fn test_trailer_empty_immediate_done() {
453        let mut dec = ChunkedDecoder::new(1024);
454        let input = b"5\r\nhello\r\n0\r\n";
455        dec.feed(input).unwrap();
456        let _ = dec.feed(b"\r\n");
457        assert!(dec.is_done());
458    }
459
460    #[test]
461    fn test_reset_decoder() {
462        let mut dec = ChunkedDecoder::new(1024);
463        let input = b"5\r\nhello\r\n0\r\n\r\n";
464        dec.feed(input).unwrap();
465        assert!(dec.is_done());
466        dec.reset();
467        assert_eq!(dec.state, ChunkedState::SizeLine);
468        assert_eq!(dec.total_decoded, 0);
469        assert!(!dec.saw_last);
470        assert_eq!(dec.current_chunk_size, 0);
471    }
472
473    #[test]
474    fn test_chunked_state_variants() {
475        let states = [
476            ChunkedState::SizeLine,
477            ChunkedState::ChunkData,
478            ChunkedState::ChunkDataEnd,
479            ChunkedState::Trailer,
480            ChunkedState::Done,
481        ];
482        for (i, s) in states.iter().enumerate() {
483            assert_eq!(*s, states[i]);
484        }
485        assert_ne!(ChunkedState::SizeLine, ChunkedState::Done);
486    }
487
488    #[test]
489    fn test_decoder_clone() {
490        let dec = ChunkedDecoder::new(1024);
491        let dec2 = dec.clone();
492        assert_eq!(dec.state, dec2.state);
493        assert_eq!(dec.max_total, dec2.max_total);
494    }
495
496    #[test]
497    fn test_total_decoded_tracking() {
498        let mut dec = ChunkedDecoder::new(1024);
499        let input = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
500        let (_, _) = dec.feed(input).unwrap();
501        assert_eq!(dec.total_decoded, 11);
502    }
503
504    #[test]
505    fn test_uppercase_hex() {
506        let mut dec = ChunkedDecoder::new(1024);
507        let input = b"A\r\n0123456789\r\n0\r\n\r\n";
508        let (out, _) = dec.feed(input).unwrap();
509        assert_eq!(out.len(), 10);
510        assert!(dec.is_done());
511    }
512
513    #[test]
514    fn test_lowercase_hex() {
515        let mut dec = ChunkedDecoder::new(1024);
516        let input = b"a\r\n0123456789\r\n0\r\n\r\n";
517        let (out, _) = dec.feed(input).unwrap();
518        assert_eq!(out.len(), 10);
519        assert!(dec.is_done());
520    }
521
522    #[test]
523    fn test_negative_chunk_size_rejected() {
524        let mut dec = ChunkedDecoder::new(1024);
525        let input = b"-5\r\nhello\r\n0\r\n\r\n";
526        let r = dec.feed(input);
527        assert!(r.is_err());
528    }
529
530    #[test]
531    fn test_body_too_large_accumulated() {
532        let mut dec = ChunkedDecoder::new(10);
533        let input = b"6\r\nhello \r\n6\r\nworld!\r\n0\r\n\r\n";
534        let r = dec.feed(input);
535        assert!(r.is_err());
536    }
537
538    #[test]
539    fn test_total_decoded_overflow_fail_closed() {
540        // 恶意输入:第二个 chunk-size 接近 u64::MAX,
541        // total_decoded + val 溢出必须 fail-closed 返回 BodyTooLarge 而非回绕
542        let mut dec = ChunkedDecoder::new(u64::MAX);
543        // 0xFFFFFFFFFFFFFFFC = u64::MAX - 3;total_decoded(5) + val 溢出 u64
544        let input = b"5\r\nhello\r\nFFFFFFFFFFFFFFFC\r\nx\r\n0\r\n\r\n";
545        let r = dec.feed(input);
546        assert!(
547            matches!(r, Err(Http1Error::BodyTooLarge)),
548            "total_decoded 溢出必须返回 BodyTooLarge,实际 {r:?}"
549        );
550    }
551
552    #[test]
553    fn test_incremental_small_chunks() {
554        let mut dec = ChunkedDecoder::new(1024);
555        let input = b"5\r\nhello\r\n0\r\n\r\n";
556        let mut output = Vec::new();
557        let mut pos = 0;
558        while pos < input.len() {
559            let end = (pos + 2).min(input.len());
560            let (out, consumed) = dec.feed(&input[pos..end]).unwrap();
561            output.extend_from_slice(&out);
562            pos += consumed;
563            if consumed == 0 {
564                pos += 1;
565            }
566        }
567        assert_eq!(output, b"hello");
568        assert!(dec.is_done());
569    }
570}