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}