Skip to main content

form_net/multipart/
writer.rs

1//! The client side: part heads encoded as browsers encode them, and a
2//! writer whose every body parses back with form-net to the same fields.
3
4use crate::error::{FormError, Location, SyntaxError};
5use crate::multipart::boundary::Boundary;
6
7const DISPOSITION: &[u8] = b"Content-Disposition: form-data; name=\"";
8const FILENAME: &[u8] = b"\"; filename=\"";
9const CONTENT_TYPE: &[u8] = b"Content-Type: ";
10
11/// Where encoded bytes go: a caller buffer or a writer.
12trait Out {
13    fn put(&mut self, bytes: &[u8]) -> Result<(), FormError>;
14}
15
16struct SliceOut<'o> {
17    out: &'o mut [u8],
18    pos: usize,
19}
20
21impl Out for SliceOut<'_> {
22    fn put(&mut self, bytes: &[u8]) -> Result<(), FormError> {
23        let end = self.pos.saturating_add(bytes.len());
24        match self.out.get_mut(self.pos..end) {
25            Some(dst) => {
26                dst.copy_from_slice(bytes);
27                self.pos = end;
28                Ok(())
29            }
30            None => Err(FormError::BufferTooSmall { needed: end }),
31        }
32    }
33}
34
35fn syntax(reason: SyntaxError) -> FormError {
36    FormError::syntax(reason, Location::NONE)
37}
38
39/// Checks a name or file name and returns its length once escaped.
40///
41/// LF, CR, and `"` are escaped as `%0A`, `%0D`, `%22` (the browser rule);
42/// any other control byte but HTAB is refused. So are the sequences that
43/// would read back as another character: a literal `%0A`, `%0D`, or `%22`
44/// (a reader decodes it), and for names any backslash (form-net refuses one
45/// in a name, D-082), for file names a trailing one (it would read as `\"`).
46fn escaped_len(text: &str, is_name: bool) -> Result<usize, FormError> {
47    let bytes = text.as_bytes();
48    if is_name && bytes.is_empty() {
49        return Err(syntax(SyntaxError::EmptyName));
50    }
51    if crate::multipart::filename::has_html_escape(bytes) {
52        return Err(syntax(SyntaxError::AmbiguousQuote));
53    }
54    if (is_name && bytes.contains(&b'\\')) || (!is_name && bytes.last() == Some(&b'\\')) {
55        return Err(syntax(SyntaxError::AmbiguousQuote));
56    }
57    let mut len = 0usize;
58    for &b in bytes {
59        len = len.saturating_add(match b {
60            b'\n' | b'\r' | b'"' => 3,
61            b'\t' => 1,
62            b if b < 0x20 || b == 0x7F => return Err(syntax(SyntaxError::ControlInHeader)),
63            _ => 1,
64        });
65    }
66    Ok(len)
67}
68
69/// Writes `text` with the browser escapes. `escaped_len` has checked it.
70fn put_escaped(out: &mut impl Out, text: &str) -> Result<(), FormError> {
71    let bytes = text.as_bytes();
72    let mut from = 0usize;
73    for (i, &b) in bytes.iter().enumerate() {
74        let esc: &[u8] = match b {
75            b'\n' => b"%0A",
76            b'\r' => b"%0D",
77            b'"' => b"%22",
78            _ => continue,
79        };
80        out.put(bytes.get(from..i).unwrap_or(&[]))?;
81        out.put(esc)?;
82        from = i.saturating_add(1);
83    }
84    out.put(bytes.get(from..).unwrap_or(&[]))
85}
86
87/// Checks a part's `Content-Type` value the way the parser will read it.
88fn check_content_type(ct: &str) -> Result<(), FormError> {
89    let raw = ct.as_bytes();
90    if raw.iter().any(|&b| (b < 0x20 && b != b'\t') || b == 0x7F) {
91        return Err(syntax(SyntaxError::ControlInHeader));
92    }
93    if raw.windows(2).any(|w| w == b"\\\"") {
94        return Err(syntax(SyntaxError::AmbiguousQuote));
95    }
96    let invalid = || syntax(SyntaxError::InvalidContentType);
97    let mt = parse_net::media::parse(raw, &crate::kind::HEADER_LIMITS).map_err(|_| invalid())?;
98    if crate::kind::repeated_name(&mt.params()).is_some()
99        || mt.type_().eq_ignore_ascii_case(b"multipart")
100    {
101        return Err(invalid());
102    }
103    Ok(())
104}
105
106/// The exact length of a part head, after checking every input.
107fn head_len(
108    boundary: &Boundary,
109    first: bool,
110    name: &str,
111    filename: Option<&str>,
112    content_type: Option<&str>,
113) -> Result<usize, FormError> {
114    let mut len = if first { 0usize } else { 2 };
115    len = len
116        .saturating_add(2)
117        .saturating_add(boundary.as_bytes().len())
118        .saturating_add(2)
119        .saturating_add(DISPOSITION.len())
120        .saturating_add(escaped_len(name, true)?);
121    if let Some(f) = filename {
122        len = len
123            .saturating_add(FILENAME.len())
124            .saturating_add(escaped_len(f, false)?);
125    }
126    // The closing quote and CRLF of the disposition line.
127    len = len.saturating_add(3);
128    if let Some(ct) = content_type {
129        check_content_type(ct)?;
130        len = len
131            .saturating_add(CONTENT_TYPE.len())
132            .saturating_add(ct.len())
133            .saturating_add(2);
134    }
135    Ok(len.saturating_add(2))
136}
137
138/// Writes a head that `head_len` accepted.
139fn put_head(
140    out: &mut impl Out,
141    boundary: &Boundary,
142    first: bool,
143    name: &str,
144    filename: Option<&str>,
145    content_type: Option<&str>,
146) -> Result<(), FormError> {
147    if !first {
148        out.put(b"\r\n")?;
149    }
150    out.put(b"--")?;
151    out.put(boundary.as_bytes())?;
152    out.put(b"\r\n")?;
153    out.put(DISPOSITION)?;
154    put_escaped(out, name)?;
155    if let Some(f) = filename {
156        out.put(FILENAME)?;
157        put_escaped(out, f)?;
158    }
159    out.put(b"\"\r\n")?;
160    if let Some(ct) = content_type {
161        out.put(CONTENT_TYPE)?;
162        out.put(ct.as_bytes())?;
163        out.put(b"\r\n")?;
164    }
165    out.put(b"\r\n")
166}
167
168/// Writes one part's head into `out` without allocating: the delimiter
169/// (with its leading CRLF unless `first`), the `Content-Disposition` line
170/// with browser escaping, the optional `Content-Type` line, and the blank
171/// line. Returns the bytes written.
172///
173/// After the last part's data the caller writes the close delimiter:
174/// [`Boundary::delimiter`] followed by `--\r\n`; for a body with no parts,
175/// `--`, the boundary, and `--\r\n`.
176///
177/// # Errors
178///
179/// `BufferTooSmall { needed }` (nothing is written);
180/// `Syntax { EmptyName | ControlInHeader | AmbiguousQuote | InvalidContentType }`.
181///
182/// # Examples
183///
184/// ```
185/// let b = form_net::Boundary::new(b"X")?;
186/// let mut out = [0u8; 128];
187/// let n = form_net::encode_part_head(&b, true, "a\"b", None, None, &mut out)?;
188/// assert_eq!(&out[..n], b"--X\r\nContent-Disposition: form-data; name=\"a%22b\"\r\n\r\n");
189/// # Ok::<(), form_net::FormError>(())
190/// ```
191pub fn encode_part_head(
192    boundary: &Boundary,
193    first: bool,
194    name: &str,
195    filename: Option<&str>,
196    content_type: Option<&str>,
197    out: &mut [u8],
198) -> Result<usize, FormError> {
199    let needed = head_len(boundary, first, name, filename, content_type)?;
200    if out.len() < needed {
201        return Err(FormError::BufferTooSmall { needed });
202    }
203    let mut sink = SliceOut { out, pos: 0 };
204    put_head(&mut sink, boundary, first, name, filename, content_type)?;
205    Ok(sink.pos)
206}
207
208#[cfg(feature = "std")]
209pub use writer_std::MultipartWriter;
210
211#[cfg(feature = "std")]
212mod writer_std {
213    use alloc::string::String;
214    use core::fmt;
215    use std::io::{Read, Write};
216
217    use super::{Out, head_len, put_head};
218    use crate::error::{EncodingError, FormError, Location, SyntaxError};
219    use crate::multipart::boundary::Boundary;
220
221    struct IoOut<'w, W: Write>(&'w mut W);
222
223    impl<W: Write> Out for IoOut<'_, W> {
224        fn put(&mut self, bytes: &[u8]) -> Result<(), FormError> {
225            self.0
226                .write_all(bytes)
227                .map_err(|e| FormError::Io { kind: e.kind() })
228        }
229    }
230
231    /// Writes a `multipart/form-data` body as browsers do.
232    ///
233    /// Names and file names are escaped by the browser rule; every input
234    /// that could read back differently is refused; text line breaks become
235    /// CRLF. With a caller boundary, data is scanned for the delimiter
236    /// across chunk splits and refused if it contains one. So every body the
237    /// writer produces parses back with form-net to the same fields.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// let mut body = Vec::new();
243    /// // `MultipartWriter::new` (feature `random`) generates the boundary.
244    /// let boundary = form_net::Boundary::new(b"----form-net-example")?;
245    /// let mut w = form_net::MultipartWriter::with_boundary(&mut body, boundary);
246    /// w.text("title", "Holiday")?;
247    /// w.file("photo", "beach.jpg", Some("image/jpeg"), &b"\xff\xd8 jpeg"[..])?;
248    /// let content_type = w.content_type();
249    /// w.finish()?;
250    ///
251    /// let mp = form_net::parse_multipart(content_type.as_bytes(), &body)?;
252    /// assert_eq!(mp.field("title"), Some("Holiday"));
253    /// assert_eq!(mp.file("photo").map(|p| p.bytes()), Some(&b"\xff\xd8 jpeg"[..]));
254    /// # Ok::<(), form_net::FormError>(())
255    /// ```
256    pub struct MultipartWriter<W: Write> {
257        out: W,
258        boundary: Boundary,
259        verify: bool,
260        first: bool,
261        in_file: bool,
262        poisoned: bool,
263        /// The delimiter matcher's state over the current part's data.
264        k: usize,
265        /// Text normalisation: the last byte written was a CR.
266        pending_cr: bool,
267    }
268
269    impl<W: Write> fmt::Debug for MultipartWriter<W> {
270        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271            f.debug_struct("MultipartWriter")
272                .field("boundary", &self.boundary)
273                .field("in_file", &self.in_file)
274                .finish_non_exhaustive()
275        }
276    }
277
278    impl<W: Write> MultipartWriter<W> {
279        /// A writer with a generated boundary.
280        ///
281        /// # Errors
282        ///
283        /// `Random` when the OS random source fails.
284        #[cfg(feature = "random")]
285        #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
286        pub fn new(out: W) -> Result<Self, FormError> {
287            let mut w = Self::with_boundary(out, Boundary::generate()?);
288            // 128 random bits: a collision with the data is negligible.
289            w.verify = false;
290            Ok(w)
291        }
292
293        /// A writer with the caller's boundary; data is scanned for it.
294        pub fn with_boundary(out: W, boundary: Boundary) -> Self {
295            MultipartWriter {
296                out,
297                boundary,
298                verify: true,
299                first: true,
300                in_file: false,
301                poisoned: false,
302                k: 2,
303                pending_cr: false,
304            }
305        }
306
307        /// The boundary.
308        pub const fn boundary(&self) -> &Boundary {
309            &self.boundary
310        }
311
312        /// The `Content-Type` header value to send:
313        /// `multipart/form-data; boundary=...`, quoted when the boundary
314        /// holds a character that is not a token character.
315        #[must_use]
316        pub fn content_type(&self) -> String {
317            let b = self.boundary.as_bytes();
318            let token = b.iter().all(|&c| parse_net::class::is_tchar(c));
319            let b = core::str::from_utf8(b).unwrap_or("");
320            if token {
321                alloc::format!("multipart/form-data; boundary={b}")
322            } else {
323                alloc::format!("multipart/form-data; boundary=\"{b}\"")
324            }
325        }
326
327        /// Turns the delimiter scan on or off. It is on for a caller
328        /// boundary and off for a generated one.
329        pub fn verify_boundary(&mut self, on: bool) {
330            self.verify = on;
331        }
332
333        /// Writes a text field. Line breaks in `value` become CRLF. A refused
334        /// head writes nothing; after any error the output is incomplete and
335        /// must be discarded.
336        ///
337        /// # Errors
338        ///
339        /// The head errors of [`encode_part_head`](crate::encode_part_head);
340        /// `Syntax { BoundaryInContent }`; `Encoding { UnsupportedCharset }`
341        /// for a `_charset_` field naming anything but UTF-8 or US-ASCII
342        /// (the text after it would not read back); `Io`; `Poisoned`.
343        pub fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
344            self.guard(|w| {
345                if name == "_charset_"
346                    && !(value.eq_ignore_ascii_case("utf-8")
347                        || value.eq_ignore_ascii_case("us-ascii"))
348                {
349                    return Err(FormError::encoding(
350                        EncodingError::UnsupportedCharset,
351                        Location::NONE,
352                    ));
353                }
354                w.head(name, None, None)?;
355                w.in_file = false;
356                w.pending_cr = false;
357                let bytes = value.as_bytes();
358                let mut from = 0usize;
359                for (i, &b) in bytes.iter().enumerate() {
360                    match b {
361                        b'\r' => {
362                            w.data(bytes.get(from..i).unwrap_or(&[]))?;
363                            w.data(b"\r\n")?;
364                            w.pending_cr = true;
365                            from = i.saturating_add(1);
366                        }
367                        b'\n' => {
368                            w.data(bytes.get(from..i).unwrap_or(&[]))?;
369                            if !w.pending_cr {
370                                w.data(b"\r\n")?;
371                            }
372                            w.pending_cr = false;
373                            from = i.saturating_add(1);
374                        }
375                        _ => w.pending_cr = false,
376                    }
377                }
378                w.data(bytes.get(from..).unwrap_or(&[]))
379            })
380        }
381
382        /// Starts a file part; its data follows through
383        /// [`file_data`](Self::file_data).
384        ///
385        /// # Errors
386        ///
387        /// As [`text`](Self::text), without the charset rule.
388        pub fn file_start(
389            &mut self,
390            name: &str,
391            filename: &str,
392            content_type: Option<&str>,
393        ) -> Result<(), FormError> {
394            self.guard(|w| {
395                w.head(name, Some(filename), content_type)?;
396                w.in_file = true;
397                Ok(())
398            })
399        }
400
401        /// More data of the current file part.
402        ///
403        /// # Errors
404        ///
405        /// `Syntax { BoundaryInContent }`; `Io`; `Poisoned` outside a file
406        /// part.
407        pub fn file_data(&mut self, chunk: &[u8]) -> Result<(), FormError> {
408            if !self.in_file {
409                self.poisoned = true;
410                return Err(FormError::Poisoned);
411            }
412            self.guard(|w| w.data(chunk))
413        }
414
415        /// A whole file part read from `data`. Returns its length.
416        ///
417        /// # Errors
418        ///
419        /// As [`file_start`](Self::file_start) and
420        /// [`file_data`](Self::file_data); `Io` from `data`.
421        pub fn file<R: Read>(
422            &mut self,
423            name: &str,
424            filename: &str,
425            content_type: Option<&str>,
426            mut data: R,
427        ) -> Result<u64, FormError> {
428            self.file_start(name, filename, content_type)?;
429            let mut buf = [0u8; 8192];
430            let mut total = 0u64;
431            let r = loop {
432                let n = match data.read(&mut buf) {
433                    Ok(0) => break Ok(total),
434                    Ok(n) => n,
435                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
436                    Err(e) => {
437                        self.poisoned = true;
438                        break Err(FormError::Io { kind: e.kind() });
439                    }
440                };
441                if let Err(e) = self.file_data(buf.get(..n).unwrap_or(&[])) {
442                    break Err(e);
443                }
444                total = total.saturating_add(n as u64);
445            };
446            crate::wipe::wipe(&mut buf);
447            r
448        }
449
450        /// Writes the close delimiter and returns the writer.
451        ///
452        /// # Errors
453        ///
454        /// `Io`; `Poisoned`.
455        pub fn finish(mut self) -> Result<W, FormError> {
456            self.guard(|w| {
457                let mut out = IoOut(&mut w.out);
458                if !w.first {
459                    out.put(b"\r\n")?;
460                }
461                out.put(b"--")?;
462                out.put(w.boundary.as_bytes())?;
463                out.put(b"--\r\n")?;
464                w.out.flush().map_err(|e| FormError::Io { kind: e.kind() })
465            })?;
466            Ok(self.out)
467        }
468
469        /// Runs `f`; any error poisons the writer.
470        fn guard(
471            &mut self,
472            f: impl FnOnce(&mut Self) -> Result<(), FormError>,
473        ) -> Result<(), FormError> {
474            if self.poisoned {
475                return Err(FormError::Poisoned);
476            }
477            let r = f(self);
478            if r.is_err() {
479                self.poisoned = true;
480            }
481            r
482        }
483
484        fn head(
485            &mut self,
486            name: &str,
487            filename: Option<&str>,
488            content_type: Option<&str>,
489        ) -> Result<(), FormError> {
490            // Everything is checked before the first byte goes out.
491            head_len(&self.boundary, self.first, name, filename, content_type)?;
492            put_head(
493                &mut IoOut(&mut self.out),
494                &self.boundary,
495                self.first,
496                name,
497                filename,
498                content_type,
499            )?;
500            self.first = false;
501            // The data starts right after a CRLF: as if it had been seen.
502            self.k = 2;
503            Ok(())
504        }
505
506        /// Writes part data, scanning it for the delimiter when verifying.
507        fn data(&mut self, bytes: &[u8]) -> Result<(), FormError> {
508            if self.verify {
509                let (k, found) = self.boundary.run(self.k, bytes);
510                if found.is_some() {
511                    return Err(FormError::syntax(
512                        SyntaxError::BoundaryInContent,
513                        Location::NONE,
514                    ));
515                }
516                self.k = k;
517            }
518            IoOut(&mut self.out).put(bytes)
519        }
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    fn head(
528        first: bool,
529        name: &str,
530        filename: Option<&str>,
531        ct: Option<&str>,
532    ) -> Result<alloc::vec::Vec<u8>, FormError> {
533        let b = Boundary::new(b"X").unwrap();
534        let mut out = [0u8; 256];
535        let n = encode_part_head(&b, first, name, filename, ct, &mut out)?;
536        Ok(out[..n].to_vec())
537    }
538
539    #[test]
540    fn test_head_shape_and_escapes() {
541        assert_eq!(
542            head(false, "a\r\nb", Some("c\".txt"), Some("text/plain")).unwrap(),
543            b"\r\n--X\r\nContent-Disposition: form-data; name=\"a%0D%0Ab\"; filename=\"c%22.txt\"\r\nContent-Type: text/plain\r\n\r\n"
544        );
545        assert_eq!(
546            head(true, "t\tab", None, None).unwrap(),
547            b"--X\r\nContent-Disposition: form-data; name=\"t\tab\"\r\n\r\n"
548        );
549    }
550
551    #[test]
552    fn test_head_refusals() {
553        let refused = |r: Result<alloc::vec::Vec<u8>, FormError>| match r {
554            Err(FormError::Syntax { reason, .. }) => reason,
555            other => panic!("{other:?}"),
556        };
557        assert_eq!(refused(head(true, "", None, None)), SyntaxError::EmptyName);
558        assert_eq!(
559            refused(head(true, "a\0", None, None)),
560            SyntaxError::ControlInHeader
561        );
562        assert_eq!(
563            refused(head(true, "a\\b", None, None)),
564            SyntaxError::AmbiguousQuote
565        );
566        assert_eq!(
567            refused(head(true, "a%22", None, None)),
568            SyntaxError::AmbiguousQuote
569        );
570        assert_eq!(
571            refused(head(true, "a", Some("x\\"), None)),
572            SyntaxError::AmbiguousQuote
573        );
574        assert!(head(true, "a", Some("C:\\x\\y.txt"), None).is_ok());
575        assert_eq!(
576            refused(head(true, "a", None, Some("text/plain\r\nX: y"))),
577            SyntaxError::ControlInHeader
578        );
579        assert_eq!(
580            refused(head(true, "a", None, Some("not a type"))),
581            SyntaxError::InvalidContentType
582        );
583        assert_eq!(
584            refused(head(true, "a", None, Some("multipart/mixed; boundary=y"))),
585            SyntaxError::InvalidContentType
586        );
587        assert_eq!(
588            refused(head(true, "a", None, Some("text/plain; a=1; a=2"))),
589            SyntaxError::InvalidContentType
590        );
591    }
592
593    #[test]
594    fn test_head_buffer_too_small_writes_nothing() {
595        let b = Boundary::new(b"X").unwrap();
596        let mut out = [7u8; 10];
597        let e = encode_part_head(&b, true, "name", None, None, &mut out).unwrap_err();
598        assert_eq!(e, FormError::BufferTooSmall { needed: 52 });
599        assert!(out.iter().all(|&x| x == 7));
600    }
601}