Skip to main content

s3s_multipart/
part.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023-2026 The s3s Authors
3
4//! A single part view borrowed from the parser.
5
6use std::fmt;
7use std::future::poll_fn;
8use std::task::{Context, Poll, ready};
9
10use bytes::Bytes;
11use futures_core::Stream;
12
13use crate::Error;
14use crate::multipart::Multipart;
15use crate::part_data_stream::PartDataStream;
16
17/// A single multipart part yielded by [`Multipart`].
18///
19/// The part borrows the parser mutably. The returned headers and data borrow
20/// the parser's internal buffer, so callers process each item before asking
21/// for the next one.
22pub struct Part<'m, S>
23where
24    S: Stream<Item = Result<Bytes, Error>> + Send + Unpin,
25{
26    mp: &'m mut Multipart<S>,
27}
28
29impl<S> fmt::Debug for Part<'_, S>
30where
31    S: Stream<Item = Result<Bytes, Error>> + Send + Unpin,
32{
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.debug_struct("Part").finish_non_exhaustive()
35    }
36}
37
38impl<'m, S> Part<'m, S>
39where
40    S: Stream<Item = Result<Bytes, Error>> + Send + Unpin,
41{
42    pub(super) fn new(mp: &'m mut Multipart<S>) -> Self {
43        Part { mp }
44    }
45
46    /// Poll-based variant of [`Part::next_header`].
47    ///
48    /// The two paths repeat the same `poll_ensure_headers` handshake by hand;
49    /// see the note in [`Part::next_header`] for why they cannot share it.
50    pub fn poll_next_header(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<httparse::Header<'_>>, Error>> {
51        ready!(self.mp.poll_ensure_headers(cx)?);
52        Poll::Ready(self.mp.next_header_inner())
53    }
54
55    /// Poll-based variant of [`Part::next_data`].
56    pub fn poll_next_data(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<Bytes>, Error>> {
57        ready!(self.mp.poll_ensure_headers(cx)?);
58        self.mp.finish_headers();
59        self.mp.poll_next_part_data_chunk(cx)
60    }
61
62    /// Returns the next header of this part.
63    ///
64    /// Headers are yielded one at a time and borrow the parser's internal
65    /// buffer for the duration of the call. `None` marks the end of the
66    /// header block; the part is then positioned at the start of its data.
67    ///
68    /// Parts carrying more than three header fields are accepted; fields beyond
69    /// the first three are ignored, as RFC 7578 section 4.8 asks of a receiver.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error when the header block is malformed, exceeds the
74    /// configured buffer limit, or the underlying stream fails.
75    pub async fn next_header(&mut self) -> Result<Option<httparse::Header<'_>>, Error> {
76        // Deliberately not `poll_fn(|cx| self.poll_next_header(cx)).await`: the
77        // returned header borrows the parser buffer, and such a reference cannot
78        // escape the `FnMut` closure capture ("captured variable cannot escape
79        // `FnMut` closure body"). `next_data` can delegate to `poll_next_data`
80        // because it hands out owned `Bytes`. Keep both handshakes in sync.
81        poll_fn(|cx| self.mp.poll_ensure_headers(cx)).await?;
82        self.mp.next_header_inner()
83    }
84
85    /// Returns the next data chunk of this part.
86    ///
87    /// Entering the data phase discards headers that have not been yielded yet:
88    /// the header block is parsed if needed and the parser then moves past it.
89    /// Data chunks are passed through from the underlying stream whenever
90    /// possible. Every yielded chunk is non-empty: when the delimiter is
91    /// already at the front there is no data to hand out, and `None` marks the
92    /// end of this part's data right away. The parser has then consumed the
93    /// delimiter and is positioned before the next part.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error when the stream ends without a delimiter, the data
98    /// is malformed, or the underlying stream fails.
99    pub async fn next_data(&mut self) -> Result<Option<Bytes>, Error> {
100        poll_fn(|cx| self.poll_next_data(cx)).await
101    }
102
103    /// Takes the remaining multipart stream, returning a self-contained
104    /// [`PartDataStream`].
105    ///
106    /// The header block must already have been parsed, which the first
107    /// completed [`Part::next_header`] call guarantees (a part without headers
108    /// returns `None` from it). Headers that have not been yielded yet are
109    /// discarded, exactly as when the data phase is entered through
110    /// [`Part::next_data`]. The returned stream yields this part's data
111    /// and ends at the closing delimiter `\r\n--boundary`; it does not
112    /// interpret what follows. Convert it with
113    /// [`PartDataStream::into_final`] to validate the strict closing
114    /// trailer (the `--boundary--\r\n` form followed by end of stream).
115    ///
116    /// # Errors
117    ///
118    /// Returns [`Error::InvalidFormat`] when the header block has not been
119    /// parsed at all.
120    pub fn take_data_stream(self) -> Result<PartDataStream<S>, Error> {
121        self.mp.take_data_stream()
122    }
123}
124
125#[cfg(test)]
126#[allow(
127    clippy::expect_used,
128    clippy::indexing_slicing,
129    clippy::panic,
130    clippy::unreachable,
131    clippy::unwrap_used
132)]
133mod tests {
134    use super::*;
135
136    use std::collections::VecDeque;
137    use std::pin::Pin;
138
139    use futures::executor::block_on;
140    use futures::task::noop_waker;
141    use futures_util::StreamExt;
142
143    use crate::Boundary;
144
145    const BOUNDARY: &[u8] = b"B";
146    const DATA: &[u8] = b"hello file data";
147
148    /// Two headers, then the part data, then the closing delimiter.
149    fn body() -> Vec<u8> {
150        let mut body = Vec::new();
151        body.extend_from_slice(b"--B\r\nContent-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\n");
152        body.extend_from_slice(b"Content-Type: text/plain\r\n\r\n");
153        body.extend_from_slice(DATA);
154        body.extend_from_slice(b"\r\n--B--\r\n");
155        body
156    }
157
158    /// Byte offset at which the header block ends (start of the part data).
159    fn data_start(body: &[u8]) -> usize {
160        body.windows(4).position(|w| w == b"\r\n\r\n").map_or(0, |pos| pos + 4)
161    }
162
163    fn stream_error() -> Error {
164        Error::stream_read_failed(std::io::Error::other("boom"))
165    }
166
167    fn error_kind(err: &Error) -> &'static str {
168        if matches!(err, Error::StreamReadFailed(_)) {
169            "StreamReadFailed"
170        } else if matches!(err, Error::InvalidFormat) {
171            "InvalidFormat"
172        } else if matches!(err, Error::IncompleteStream) {
173            "IncompleteStream"
174        } else if matches!(err, Error::HeaderSizeExceeded { .. }) {
175            "HeaderSizeExceeded"
176        } else {
177            "other"
178        }
179    }
180
181    /// Yields scripted items, going `Pending` once right before the item that
182    /// starts at byte offset `pending_at` (waking immediately). Byte offsets,
183    /// unlike item indices, place the `Pending` in a chosen parsing phase
184    /// regardless of chunking.
185    struct Scripted {
186        items: VecDeque<Result<Bytes, Error>>,
187        pending_at: Option<usize>,
188        offset: usize,
189        pending_done: bool,
190    }
191
192    impl Scripted {
193        fn new(items: Vec<Result<Bytes, Error>>, pending_at: Option<usize>) -> Self {
194            Scripted {
195                items: items.into(),
196                pending_at,
197                offset: 0,
198                pending_done: false,
199            }
200        }
201
202        fn chunked(body: &[u8], chunk: usize, pending_at: Option<usize>) -> Self {
203            Scripted::new(body.chunks(chunk.max(1)).map(|c| Ok(Bytes::copy_from_slice(c))).collect(), pending_at)
204        }
205    }
206
207    impl Stream for Scripted {
208        type Item = Result<Bytes, Error>;
209
210        fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
211            if !self.pending_done && self.pending_at.is_some_and(|at| self.offset >= at) {
212                self.pending_done = true;
213                cx.waker().wake_by_ref();
214                return Poll::Pending;
215            }
216            let item = self.items.pop_front();
217            if let Some(Ok(chunk)) = &item {
218                self.offset = self.offset.saturating_add(chunk.len());
219            }
220            Poll::Ready(item)
221        }
222    }
223
224    fn parser(stream: Scripted) -> Multipart<Scripted> {
225        Multipart::new(stream, &Boundary::new(BOUNDARY).unwrap(), 4096)
226    }
227
228    fn drain_headers_async<S>(part: &mut Part<'_, S>) -> Vec<(String, Vec<u8>)>
229    where
230        S: Stream<Item = Result<Bytes, Error>> + Send + Unpin,
231    {
232        block_on(async {
233            let mut headers = Vec::new();
234            while let Some(header) = part.next_header().await.unwrap() {
235                headers.push((header.name.to_string(), header.value.to_vec()));
236            }
237            headers
238        })
239    }
240
241    fn drain_headers_poll<S>(part: &mut Part<'_, S>, cx: &mut Context<'_>) -> (Vec<(String, Vec<u8>)>, usize)
242    where
243        S: Stream<Item = Result<Bytes, Error>> + Send + Unpin,
244    {
245        let mut headers = Vec::new();
246        let mut pending = 0;
247        loop {
248            match part.poll_next_header(cx) {
249                Poll::Pending => pending += 1,
250                Poll::Ready(Ok(Some(header))) => headers.push((header.name.to_string(), header.value.to_vec())),
251                Poll::Ready(Ok(None)) => break,
252                Poll::Ready(Err(err)) => panic!("unexpected error: {err}"),
253            }
254        }
255        (headers, pending)
256    }
257
258    /// F27: the poll path must yield exactly what the async path yields, must
259    /// resume across `Pending`, and must stay exhausted once the block is done.
260    #[test]
261    fn poll_next_header_matches_the_async_path() {
262        let body = body();
263        let data_start = data_start(&body);
264        for (chunk, pending_at) in [(body.len(), None), (1, None), (7, Some(20)), (1, Some(data_start + 1))] {
265            let mut mp = parser(Scripted::chunked(&body, chunk, pending_at));
266            let mut part = block_on(mp.next_part()).unwrap().unwrap();
267            let expected = drain_headers_async(&mut part);
268            assert_eq!(expected.len(), 2, "chunk={chunk}");
269
270            let mut mp = parser(Scripted::chunked(&body, chunk, pending_at));
271            let mut part = block_on(mp.next_part()).unwrap().unwrap();
272            let waker = noop_waker();
273            let mut cx = Context::from_waker(&waker);
274            let (headers, pending) = drain_headers_poll(&mut part, &mut cx);
275            assert_eq!(headers, expected, "chunk={chunk}");
276            assert_eq!(headers[0].0, "Content-Disposition", "chunk={chunk}");
277            assert_eq!(headers[1].0, "Content-Type", "chunk={chunk}");
278
279            // A `Pending` is expected only when it falls inside the header block:
280            // `next_part` already consumed one that lands earlier in the stream.
281            let expect_pending = pending_at.is_some_and(|at| at < data_start);
282            assert_eq!(pending > 0, expect_pending, "chunk={chunk}");
283
284            // An exhausted block stays exhausted instead of restarting or erroring.
285            for _ in 0..2 {
286                assert!(matches!(part.poll_next_header(&mut cx), Poll::Ready(Ok(None))), "chunk={chunk}");
287            }
288        }
289    }
290
291    /// F27: both header paths must classify malformed, failing and truncated
292    /// streams identically.
293    #[test]
294    fn poll_next_header_reports_the_same_errors_as_the_async_path() {
295        type Items = fn() -> Vec<Result<Bytes, Error>>;
296        let cases: [(&str, &str, Items); 3] = [
297            ("malformed header line", "InvalidFormat", || {
298                vec![Ok(Bytes::from_static(b"--B\r\nBad Header Line\r\n\r\nx\r\n--B--\r\n"))]
299            }),
300            ("stream error inside the header block", "StreamReadFailed", || {
301                vec![Ok(Bytes::from_static(b"--B\r\nX: y\r\n")), Err(stream_error())]
302            }),
303            ("stream ends inside the header block", "IncompleteStream", || {
304                vec![Ok(Bytes::from_static(b"--B\r\nX: y\r\n"))]
305            }),
306        ];
307
308        for (label, expected, items) in cases {
309            let mut mp = parser(Scripted::new(items(), None));
310            let mut part = block_on(mp.next_part()).unwrap().unwrap();
311            let async_kind = block_on(async {
312                loop {
313                    match part.next_header().await {
314                        Ok(Some(_)) => {}
315                        Ok(None) => break "none",
316                        Err(err) => break error_kind(&err),
317                    }
318                }
319            });
320            assert_eq!(async_kind, expected, "{label}");
321
322            let mut mp = parser(Scripted::new(items(), None));
323            let mut part = block_on(mp.next_part()).unwrap().unwrap();
324            let waker = noop_waker();
325            let mut cx = Context::from_waker(&waker);
326            let poll_kind = loop {
327                match part.poll_next_header(&mut cx) {
328                    Poll::Pending | Poll::Ready(Ok(Some(_))) => {}
329                    Poll::Ready(Ok(None)) => break "none",
330                    Poll::Ready(Err(err)) => break error_kind(&err),
331                }
332            };
333            assert_eq!(poll_kind, expected, "{label}");
334        }
335    }
336
337    /// F28: the data phase may suspend before it hands out a chunk.
338    #[test]
339    fn poll_next_data_reports_pending_before_the_next_chunk() {
340        let body = body();
341        let mut mp = parser(Scripted::chunked(&body, 1, Some(data_start(&body) + 1)));
342        let mut part = block_on(mp.next_part()).unwrap().unwrap();
343        let waker = noop_waker();
344        let mut cx = Context::from_waker(&waker);
345        let (headers, _) = drain_headers_poll(&mut part, &mut cx);
346        assert_eq!(headers.len(), 2);
347
348        let mut data = Vec::new();
349        let mut pending = 0;
350        loop {
351            match part.poll_next_data(&mut cx) {
352                Poll::Pending => pending += 1,
353                Poll::Ready(Ok(Some(chunk))) => data.extend_from_slice(&chunk),
354                Poll::Ready(Ok(None)) => break,
355                Poll::Ready(Err(err)) => panic!("unexpected error: {err}"),
356            }
357        }
358        assert!(pending > 0, "expected a Pending during the data phase");
359        assert_eq!(data, DATA);
360    }
361
362    /// F28: a failing underlying stream surfaces from the data phase.
363    #[test]
364    fn poll_next_data_reports_a_stream_error() {
365        let items = vec![Ok(Bytes::from_static(b"--B\r\nX: y\r\n\r\nhel")), Err(stream_error())];
366        let mut mp = parser(Scripted::new(items, None));
367        let mut part = block_on(mp.next_part()).unwrap().unwrap();
368        let waker = noop_waker();
369        let mut cx = Context::from_waker(&waker);
370        let (headers, _) = drain_headers_poll(&mut part, &mut cx);
371        assert_eq!(headers.len(), 1);
372
373        let kind = loop {
374            match part.poll_next_data(&mut cx) {
375                Poll::Pending | Poll::Ready(Ok(Some(_))) => {}
376                Poll::Ready(Ok(None)) => break "none",
377                Poll::Ready(Err(err)) => break error_kind(&err),
378            }
379        };
380        assert_eq!(kind, "StreamReadFailed");
381    }
382
383    /// F28: a malformed header block surfaces from `poll_next_data` as well,
384    /// because that call parses the block itself when no header was read.
385    #[test]
386    fn poll_next_data_reports_a_header_block_error() {
387        let items = vec![Ok(Bytes::from_static(b"--B\r\nBad Header Line\r\n\r\nx\r\n--B--\r\n"))];
388        let mut mp = parser(Scripted::new(items, None));
389        let mut part = block_on(mp.next_part()).unwrap().unwrap();
390        let waker = noop_waker();
391        let mut cx = Context::from_waker(&waker);
392        match part.poll_next_data(&mut cx) {
393            Poll::Ready(Err(err)) => assert_eq!(error_kind(&err), "InvalidFormat"),
394            other => panic!("expected InvalidFormat, got {other:?}"),
395        }
396    }
397
398    /// F28: the first `poll_next_data` may suspend while it is still reading the
399    /// header block, i.e. before any data chunk exists.
400    #[test]
401    fn poll_next_data_reports_pending_while_the_header_block_is_read() {
402        let body = body();
403        let mut mp = parser(Scripted::chunked(&body, 7, Some(20)));
404        let mut part = block_on(mp.next_part()).unwrap().unwrap();
405        let waker = noop_waker();
406        let mut cx = Context::from_waker(&waker);
407        assert!(matches!(part.poll_next_data(&mut cx), Poll::Pending));
408
409        let mut data = Vec::new();
410        loop {
411            match part.poll_next_data(&mut cx) {
412                Poll::Pending => {}
413                Poll::Ready(Ok(Some(chunk))) => data.extend_from_slice(&chunk),
414                Poll::Ready(Ok(None)) => break,
415                Poll::Ready(Err(err)) => panic!("unexpected error: {err}"),
416            }
417        }
418        assert_eq!(data, DATA);
419    }
420
421    /// F29: entering the data phase discards headers that were never yielded.
422    /// Taking the stream after the first header therefore succeeds and still
423    /// delivers the whole part body.
424    #[test]
425    fn take_data_stream_after_a_partial_header_read_discards_the_rest() {
426        let body = body();
427        let mut mp = parser(Scripted::chunked(&body, body.len(), None));
428        let mut part = block_on(mp.next_part()).unwrap().unwrap();
429        let first = block_on(part.next_header()).unwrap().unwrap();
430        assert_eq!(first.name, "Content-Disposition");
431
432        let mut stream = part.take_data_stream().unwrap();
433        let data = block_on(async {
434            let mut data = Vec::new();
435            while let Some(chunk) = stream.next().await {
436                data.extend_from_slice(&chunk.unwrap());
437            }
438            data
439        });
440        assert_eq!(data, DATA);
441
442        // The unread `Content-Type` header is gone, the trailer is still strict.
443        let mut trailer = stream.into_final();
444        assert!(block_on(trailer.next()).is_none());
445    }
446
447    /// F29: `next_data` discards unread headers as well, so a caller that only
448    /// wants the body does not have to drain them first.
449    #[test]
450    fn next_data_without_reading_headers_yields_the_part_data() {
451        let body = body();
452        let mut mp = parser(Scripted::chunked(&body, body.len(), None));
453        let mut part = block_on(mp.next_part()).unwrap().unwrap();
454        let data = block_on(async {
455            let mut data = Vec::new();
456            while let Some(chunk) = part.next_data().await.unwrap() {
457                data.extend_from_slice(&chunk);
458            }
459            data
460        });
461        assert_eq!(data, DATA);
462    }
463}