Skip to main content

churust_core/
multipart.rs

1//! `multipart/form-data` bodies (feature `multipart`) — file uploads.
2//!
3//! ```
4//! use churust_core::{Churust, Multipart, TestClient};
5//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
6//! let app = Churust::server()
7//!     .routing(|r| {
8//!         r.post("/upload", |form: Multipart| async move {
9//!             let f = form.file("doc").expect("a file part named doc");
10//!             format!("{} ({} bytes)", f.filename.clone().unwrap_or_default(), f.bytes.len())
11//!         });
12//!     })
13//!     .build();
14//!
15//! let body = "--X\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.txt\"\r\n\r\nhello\r\n--X--\r\n";
16//! let res = TestClient::new(app)
17//!     .post("/upload")
18//!     .header("content-type", "multipart/form-data; boundary=X")
19//!     .body(body)
20//!     .send()
21//!     .await;
22//! assert_eq!(res.text(), "a.txt (5 bytes)");
23//! # });
24//! ```
25//!
26//! # Two parsers, and which one to reach for
27//!
28//! [`Multipart`] parses the already-buffered request body, so an upload through
29//! it is capped by the server-wide
30//! [`max_body_bytes`](crate::AppBuilder::max_body_bytes) and by any per-route
31//! [`max_body_bytes`](crate::RouteBuilder::max_body_bytes). Everything lands in
32//! memory at once. That is the right answer for form fields and small
33//! attachments, and it stays the default because it cannot surprise anyone.
34//!
35//! [`MultipartStream`] parses incrementally: fields arrive one at a time and
36//! each field's content is read in chunks, so an upload can be written to disk
37//! or forwarded onward while it is still arriving. Only one chunk plus the
38//! boundary is held at any moment.
39//!
40//! What that changes, precisely: **memory stops scaling with upload size.** The
41//! buffered parser holds the whole body, so an upload route sized for 2 GiB
42//! costs 2 GiB per concurrent upload; the streaming one costs a chunk. What it
43//! does **not** change is the ceiling itself. The engine wraps every request
44//! body in a limit of
45//! [`max_body_bytes`](crate::AppBuilder::max_body_bytes), so an upload larger
46//! than that is still refused with `413` here exactly as it is through
47//! [`Payload`](crate::Payload). Raise it deliberately for the upload route; the
48//! difference is that raising it is now affordable.
49//!
50//! Everything else stays bounded: the per-route body cap applies to the stream,
51//! part headers are capped, the part count is capped, and [`Field::bytes`]
52//! refuses to collect more than
53//! [`max_field_bytes`](MultipartStream::max_field_bytes). What the parser will
54//! not do is pick a total for you, because a handler that streams to disk has a
55//! different ceiling from one that does not.
56
57use crate::call::Call;
58use crate::error::{Error, Result};
59use crate::extract::FromCall;
60use async_trait::async_trait;
61use bytes::Bytes;
62
63/// The most parts accepted in one body. A small body can still carry tens of
64/// thousands of parts, each costing an allocation.
65const MAX_PARTS: usize = 256;
66
67/// One part of a `multipart/form-data` body.
68#[derive(Debug, Clone)]
69pub struct Part {
70    /// The `name` from `Content-Disposition`.
71    pub name: String,
72    /// The `filename`, when the part is a file upload.
73    pub filename: Option<String>,
74    /// The part's `Content-Type`, if it declared one.
75    pub content_type: Option<String>,
76    /// The part's raw content.
77    pub bytes: Bytes,
78}
79
80impl Part {
81    /// The content as UTF-8, or `None` if it is not valid UTF-8.
82    pub fn text(&self) -> Option<String> {
83        String::from_utf8(self.bytes.to_vec()).ok()
84    }
85}
86
87/// A parsed `multipart/form-data` body.
88///
89/// Consumes the request body, so it must be the last handler argument.
90#[derive(Debug, Clone, Default)]
91pub struct Multipart {
92    parts: Vec<Part>,
93}
94
95impl Multipart {
96    /// Every part, in body order.
97    pub fn parts(&self) -> &[Part] {
98        &self.parts
99    }
100
101    /// The first part with this name.
102    pub fn part(&self, name: &str) -> Option<&Part> {
103        self.parts.iter().find(|p| p.name == name)
104    }
105
106    /// The first part with this name that carries a filename.
107    pub fn file(&self, name: &str) -> Option<&Part> {
108        self.parts
109            .iter()
110            .find(|p| p.name == name && p.filename.is_some())
111    }
112
113    /// The text of the first part with this name — the plain form field case.
114    pub fn field(&self, name: &str) -> Option<String> {
115        self.part(name).and_then(|p| p.text())
116    }
117}
118
119#[async_trait]
120impl FromCall for Multipart {
121    async fn from_call(mut call: Call) -> Result<Self> {
122        let ct = call
123            .header(http::header::CONTENT_TYPE.as_str())
124            .unwrap_or("")
125            .to_string();
126        let boundary = boundary_of(&ct).ok_or_else(|| {
127            Error::new(
128                http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
129                "expected multipart/form-data with a boundary",
130            )
131        })?;
132
133        let body = call.try_receive_bytes().await?;
134        crate::extract::check_body_limit(&call, body.len())?;
135        parse(&body, &boundary).map(|parts| Multipart { parts })
136    }
137}
138
139/// Pull `boundary=` out of a `multipart/form-data` content type.
140fn boundary_of(content_type: &str) -> Option<String> {
141    let mut it = content_type.split(';');
142    let media = it.next()?.trim();
143    if !media.eq_ignore_ascii_case("multipart/form-data") {
144        return None;
145    }
146    for param in it {
147        let Some((k, v)) = param.split_once('=') else {
148            continue;
149        };
150        if k.trim().eq_ignore_ascii_case("boundary") {
151            // The value may be quoted.
152            let value = v.trim().trim_matches('"');
153            // An empty boundary would make the delimiter a bare `--`, splitting
154            // the body on every occurrence of two hyphens.
155            if value.is_empty() {
156                return None;
157            }
158            return Some(value.to_string());
159        }
160    }
161    None
162}
163
164/// The most transport padding accepted on a delimiter line.
165///
166/// RFC 2046 writes `transport-padding := *LWSP-char`, so in principle it is
167/// unbounded, but it exists for line-oriented transports that pad and no HTTP
168/// client sends any. The streaming parser has to buffer the padding before it
169/// can tell a delimiter from content, so an unbounded run would be an
170/// unbounded buffer; both parsers use the same ceiling, because two parsers
171/// that disagree about where a part ends are exactly the differential this
172/// module is trying not to be.
173const MAX_TRANSPORT_PADDING: usize = 64;
174
175/// What follows a `\r\n--boundary` match in the body.
176///
177/// RFC 2046 §5.1.1 is precise about the shape of a delimiter *line*: the
178/// delimiter, then `transport-padding` — SP and HTAB only — then CRLF; the
179/// closing delimiter puts `--` immediately after the boundary instead. A run of
180/// bytes that looks like a delimiter but is followed by anything else was never
181/// a delimiter, and belongs to the enclosing part's content.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183enum Tail {
184    /// Another part follows. The payload is how many bytes past the match its
185    /// headers begin, which is the padding plus the CRLF.
186    Part(usize),
187    /// The closing delimiter: everything after it is epilogue.
188    Close,
189    /// Not a delimiter line, so the matched bytes are part content.
190    Content,
191    /// Undecidable until more of the body arrives. Only the streaming parser
192    /// ever sees this; the buffered one has the whole body and says so.
193    NeedMore,
194}
195
196/// Classify the bytes immediately after a `\r\n--boundary` match.
197///
198/// `at_end` says that no further bytes can arrive, which turns every otherwise
199/// ambiguous truncation into a decision rather than a wait.
200fn delimiter_tail(after: &[u8], at_end: bool) -> Tail {
201    // The closing delimiter's `--` comes before its own padding, so the first
202    // two bytes settle that case on their own.
203    match (after.first(), after.get(1)) {
204        (Some(b'-'), Some(b'-')) => return Tail::Close,
205        (Some(b'-'), Some(_)) => return Tail::Content,
206        (Some(b'-'), None) => {
207            return if at_end {
208                Tail::Content
209            } else {
210                Tail::NeedMore
211            }
212        }
213        _ => {}
214    }
215
216    let mut i = 0;
217    loop {
218        match after.get(i) {
219            Some(b' ' | b'\t') if i < MAX_TRANSPORT_PADDING => i += 1,
220            Some(b'\r') => {
221                return match after.get(i + 1) {
222                    Some(b'\n') => Tail::Part(i + 2),
223                    Some(_) => Tail::Content,
224                    None if at_end => Tail::Content,
225                    None => Tail::NeedMore,
226                }
227            }
228            // Anything else — including padding past the ceiling — means this
229            // was not a delimiter line.
230            Some(_) => return Tail::Content,
231            None if at_end => return Tail::Content,
232            None => return Tail::NeedMore,
233        }
234    }
235}
236
237fn parse(body: &[u8], boundary: &str) -> Result<Vec<Part>> {
238    // RFC 2046 §5.1.1: a delimiter is CRLF followed by `--boundary`, and the
239    // line it opens carries nothing but transport padding before its CRLF. Both
240    // halves of that are load-bearing.
241    //
242    // Splitting on the bare `--boundary` let a part whose *content* embedded the
243    // boundary forge additional parts — so a single uploaded file could inject a
244    // second field the client never sent. Accepting arbitrary bytes where only
245    // padding is allowed left exactly the same hole one character further along:
246    // `\r\n--boundaryZ` is content to Go, to Python and to anything else in
247    // front, and used to be a delimiter here. That difference of opinion is the
248    // dangerous half, because a proxy or WAF that filters on a field name never
249    // sees the field it is there to reject while the origin parses it out and
250    // acts on it.
251    //
252    // So a match whose tail is not a well-formed delimiter line is content, and
253    // the scan carries on straight through it.
254    //
255    // The opening delimiter has no preceding CRLF, so a synthetic one is
256    // prepended rather than special-casing offset 0.
257    let delim = format!("\r\n--{boundary}").into_bytes();
258    let mut framed = Vec::with_capacity(body.len() + 2);
259    framed.extend_from_slice(b"\r\n");
260    framed.extend_from_slice(body);
261    let body: &[u8] = &framed;
262
263    let mut parts = Vec::new();
264    // Where the current part's headers begin, or `None` while the scan is still
265    // in the preamble that precedes the first delimiter.
266    let mut part_at: Option<usize> = None;
267    let mut closed = false;
268    let mut at = 0;
269
270    while let Some(rel) = find(&body[at..], &delim) {
271        let hit = at + rel;
272        // The whole body is in hand, so the tail is always decidable; the
273        // `NeedMore` arm below only keeps the match exhaustive without a panic.
274        match delimiter_tail(&body[hit + delim.len()..], true) {
275            Tail::Part(skip) => {
276                if let Some(from) = part_at {
277                    push_part(&mut parts, &body[from..hit])?;
278                }
279                at = hit + delim.len() + skip;
280                part_at = Some(at);
281            }
282            Tail::Close => {
283                // `take` clears the open part because it has just ended
284                // properly; the check after the loop is only for a body that
285                // ran out with a part still open.
286                if let Some(from) = part_at.take() {
287                    push_part(&mut parts, &body[from..hit])?;
288                }
289                closed = true;
290                break;
291            }
292            // Content the scan must keep. Resuming one byte past the start of
293            // the false match rather than past the whole of it matters when the
294            // boundary can overlap itself: a real delimiter may begin inside the
295            // bytes that just failed to be one.
296            Tail::Content | Tail::NeedMore => at = hit + 1,
297        }
298    }
299
300    if part_at.is_some() {
301        // A part was opened and the body ran out before its delimiter arrived.
302        // Reporting the truncation is what the streaming parser does, and a
303        // truncated upload silently becoming a shorter file is the failure this
304        // avoids.
305        return Err(Error::bad_request("multipart body ended inside a part"));
306    }
307    if !closed {
308        // No delimiter line anywhere, so this was not a multipart body at all.
309        // It used to be `200` with no parts here and `400` through
310        // `MultipartStream`; one body, one answer.
311        return Err(Error::bad_request(
312            "multipart body has no boundary delimiter",
313        ));
314    }
315
316    Ok(parts)
317}
318
319/// Parse one part out of the bytes between the end of its delimiter line and
320/// the start of the next delimiter, and append it.
321///
322/// `chunk` carries no framing of its own: the CRLF ending the delimiter line
323/// was consumed by the caller and the CRLF before the next delimiter is part of
324/// that delimiter, so everything after the header block is content exactly as
325/// it was sent.
326fn push_part(parts: &mut Vec<Part>, chunk: &[u8]) -> Result<()> {
327    let Some(split) = find(chunk, b"\r\n\r\n") else {
328        return Err(Error::bad_request(
329            "malformed multipart part: no header block",
330        ));
331    };
332    let (head, content) = chunk.split_at(split);
333    let content = &content[4..];
334
335    let headers = std::str::from_utf8(head)
336        .map_err(|_| Error::bad_request("multipart headers are not valid UTF-8"))?;
337
338    let mut name = None;
339    let mut filename = None;
340    let mut content_type = None;
341    for line in headers.split("\r\n") {
342        let Some((k, v)) = line.split_once(':') else {
343            continue;
344        };
345        match k.trim().to_ascii_lowercase().as_str() {
346            "content-disposition" => {
347                name = param_of(v, "name");
348                filename = param_of(v, "filename");
349            }
350            "content-type" => content_type = Some(v.trim().to_string()),
351            _ => {}
352        }
353    }
354
355    let Some(name) = name else {
356        return Err(Error::bad_request(
357            "multipart part is missing a Content-Disposition name",
358        ));
359    };
360
361    if parts.len() >= MAX_PARTS {
362        return Err(Error::new(
363            http::StatusCode::PAYLOAD_TOO_LARGE,
364            "too many multipart parts",
365        ));
366    }
367
368    parts.push(Part {
369        name,
370        filename,
371        content_type,
372        bytes: Bytes::copy_from_slice(content),
373    });
374    Ok(())
375}
376
377/// The most bytes of part headers accepted before a part is refused.
378///
379/// A part may declare any number of headers, and without a cap a body that is
380/// nothing but header lines grows the buffer without ever producing a field.
381const MAX_PART_HEADER_BYTES: usize = 8 * 1024;
382
383/// Default ceiling for [`Field::bytes`].
384const DEFAULT_MAX_FIELD_BYTES: usize = 8 * 1024 * 1024;
385
386/// Where the incremental parser is in the body.
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388enum Phase {
389    /// Before the first delimiter. Anything here is preamble and is discarded.
390    Preamble,
391    /// A whole delimiter line was consumed, padding and CRLF included, so the
392    /// buffer now starts at the next part's headers. Whoever matched the
393    /// delimiter has already decided it was one, which is why nothing further
394    /// re-examines those bytes.
395    AfterDelimiter,
396    /// Inside a part's content.
397    InPart,
398    /// The closing delimiter was seen.
399    Done,
400}
401
402/// An incremental `multipart/form-data` parser.
403///
404/// Consumes the request body, so it must be the last handler argument. Fields
405/// arrive one at a time from [`next_field`](Self::next_field), and each field's
406/// content is read with [`Field::chunk`] or collected with [`Field::bytes`].
407///
408/// ```
409/// use churust_core::{Churust, MultipartStream, TestClient};
410/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
411/// let app = Churust::server()
412///     .routing(|r| {
413///         r.post("/upload", |mut form: MultipartStream| async move {
414///             let mut total = 0usize;
415///             while let Some(mut field) = form.next_field().await? {
416///                 // A real handler would write each chunk to disk here.
417///                 while let Some(chunk) = field.chunk().await? {
418///                     total += chunk.len();
419///                 }
420///             }
421///             Ok::<_, churust_core::Error>(format!("{total} bytes"))
422///         });
423///     })
424///     .build();
425///
426/// let body = "--X\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.txt\"\r\n\r\nhello\r\n--X--\r\n";
427/// let res = TestClient::new(app)
428///     .post("/upload")
429///     .header("content-type", "multipart/form-data; boundary=X")
430///     .body(body)
431///     .send()
432///     .await;
433/// assert_eq!(res.text(), "5 bytes");
434/// # });
435/// ```
436pub struct MultipartStream {
437    stream: crate::call::BodyStream,
438    /// Bytes read from the body but not yet handed out.
439    buffer: bytes::BytesMut,
440    /// `\r\n--boundary`, the sequence that ends a part's content.
441    delimiter: Vec<u8>,
442    phase: Phase,
443    /// True once the body stream has ended.
444    drained: bool,
445    parts_seen: usize,
446    max_field_bytes: usize,
447}
448
449impl std::fmt::Debug for MultipartStream {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        f.debug_struct("MultipartStream")
452            .field("phase", &self.phase)
453            .field("buffered", &self.buffer.len())
454            .field("parts_seen", &self.parts_seen)
455            .finish_non_exhaustive()
456    }
457}
458
459impl MultipartStream {
460    /// Build a parser over `stream` for `boundary`.
461    fn new(stream: crate::call::BodyStream, boundary: &str) -> Self {
462        let mut buffer = bytes::BytesMut::new();
463        // The first delimiter in a body has no CRLF before it, every later one
464        // does. Seeding the buffer with a CRLF lets one search handle both
465        // instead of special-casing the opening delimiter everywhere.
466        buffer.extend_from_slice(b"\r\n");
467        Self {
468            stream,
469            buffer,
470            delimiter: format!("\r\n--{boundary}").into_bytes(),
471            phase: Phase::Preamble,
472            drained: false,
473            parts_seen: 0,
474            max_field_bytes: DEFAULT_MAX_FIELD_BYTES,
475        }
476    }
477
478    /// Cap what [`Field::bytes`] will collect (default 8 MiB).
479    ///
480    /// Only affects collecting. [`Field::chunk`] is unbounded on purpose: a
481    /// handler streaming to disk is choosing its own ceiling, and the framework
482    /// has no way to know what it is.
483    pub fn max_field_bytes(mut self, bytes: usize) -> Self {
484        self.max_field_bytes = bytes;
485        self
486    }
487
488    /// Pull one more chunk from the body.
489    ///
490    /// Returns `false` once the body has ended.
491    async fn fill(&mut self) -> Result<bool> {
492        if self.drained {
493            return Ok(false);
494        }
495        use futures_util::StreamExt;
496        match self.stream.next().await {
497            Some(Ok(chunk)) => {
498                self.buffer.extend_from_slice(&chunk);
499                Ok(true)
500            }
501            // The body's own error, which for an over-limit body is the 413
502            // raised by the per-route cap. Propagating it keeps that status.
503            Some(Err(e)) => Err(e),
504            None => {
505                self.drained = true;
506                Ok(false)
507            }
508        }
509    }
510
511    /// The next field, or `None` at the end of the body.
512    ///
513    /// A field that was not read to the end is discarded first, so a handler
514    /// may skip a part it does not want without leaving the parser mid-stream.
515    ///
516    /// # Errors
517    ///
518    /// If the body is malformed, ends early, exceeds the part count, or the
519    /// underlying body stream fails, which includes the per-route body cap
520    /// being crossed.
521    pub async fn next_field(&mut self) -> Result<Option<Field<'_>>> {
522        if self.phase == Phase::InPart {
523            self.skip_rest_of_part().await?;
524        }
525        if self.phase == Phase::Preamble {
526            self.consume_through_delimiter().await?;
527        }
528        if self.phase == Phase::Done {
529            return Ok(None);
530        }
531
532        // AfterDelimiter: the delimiter line was classified and consumed where
533        // it was matched, so what is buffered here is the part's header block.
534        // This used to skip to the next CRLF from wherever the delimiter ended,
535        // which quietly accepted arbitrary bytes in place of the transport
536        // padding RFC 2046 allows, and so let `\r\n--boundaryZ` inside a part's
537        // content open a part of its own.
538        let (name, filename, content_type) = self.read_part_headers().await?;
539
540        self.parts_seen += 1;
541        if self.parts_seen > MAX_PARTS {
542            return Err(Error::new(
543                http::StatusCode::PAYLOAD_TOO_LARGE,
544                "too many multipart parts",
545            ));
546        }
547
548        self.phase = Phase::InPart;
549        let max_field_bytes = self.max_field_bytes;
550        Ok(Some(Field {
551            name,
552            filename,
553            content_type,
554            max_field_bytes,
555            parser: self,
556        }))
557    }
558
559    /// Decide whether the boundary match at `at` in the buffer really opens a
560    /// delimiter line, reading more of the body while the tail is ambiguous.
561    ///
562    /// Never returns [`Tail::NeedMore`]: once the body has drained there is
563    /// nothing left to wait for and the tail is judged on what is there. The
564    /// wait is bounded by [`MAX_TRANSPORT_PADDING`] plus the two bytes of the
565    /// CRLF, so a hostile run of padding cannot grow the buffer.
566    async fn classify_at(&mut self, at: usize) -> Result<Tail> {
567        loop {
568            let after = at + self.delimiter.len();
569            let tail = delimiter_tail(&self.buffer[after..], self.drained);
570            if tail != Tail::NeedMore {
571                return Ok(tail);
572            }
573            self.fill().await?;
574        }
575    }
576
577    /// Discard everything up to and including the next delimiter line.
578    async fn consume_through_delimiter(&mut self) -> Result<()> {
579        loop {
580            if let Some(at) = find(&self.buffer, &self.delimiter) {
581                match self.classify_at(at).await? {
582                    Tail::Part(skip) => {
583                        let _ = self.buffer.split_to(at + self.delimiter.len() + skip);
584                        self.phase = Phase::AfterDelimiter;
585                        return Ok(());
586                    }
587                    Tail::Close => {
588                        self.phase = Phase::Done;
589                        return Ok(());
590                    }
591                    // A boundary-looking run that is not a delimiter line is
592                    // preamble like everything else before the first real one,
593                    // so it can simply be dropped. Dropping one byte rather
594                    // than the whole match keeps a genuine delimiter that
595                    // overlaps it findable.
596                    Tail::Content | Tail::NeedMore => {
597                        let _ = self.buffer.split_to(at + 1);
598                        continue;
599                    }
600                }
601            }
602            // Keep only what could still be the start of a delimiter, so a long
603            // preamble cannot grow the buffer without bound.
604            let keep = self.delimiter.len().saturating_sub(1);
605            if self.buffer.len() > keep {
606                let drop_to = self.buffer.len() - keep;
607                let _ = self.buffer.split_to(drop_to);
608            }
609            if !self.fill().await? {
610                return Err(Error::bad_request(
611                    "multipart body has no boundary delimiter",
612                ));
613            }
614        }
615    }
616
617    /// Read one part's header block.
618    async fn read_part_headers(&mut self) -> Result<(String, Option<String>, Option<String>)> {
619        let split = loop {
620            if let Some(at) = find(&self.buffer, b"\r\n\r\n") {
621                break at;
622            }
623            if self.buffer.len() > MAX_PART_HEADER_BYTES {
624                return Err(Error::new(
625                    http::StatusCode::PAYLOAD_TOO_LARGE,
626                    "multipart part headers are too large",
627                ));
628            }
629            if !self.fill().await? {
630                return Err(Error::bad_request(
631                    "malformed multipart part: no header block",
632                ));
633            }
634        };
635
636        let head = self.buffer.split_to(split);
637        let _ = self.buffer.split_to(4);
638
639        let headers = std::str::from_utf8(&head)
640            .map_err(|_| Error::bad_request("multipart headers are not valid UTF-8"))?;
641
642        let mut name = None;
643        let mut filename = None;
644        let mut content_type = None;
645        for line in headers.split("\r\n") {
646            let Some((k, v)) = line.split_once(':') else {
647                continue;
648            };
649            match k.trim().to_ascii_lowercase().as_str() {
650                "content-disposition" => {
651                    name = param_of(v, "name");
652                    filename = param_of(v, "filename");
653                }
654                "content-type" => content_type = Some(v.trim().to_string()),
655                _ => {}
656            }
657        }
658
659        let name = name.ok_or_else(|| {
660            Error::bad_request("multipart part is missing a Content-Disposition name")
661        })?;
662        Ok((name, filename, content_type))
663    }
664
665    /// Read and discard the rest of the current part.
666    async fn skip_rest_of_part(&mut self) -> Result<()> {
667        while self.next_content_chunk().await?.is_some() {}
668        Ok(())
669    }
670
671    /// The next slice of the current part's content, or `None` at its end.
672    ///
673    /// Consuming the trailing delimiter is what moves the parser on, so this is
674    /// the single place that advances out of [`Phase::InPart`].
675    async fn next_content_chunk(&mut self) -> Result<Option<Bytes>> {
676        if self.phase != Phase::InPart {
677            return Ok(None);
678        }
679        // How far into the buffer the search may start: everything before it has
680        // already been judged not to open a delimiter line. Only ever advances
681        // within one call, and `fill` appends, so the offset stays valid.
682        let mut from = 0;
683        loop {
684            if let Some(rel) = find(&self.buffer[from..], &self.delimiter) {
685                let at = from + rel;
686                // A boundary match only ends the part if a well-formed
687                // delimiter line follows it. Deciding that here rather than in
688                // `next_field` is what makes the decision act on the content:
689                // by the time the handler has been handed these bytes it is too
690                // late to take them back and call them a delimiter, or to call
691                // a delimiter content.
692                match self.classify_at(at).await? {
693                    Tail::Part(skip) => {
694                        let data = self.buffer.split_to(at).freeze();
695                        let _ = self.buffer.split_to(self.delimiter.len() + skip);
696                        self.phase = Phase::AfterDelimiter;
697                        return Ok((!data.is_empty()).then_some(data));
698                    }
699                    Tail::Close => {
700                        let data = self.buffer.split_to(at).freeze();
701                        self.phase = Phase::Done;
702                        return Ok((!data.is_empty()).then_some(data));
703                    }
704                    // Not a delimiter, so these bytes are this part's content
705                    // and will be handed out with the rest of it. Resuming one
706                    // byte in keeps a real delimiter overlapping the false one
707                    // findable.
708                    Tail::Content | Tail::NeedMore => {
709                        from = at + 1;
710                        continue;
711                    }
712                }
713            }
714
715            // Nothing but a possible partial delimiter is safe to emit, so hold
716            // back that many bytes and hand out the rest.
717            let hold = self.delimiter.len().saturating_sub(1);
718            if self.buffer.len() > hold {
719                let take = self.buffer.len() - hold;
720                let data = self.buffer.split_to(take).freeze();
721                if !data.is_empty() {
722                    return Ok(Some(data));
723                }
724            }
725
726            if !self.fill().await? {
727                return Err(Error::bad_request("multipart body ended inside a part"));
728            }
729        }
730    }
731}
732
733#[async_trait]
734impl FromCall for MultipartStream {
735    async fn from_call(call: Call) -> Result<Self> {
736        let ct = call
737            .header(http::header::CONTENT_TYPE.as_str())
738            .unwrap_or("")
739            .to_string();
740        let boundary = boundary_of(&ct).ok_or_else(|| {
741            Error::new(
742                http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
743                "expected multipart/form-data with a boundary",
744            )
745        })?;
746
747        // Going through `Payload` rather than taking the raw stream is what
748        // makes the per-route body cap apply here exactly as it does there.
749        let crate::extract::Payload(stream) = crate::extract::Payload::from_call(call).await?;
750        Ok(MultipartStream::new(stream, &boundary))
751    }
752}
753
754/// One part of a streamed `multipart/form-data` body.
755///
756/// Borrowed from the parser, so only one field exists at a time: the body is a
757/// single stream and the parts are in it in order.
758pub struct Field<'a> {
759    name: String,
760    filename: Option<String>,
761    content_type: Option<String>,
762    max_field_bytes: usize,
763    parser: &'a mut MultipartStream,
764}
765
766impl std::fmt::Debug for Field<'_> {
767    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768        f.debug_struct("Field")
769            .field("name", &self.name)
770            .field("filename", &self.filename)
771            .field("content_type", &self.content_type)
772            .finish_non_exhaustive()
773    }
774}
775
776impl Field<'_> {
777    /// The `name` from `Content-Disposition`.
778    pub fn name(&self) -> &str {
779        &self.name
780    }
781
782    /// The `filename`, when the part is a file upload.
783    pub fn filename(&self) -> Option<&str> {
784        self.filename.as_deref()
785    }
786
787    /// The part's own `Content-Type`, if it declared one.
788    pub fn content_type(&self) -> Option<&str> {
789        self.content_type.as_deref()
790    }
791
792    /// The next slice of this field's content, or `None` at its end.
793    ///
794    /// Chunk boundaries follow whatever arrived from the network and carry no
795    /// meaning: a field's content is the concatenation of its chunks.
796    ///
797    /// # Errors
798    ///
799    /// If the body is malformed or ends inside the part.
800    pub async fn chunk(&mut self) -> Result<Option<Bytes>> {
801        self.parser.next_content_chunk().await
802    }
803
804    /// Collect the whole field into memory.
805    ///
806    /// # Errors
807    ///
808    /// `413` past [`MultipartStream::max_field_bytes`], or whatever
809    /// [`chunk`](Self::chunk) would have returned. The limit is checked as the
810    /// field is read, so an oversized field is refused partway rather than
811    /// after it has all been buffered.
812    pub async fn bytes(&mut self) -> Result<Bytes> {
813        let mut out = bytes::BytesMut::new();
814        while let Some(chunk) = self.chunk().await? {
815            if out.len() + chunk.len() > self.max_field_bytes {
816                return Err(Error::new(
817                    http::StatusCode::PAYLOAD_TOO_LARGE,
818                    "multipart field too large",
819                ));
820            }
821            out.extend_from_slice(&chunk);
822        }
823        Ok(out.freeze())
824    }
825
826    /// Collect the whole field as UTF-8 text.
827    ///
828    /// # Errors
829    ///
830    /// As [`bytes`](Self::bytes), plus `400` if the content is not UTF-8.
831    pub async fn text(&mut self) -> Result<String> {
832        let raw = self.bytes().await?;
833        String::from_utf8(raw.to_vec())
834            .map_err(|_| Error::bad_request("multipart field is not valid UTF-8"))
835    }
836}
837
838/// Read a quoted parameter such as `name="x"` out of a header value.
839fn param_of(value: &str, key: &str) -> Option<String> {
840    for param in value.split(';') {
841        // The first segment is the disposition type ("form-data") and carries
842        // no '='. Skipping is required — bailing out here would mean no part
843        // ever parsed a name.
844        let Some((k, v)) = param.split_once('=') else {
845            continue;
846        };
847        if k.trim().eq_ignore_ascii_case(key) {
848            return Some(v.trim().trim_matches('"').to_string());
849        }
850    }
851    None
852}
853
854fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
855    haystack.windows(needle.len()).position(|w| w == needle)
856}
857
858#[cfg(test)]
859mod tests {
860    use super::*;
861
862    #[test]
863    fn a_delimiter_line_admits_only_spaces_and_tabs_before_its_crlf() {
864        assert_eq!(delimiter_tail(b"\r\nnext", true), Tail::Part(2));
865        assert_eq!(delimiter_tail(b" \t \r\nnext", true), Tail::Part(5));
866        assert_eq!(delimiter_tail(b"--\r\n", true), Tail::Close);
867        // The forged-part case: any other byte means the boundary-looking run
868        // was content.
869        assert_eq!(delimiter_tail(b"z\r\nnext", true), Tail::Content);
870        assert_eq!(delimiter_tail(b"-z", true), Tail::Content);
871        assert_eq!(delimiter_tail(b"\rx", true), Tail::Content);
872        // Padding past the ceiling is refused the same way, since a parser that
873        // buffers unbounded padding is a parser with an unbounded buffer.
874        let over = vec![b' '; MAX_TRANSPORT_PADDING + 1];
875        assert_eq!(delimiter_tail(&over, true), Tail::Content);
876        // A truncation is only undecidable while more bytes may still arrive.
877        assert_eq!(delimiter_tail(b"", false), Tail::NeedMore);
878        assert_eq!(delimiter_tail(b"\r", false), Tail::NeedMore);
879        assert_eq!(delimiter_tail(b"-", false), Tail::NeedMore);
880        assert_eq!(delimiter_tail(b"", true), Tail::Content);
881        assert_eq!(delimiter_tail(b"\r", true), Tail::Content);
882    }
883
884    #[test]
885    fn reads_the_boundary() {
886        assert_eq!(
887            boundary_of("multipart/form-data; boundary=abc").as_deref(),
888            Some("abc")
889        );
890        assert_eq!(
891            boundary_of("multipart/form-data; boundary=\"a b\"").as_deref(),
892            Some("a b")
893        );
894        assert_eq!(boundary_of("application/json"), None);
895        assert_eq!(boundary_of("multipart/form-data"), None);
896    }
897}