form_net/multipart/mod.rs
1//! `multipart/form-data` (RFC 7578, framed by RFC 2046): the streaming
2//! parser, and the buffered entry points built on it.
3
4pub(crate) mod boundary;
5#[cfg(feature = "alloc")]
6pub(crate) mod driver;
7pub(crate) mod filename;
8pub(crate) mod header;
9#[cfg(feature = "alloc")]
10pub(crate) mod owned_head;
11pub(crate) mod parser;
12pub(crate) mod writer;
13
14#[cfg(feature = "alloc")]
15mod buffered {
16 use alloc::borrow::Cow;
17 use alloc::string::String;
18 use alloc::vec::Vec;
19 use core::fmt;
20
21 use super::filename::{FileName, FileNameSource};
22 use super::header::Text;
23 use super::parser::{
24 Charset, ContentType, MultipartParser, Outcome, PartHead, PartKind, PartSummary, Source,
25 };
26 use crate::error::{EncodingError, ExtractError, FormError, Location};
27 use crate::hook::{Inspector, NoInspector, Verdict};
28 use crate::limits::{Limits, Policy};
29
30 /// One part of a buffered multipart body. Its data borrows the body.
31 ///
32 /// # Examples
33 ///
34 /// ```
35 /// # let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHoliday\r\n--X--";
36 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
37 /// let part = &mp.parts()[0];
38 /// assert_eq!(part.name(), "title");
39 /// assert_eq!(part.text()?, "Holiday");
40 /// # Ok::<(), form_net::FormError>(())
41 /// ```
42 pub struct Part<'a> {
43 index: u32,
44 name: Cow<'a, str>,
45 name_raw: &'a [u8],
46 block: &'a [u8],
47 kind: PartKind,
48 charset: Charset,
49 filename: Option<(Cow<'a, [u8]>, FileNameSource)>,
50 content_type: Option<&'a [u8]>,
51 body: &'a [u8],
52 offset: u64,
53 }
54
55 impl fmt::Debug for Part<'_> {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.debug_struct("Part")
58 .field("index", &self.index)
59 .field("name", &self.name)
60 .field("kind", &self.kind)
61 .field("len", &self.body.len())
62 .finish_non_exhaustive()
63 }
64 }
65
66 impl<'a> Part<'a> {
67 /// The part index, from 0, in body order.
68 ///
69 /// # Examples
70 ///
71 /// ```
72 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X--";
73 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
74 /// assert_eq!(mp.parts()[0].index(), 0);
75 /// # Ok::<(), form_net::FormError>(())
76 /// ```
77 #[must_use]
78 pub const fn index(&self) -> u32 {
79 self.index
80 }
81
82 /// The field name, HTML escapes (`%0A`, `%0D`, `%22`) decoded.
83 ///
84 /// # Examples
85 ///
86 /// ```
87 /// # let body = b"--X\r\nContent-Disposition: form-data; name=\"a%22b\"\r\n\r\n1\r\n--X--";
88 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
89 /// assert_eq!(mp.parts()[0].name(), "a\"b");
90 /// # Ok::<(), form_net::FormError>(())
91 /// ```
92 #[must_use]
93 pub fn name(&self) -> &str {
94 &self.name
95 }
96
97 /// Field or file.
98 ///
99 /// # Examples
100 ///
101 /// ```
102 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a; filename=b\r\n\r\n1\r\n--X--";
103 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
104 /// assert_eq!(mp.parts()[0].kind(), form_net::PartKind::File);
105 /// # Ok::<(), form_net::FormError>(())
106 /// ```
107 #[must_use]
108 pub const fn kind(&self) -> PartKind {
109 self.kind
110 }
111
112 /// Whether this is a file part.
113 ///
114 /// # Examples
115 ///
116 /// ```
117 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X--";
118 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
119 /// assert!(!mp.parts()[0].is_file());
120 /// # Ok::<(), form_net::FormError>(())
121 /// ```
122 #[must_use]
123 pub const fn is_file(&self) -> bool {
124 matches!(self.kind, PartKind::File)
125 }
126
127 /// The client's file name, for file parts.
128 ///
129 /// # Examples
130 ///
131 /// ```
132 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a; filename=\"me.png\"\r\n\r\n1\r\n--X--";
133 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
134 /// assert_eq!(mp.parts()[0].filename().unwrap().raw(), b"me.png");
135 /// # Ok::<(), form_net::FormError>(())
136 /// ```
137 #[must_use]
138 pub fn filename(&self) -> Option<FileName<'_>> {
139 self.filename
140 .as_ref()
141 .map(|(raw, source)| FileName::new(raw, *source))
142 }
143
144 /// The declared `Content-Type`: a hint, never trusted.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a; filename=b\r\nContent-Type: image/png\r\n\r\n1\r\n--X--";
150 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
151 /// assert!(mp.parts()[0].content_type().unwrap().is("image", "png"));
152 /// # Ok::<(), form_net::FormError>(())
153 /// ```
154 #[must_use]
155 pub fn content_type(&self) -> Option<ContentType<'a>> {
156 self.content_type.and_then(ContentType::parse)
157 }
158
159 /// The part's data, borrowed from the body.
160 ///
161 /// # Examples
162 ///
163 /// ```
164 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\nhello\r\n--X--";
165 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
166 /// assert_eq!(mp.parts()[0].bytes(), b"hello");
167 /// # Ok::<(), form_net::FormError>(())
168 /// ```
169 #[must_use]
170 pub const fn bytes(&self) -> &'a [u8] {
171 self.body
172 }
173
174 /// The data as text. Text fields were checked at parse time, so this
175 /// cannot fail for them; for file parts it checks UTF-8 now.
176 ///
177 /// # Errors
178 ///
179 /// `Encoding { InvalidUtf8 }`.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\nhello\r\n--X--";
185 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
186 /// assert_eq!(mp.parts()[0].text()?, "hello");
187 /// # Ok::<(), form_net::FormError>(())
188 /// ```
189 pub fn text(&self) -> Result<&'a str, FormError> {
190 core::str::from_utf8(self.body).map_err(|_| {
191 FormError::encoding(
192 EncodingError::InvalidUtf8,
193 Location::in_part(Some(self.index), self.offset),
194 )
195 })
196 }
197
198 /// The data length.
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\nhello\r\n--X--";
204 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
205 /// assert_eq!(mp.parts()[0].len(), 5);
206 /// # Ok::<(), form_net::FormError>(())
207 /// ```
208 #[must_use]
209 pub const fn len(&self) -> usize {
210 self.body.len()
211 }
212
213 /// Whether the data is empty.
214 ///
215 /// # Examples
216 ///
217 /// ```
218 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n\r\n--X--";
219 /// # let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
220 /// assert!(mp.parts()[0].is_empty());
221 /// # Ok::<(), form_net::FormError>(())
222 /// ```
223 #[must_use]
224 pub const fn is_empty(&self) -> bool {
225 self.body.is_empty()
226 }
227
228 /// The head of this part, for inspectors.
229 pub(crate) fn head(&self) -> PartHead<'_> {
230 PartHead::from_parts(
231 self.index,
232 self.block,
233 &self.name,
234 self.name_raw,
235 self.kind,
236 self.filename(),
237 self.content_type,
238 self.charset,
239 )
240 }
241
242 fn field_text(&self) -> &'a str {
243 // Text fields were checked as UTF-8 at parse time.
244 core::str::from_utf8(self.body).unwrap_or("")
245 }
246 }
247
248 /// The parts of a buffered multipart body, in body order.
249 ///
250 /// `Debug` prints names, kinds, and lengths, never data.
251 ///
252 /// # Examples
253 ///
254 /// ```
255 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X\r\nContent-Disposition: form-data; name=f; filename=a.txt\r\n\r\nhi\r\n--X--";
256 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
257 /// assert_eq!(mp.len(), 2);
258 /// assert_eq!(mp.field("a"), Some("1"));
259 /// assert_eq!(mp.file("f").unwrap().bytes(), b"hi");
260 /// # Ok::<(), form_net::FormError>(())
261 /// ```
262 #[must_use]
263 pub struct Multipart<'a> {
264 parts: Vec<Part<'a>>,
265 }
266
267 impl fmt::Debug for Multipart<'_> {
268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269 f.debug_list().entries(&self.parts).finish()
270 }
271 }
272
273 impl<'a> Multipart<'a> {
274 /// Every part, in body order.
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X--";
280 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
281 /// assert_eq!(mp.parts().len(), 1);
282 /// # Ok::<(), form_net::FormError>(())
283 /// ```
284 #[must_use]
285 pub fn parts(&self) -> &[Part<'a>] {
286 &self.parts
287 }
288
289 /// The first text field named `name`.
290 ///
291 /// # Examples
292 ///
293 /// ```
294 /// # let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHoliday\r\n--X--";
295 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
296 /// assert_eq!(mp.field("title"), Some("Holiday"));
297 /// assert_eq!(mp.field("other"), None);
298 /// # Ok::<(), form_net::FormError>(())
299 /// ```
300 #[must_use]
301 pub fn field(&self, name: &str) -> Option<&'a str> {
302 self.parts
303 .iter()
304 .find(|p| !p.is_file() && p.name() == name)
305 .map(Part::field_text)
306 }
307
308 /// The text field named `name`, refusing a name given twice.
309 ///
310 /// # Errors
311 ///
312 /// `Extract { reason: Duplicate, field: None }`.
313 ///
314 /// # Examples
315 ///
316 /// ```
317 /// # let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X\r\nContent-Disposition: form-data; name=a\r\n\r\n2\r\n--X--";
318 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
319 /// assert!(mp.field_one("a").is_err());
320 /// assert_eq!(mp.field_one("b")?, None);
321 /// # Ok::<(), form_net::FormError>(())
322 /// ```
323 pub fn field_one(&self, name: &str) -> Result<Option<&'a str>, FormError> {
324 let mut found = None;
325 for p in self
326 .parts
327 .iter()
328 .filter(|p| !p.is_file() && p.name() == name)
329 {
330 if found.is_some() {
331 return Err(FormError::Extract {
332 reason: ExtractError::Duplicate,
333 field: None,
334 });
335 }
336 found = Some(p.field_text());
337 }
338 Ok(found)
339 }
340
341 /// Every text field named `name`, in body order.
342 ///
343 /// # Examples
344 ///
345 /// ```
346 /// # let body = b"--X\r\nContent-Disposition: form-data; name=t\r\n\r\na\r\n--X\r\nContent-Disposition: form-data; name=t\r\n\r\nb\r\n--X--";
347 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
348 /// assert_eq!(mp.fields("t").collect::<Vec<_>>(), ["a", "b"]);
349 /// # Ok::<(), form_net::FormError>(())
350 /// ```
351 pub fn fields<'m>(&'m self, name: &'m str) -> impl Iterator<Item = &'a str> + 'm {
352 self.parts
353 .iter()
354 .filter(move |p| !p.is_file() && p.name() == name)
355 .map(Part::field_text)
356 }
357
358 /// The first file part named `name`.
359 ///
360 /// # Examples
361 ///
362 /// ```
363 /// # let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=a.txt\r\n\r\nhi\r\n--X--";
364 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
365 /// assert_eq!(mp.file("f").map(|p| p.bytes()), Some(&b"hi"[..]));
366 /// # Ok::<(), form_net::FormError>(())
367 /// ```
368 #[must_use]
369 pub fn file(&self, name: &str) -> Option<&Part<'a>> {
370 self.parts.iter().find(|p| p.is_file() && p.name() == name)
371 }
372
373 /// Every file part named `name`, in body order.
374 ///
375 /// # Examples
376 ///
377 /// ```
378 /// # let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=a\r\n\r\n1\r\n--X\r\nContent-Disposition: form-data; name=f; filename=b\r\n\r\n2\r\n--X--";
379 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
380 /// assert_eq!(mp.files("f").count(), 2);
381 /// # Ok::<(), form_net::FormError>(())
382 /// ```
383 pub fn files<'m>(&'m self, name: &'m str) -> impl Iterator<Item = &'m Part<'a>> + 'm {
384 self.parts
385 .iter()
386 .filter(move |p| p.is_file() && p.name() == name)
387 }
388
389 /// The number of parts.
390 ///
391 /// # Examples
392 ///
393 /// ```
394 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", b"--X--")?;
395 /// assert_eq!(mp.len(), 0);
396 /// # Ok::<(), form_net::FormError>(())
397 /// ```
398 #[must_use]
399 pub fn len(&self) -> usize {
400 self.parts.len()
401 }
402
403 /// Whether there are no parts.
404 ///
405 /// # Examples
406 ///
407 /// ```
408 /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", b"--X--\r\n")?;
409 /// assert!(mp.is_empty());
410 /// # Ok::<(), form_net::FormError>(())
411 /// ```
412 #[must_use]
413 pub fn is_empty(&self) -> bool {
414 self.parts.is_empty()
415 }
416 }
417
418 /// Parses a buffered `multipart/form-data` body with the default limits
419 /// and policy. Part data borrows the body (zero copy); a name borrows too,
420 /// except where an HTML escape had to be decoded.
421 ///
422 /// Text fields are checked as text at parse time: a field that is not
423 /// valid UTF-8, or whose charset (its own, else the `_charset_` field
424 /// before it, else UTF-8) is not UTF-8 or US-ASCII, refuses the body.
425 ///
426 /// # Errors
427 ///
428 /// Every multipart error; `UnsupportedMediaType` when the content type is
429 /// not `multipart/form-data`.
430 ///
431 /// # Examples
432 ///
433 /// ```
434 /// let content_type = b"multipart/form-data; boundary=X";
435 /// let body = b"--X\r\n\
436 /// Content-Disposition: form-data; name=\"title\"\r\n\r\n\
437 /// Holiday\r\n\
438 /// --X\r\n\
439 /// Content-Disposition: form-data; name=\"avatar\"; filename=\"me.png\"\r\n\
440 /// Content-Type: image/png\r\n\r\n\
441 /// \x89PNG...\r\n\
442 /// --X--\r\n";
443 ///
444 /// let mp = form_net::parse_multipart(content_type, body)?;
445 /// assert_eq!(mp.field("title"), Some("Holiday"));
446 /// let avatar = mp.file("avatar").unwrap();
447 /// assert_eq!(avatar.filename().unwrap().raw(), b"me.png");
448 /// assert!(avatar.content_type().unwrap().is("image", "png"));
449 /// assert_eq!(avatar.bytes(), b"\x89PNG...");
450 /// # Ok::<(), form_net::FormError>(())
451 /// ```
452 pub fn parse_multipart<'a>(
453 content_type: &[u8],
454 body: &'a [u8],
455 ) -> Result<Multipart<'a>, FormError> {
456 parse_multipart_with(content_type, body, &Limits::DEFAULT, &Policy::DEFAULT)
457 }
458
459 /// As [`parse_multipart`] with explicit limits and policy.
460 /// `limits.validate()` runs first. The token rule of `Policy::token_field`
461 /// applies; the captured token is the value of the text field with that
462 /// name.
463 ///
464 /// # Errors
465 ///
466 /// As [`parse_multipart`]; `InvalidLimits`.
467 ///
468 /// # Examples
469 ///
470 /// ```
471 /// use form_net::{Limits, Policy};
472 ///
473 /// const AVATAR: Limits = Limits::DEFAULT
474 /// .with_max_body_bytes(512 * 1024)
475 /// .with_max_parts(4)
476 /// .with_max_files(1);
477 /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=a\r\n\r\n1\r\n--X--";
478 /// let mp = form_net::parse_multipart_with(b"multipart/form-data; boundary=X", body, &AVATAR, &Policy::DEFAULT)?;
479 /// assert_eq!(mp.len(), 1);
480 /// # Ok::<(), form_net::FormError>(())
481 /// ```
482 pub fn parse_multipart_with<'a>(
483 content_type: &[u8],
484 body: &'a [u8],
485 limits: &Limits,
486 policy: &Policy,
487 ) -> Result<Multipart<'a>, FormError> {
488 parse_multipart_inspected_raw(content_type, body, limits, policy, &mut NoInspector)
489 }
490
491 /// As [`parse_multipart_with`], with an inspector called in the order
492 /// the [`Inspector`] documentation guarantees. For a buffered body the
493 /// sniff window is a slice of the body, not a copy. A part the
494 /// inspector skips is left out of the result.
495 ///
496 /// # Errors
497 ///
498 /// As [`parse_multipart_with`]; `Refused`.
499 ///
500 /// # Examples
501 ///
502 /// ```
503 /// use form_net::{Inspector, Limits, PartHead, Policy, Refusal, Verdict};
504 ///
505 /// struct OnlyTitle;
506 /// impl Inspector for OnlyTitle {
507 /// fn part_start(&mut self, head: &PartHead<'_>) -> Verdict {
508 /// if head.name() == "title" { Verdict::Continue } else { Verdict::Skip }
509 /// }
510 /// }
511 ///
512 /// let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHi\r\n--X\r\nContent-Disposition: form-data; name=other\r\n\r\nx\r\n--X--";
513 /// let mp = form_net::parse_multipart_inspected(b"multipart/form-data; boundary=X", body, &Limits::DEFAULT, &Policy::DEFAULT, &mut OnlyTitle)?;
514 /// assert_eq!((mp.len(), mp.field("title")), (1, Some("Hi")));
515 /// # Ok::<(), form_net::FormError>(())
516 /// ```
517 pub fn parse_multipart_inspected<'a, I: Inspector>(
518 content_type: &[u8],
519 body: &'a [u8],
520 limits: &Limits,
521 policy: &Policy,
522 inspector: &mut I,
523 ) -> Result<Multipart<'a>, FormError> {
524 let result = parse_multipart_inspected_raw(content_type, body, limits, policy, inspector);
525 if let Err(e) = &result {
526 inspector.failed(e);
527 }
528 result
529 }
530
531 fn verdict(v: Verdict, at: Location) -> Result<bool, FormError> {
532 match v {
533 Verdict::Refuse(refusal) => Err(FormError::Refused { refusal, at }),
534 Verdict::Skip => Ok(true),
535 _ => Ok(false),
536 }
537 }
538
539 /// The buffered parser with its hook calls; `failed` is the caller's.
540 pub(crate) fn parse_multipart_inspected_raw<'a, I: Inspector>(
541 content_type: &[u8],
542 body: &'a [u8],
543 limits: &Limits,
544 policy: &Policy,
545 inspector: &mut I,
546 ) -> Result<Multipart<'a>, FormError> {
547 let boundary = crate::Boundary::parse(content_type)?;
548 let mut parser = MultipartParser::new(boundary, limits, policy)?;
549 let sniff = limits.sniff() as usize;
550 let skip_empty = policy.empty_files == crate::EmptyFilePolicy::Skip;
551 let mut parts = Vec::new();
552 let mut pos = 0usize;
553 // The part being read, whether its text is readable, and whether
554 // the inspector skipped it.
555 let mut current: Option<(Part<'a>, crate::multipart::parser::TextRule, bool)> = None;
556 loop {
557 let rest = body.get(pos..).unwrap_or(&[]);
558 // Read before `advance`: a `_charset_` part that starts in this
559 // step resets the record, and the part's text is read by the
560 // value declared before it, as the driver reads it (D-128, D-141).
561 let declared = crate::multipart::parser::declared_rule(parser.declared_charset());
562 let (consumed, outcome) = parser.advance(rest, true)?;
563 let base = pos;
564 pos = pos.saturating_add(consumed);
565 let at = Location::in_part(parser.parts_seen().checked_sub(1), parser.bytes_seen());
566 match outcome {
567 Outcome::PartStart(facts, source) => {
568 let block_start = match source {
569 Source::Input(start) => base.saturating_add(start),
570 // The whole body is one chunk, so a head is always in it.
571 Source::Buffer => base,
572 };
573 let block = body
574 .get(block_start..block_start.saturating_add(facts.block_len as usize))
575 .unwrap_or(&[]);
576 let name: Cow<'a, str> = match facts.name {
577 Text::Block(span) => {
578 Cow::Borrowed(core::str::from_utf8(span.get(block)).unwrap_or(""))
579 }
580 Text::Area(len) => Cow::Owned(String::from(parser.area_name(len))),
581 };
582 let filename = facts.filename.map(|(text, source)| {
583 let raw: Cow<'a, [u8]> = match text {
584 Text::Block(span) => Cow::Borrowed(span.get(block)),
585 Text::Area(len) => Cow::Owned(parser.area_filename(len).to_vec()),
586 };
587 (raw, source)
588 });
589 let charset_ok = facts.charset.rule(declared);
590 let part = Part {
591 index: parser.parts_seen().saturating_sub(1),
592 name,
593 name_raw: facts.name_raw.get(block),
594 block,
595 kind: facts.kind,
596 charset: facts.charset,
597 filename,
598 content_type: facts.content_type.map(|span| span.get(block)),
599 body: &[],
600 offset: parser.data_start(),
601 };
602 // A file input left empty under `Skip` is announced at
603 // its first data byte, which here is at its end.
604 let empty_name = part
605 .filename
606 .as_ref()
607 .is_some_and(|(raw, _)| raw.is_empty());
608 let delayed = part.is_file() && empty_name && skip_empty;
609 let skipped = !delayed && verdict(inspector.part_start(&part.head()), at)?;
610 if skipped {
611 parser.skip_part();
612 }
613 current = Some((part, charset_ok, skipped));
614 }
615 Outcome::PartEnd(summary) => {
616 let Some((mut part, charset_ok, mut skipped)) = current.take() else {
617 continue;
618 };
619 let start = usize::try_from(part.offset).unwrap_or(usize::MAX);
620 let end = start.saturating_add(usize::try_from(summary.bytes).unwrap_or(0));
621 part.body = body.get(start..end).unwrap_or(&[]);
622 let empty_name = part
623 .filename
624 .as_ref()
625 .is_some_and(|(raw, _)| raw.is_empty());
626 if part.is_file() && empty_name && skip_empty {
627 if part.body.is_empty() {
628 // A file input left empty produces nothing.
629 continue;
630 }
631 skipped = verdict(inspector.part_start(&part.head()), at)?;
632 }
633 if !skipped && part.is_file() && sniff > 0 {
634 let window = part.body.get(..sniff.min(part.body.len())).unwrap_or(&[]);
635 skipped = verdict(inspector.sniff(&part.head(), window), at)?;
636 }
637 if !skipped && !part.body.is_empty() {
638 skipped = verdict(inspector.data(part.index, part.body), at)?;
639 }
640 // A text field whose whole value was read is checked as
641 // text before `field`, as the streaming driver checks it
642 // (0.9: the two once differed on skipped fields).
643 if !skipped && !part.is_file() {
644 let text_at = Location::in_part(Some(part.index), part.offset);
645 charset_ok
646 .check(part.body)
647 .map_err(|reason| FormError::encoding(reason, text_at))?;
648 }
649 if !skipped && !part.is_file() {
650 skipped = verdict(inspector.field(part.name(), part.body), at)?;
651 }
652 let summary = PartSummary {
653 skipped: skipped || summary.skipped,
654 ..summary
655 };
656 verdict(inspector.part_end(&summary), at)?;
657 if !skipped {
658 parts.push(part);
659 }
660 }
661 Outcome::End => {
662 // Whatever follows the close delimiter is epilogue: it
663 // still counts against its budget.
664 if pos < body.len() {
665 let rest = body.get(pos..).unwrap_or(&[]);
666 let _ = parser.advance(rest, true)?;
667 }
668 if let Verdict::Refuse(refusal) = inspector.end() {
669 return Err(FormError::Refused { refusal, at });
670 }
671 break;
672 }
673 Outcome::DataInput(..) | Outcome::DataDelimiter(_) | Outcome::NeedInput => {}
674 }
675 }
676 Ok(Multipart { parts })
677 }
678}
679
680#[cfg(feature = "alloc")]
681pub(crate) use buffered::parse_multipart_inspected_raw;
682#[cfg(feature = "alloc")]
683pub use buffered::{
684 Multipart, Part, parse_multipart, parse_multipart_inspected, parse_multipart_with,
685};