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