Skip to main content

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};