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