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