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