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