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