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        /// The bare-break matcher's state (D-113).
266        m: usize,
267        /// Text normalisation: the last byte written was a CR.
268        pending_cr: bool,
269    }
270
271    impl<W: Write> fmt::Debug for MultipartWriter<W> {
272        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273            f.debug_struct("MultipartWriter")
274                .field("boundary", &self.boundary)
275                .field("in_file", &self.in_file)
276                .finish_non_exhaustive()
277        }
278    }
279
280    impl<W: Write> MultipartWriter<W> {
281        /// A writer with a generated boundary.
282        ///
283        /// # Errors
284        ///
285        /// `Random` when the OS random source fails.
286        #[cfg(feature = "random")]
287        #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
288        pub fn new(out: W) -> Result<Self, FormError> {
289            let mut w = Self::with_boundary(out, Boundary::generate()?);
290            // 128 random bits: a collision with the data is negligible.
291            w.verify = false;
292            Ok(w)
293        }
294
295        /// A writer with the caller's boundary; data is scanned for it.
296        pub fn with_boundary(out: W, boundary: Boundary) -> Self {
297            MultipartWriter {
298                out,
299                boundary,
300                verify: true,
301                first: true,
302                in_file: false,
303                poisoned: false,
304                k: 2,
305                m: 1,
306                pending_cr: false,
307            }
308        }
309
310        /// The boundary.
311        pub const fn boundary(&self) -> &Boundary {
312            &self.boundary
313        }
314
315        /// The `Content-Type` header value to send:
316        /// `multipart/form-data; boundary=...`, quoted when the boundary
317        /// holds a character that is not a token character.
318        #[must_use]
319        pub fn content_type(&self) -> String {
320            let b = self.boundary.as_bytes();
321            let token = b.iter().all(|&c| parse_net::class::is_tchar(c));
322            let b = core::str::from_utf8(b).unwrap_or("");
323            if token {
324                alloc::format!("multipart/form-data; boundary={b}")
325            } else {
326                alloc::format!("multipart/form-data; boundary=\"{b}\"")
327            }
328        }
329
330        /// Turns the delimiter scan on or off. It is on for a caller
331        /// boundary and off for a generated one.
332        pub fn verify_boundary(&mut self, on: bool) {
333            self.verify = on;
334        }
335
336        /// Writes a text field. Line breaks in `value` become CRLF. A refused
337        /// head writes nothing; after any error the output is incomplete and
338        /// must be discarded.
339        ///
340        /// # Errors
341        ///
342        /// The head errors of [`encode_part_head`](crate::encode_part_head);
343        /// `Syntax { BoundaryInContent }`; `Encoding { UnsupportedCharset }`
344        /// for a `_charset_` field naming anything but UTF-8 or US-ASCII
345        /// (the text after it would not read back); `Io`; `Poisoned`.
346        pub fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
347            self.guard(|w| {
348                if name == "_charset_"
349                    && !(value.eq_ignore_ascii_case("utf-8")
350                        || value.eq_ignore_ascii_case("us-ascii"))
351                {
352                    return Err(FormError::encoding(
353                        EncodingError::UnsupportedCharset,
354                        Location::NONE,
355                    ));
356                }
357                w.head(name, None, None)?;
358                w.in_file = false;
359                w.pending_cr = false;
360                let bytes = value.as_bytes();
361                let mut from = 0usize;
362                for (i, &b) in bytes.iter().enumerate() {
363                    match b {
364                        b'\r' => {
365                            w.data(bytes.get(from..i).unwrap_or(&[]))?;
366                            w.data(b"\r\n")?;
367                            w.pending_cr = true;
368                            from = i.saturating_add(1);
369                        }
370                        b'\n' => {
371                            w.data(bytes.get(from..i).unwrap_or(&[]))?;
372                            if !w.pending_cr {
373                                w.data(b"\r\n")?;
374                            }
375                            w.pending_cr = false;
376                            from = i.saturating_add(1);
377                        }
378                        _ => w.pending_cr = false,
379                    }
380                }
381                w.data(bytes.get(from..).unwrap_or(&[]))
382            })
383        }
384
385        /// Starts a file part; its data follows through
386        /// [`file_data`](Self::file_data).
387        ///
388        /// # Errors
389        ///
390        /// As [`text`](Self::text), without the charset rule.
391        pub fn file_start(
392            &mut self,
393            name: &str,
394            filename: &str,
395            content_type: Option<&str>,
396        ) -> Result<(), FormError> {
397            self.guard(|w| {
398                w.head(name, Some(filename), content_type)?;
399                w.in_file = true;
400                Ok(())
401            })
402        }
403
404        /// More data of the current file part.
405        ///
406        /// # Errors
407        ///
408        /// `Syntax { BoundaryInContent }`; `Io`; `Poisoned` outside a file
409        /// part.
410        pub fn file_data(&mut self, chunk: &[u8]) -> Result<(), FormError> {
411            if !self.in_file {
412                self.poisoned = true;
413                return Err(FormError::Poisoned);
414            }
415            self.guard(|w| w.data(chunk))
416        }
417
418        /// A whole file part read from `data`. Returns its length.
419        ///
420        /// # Errors
421        ///
422        /// As [`file_start`](Self::file_start) and
423        /// [`file_data`](Self::file_data); `Io` from `data`.
424        pub fn file<R: Read>(
425            &mut self,
426            name: &str,
427            filename: &str,
428            content_type: Option<&str>,
429            mut data: R,
430        ) -> Result<u64, FormError> {
431            self.file_start(name, filename, content_type)?;
432            let mut buf = [0u8; 8192];
433            let mut total = 0u64;
434            let r = loop {
435                let n = match data.read(&mut buf) {
436                    Ok(0) => break Ok(total),
437                    Ok(n) => n,
438                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
439                    Err(e) => {
440                        self.poisoned = true;
441                        break Err(FormError::Io { kind: e.kind() });
442                    }
443                };
444                if let Err(e) = self.file_data(buf.get(..n).unwrap_or(&[])) {
445                    break Err(e);
446                }
447                total = total.saturating_add(n as u64);
448            };
449            crate::wipe::wipe(&mut buf);
450            r
451        }
452
453        /// Writes the close delimiter and returns the writer.
454        ///
455        /// # Errors
456        ///
457        /// `Io`; `Poisoned`.
458        pub fn finish(mut self) -> Result<W, FormError> {
459            self.guard(|w| {
460                let mut out = IoOut(&mut w.out);
461                if !w.first {
462                    out.put(b"\r\n")?;
463                }
464                out.put(b"--")?;
465                out.put(w.boundary.as_bytes())?;
466                out.put(b"--\r\n")?;
467                w.out.flush().map_err(|e| FormError::Io { kind: e.kind() })
468            })?;
469            Ok(self.out)
470        }
471
472        /// Runs `f`; any error poisons the writer.
473        fn guard(
474            &mut self,
475            f: impl FnOnce(&mut Self) -> Result<(), FormError>,
476        ) -> Result<(), FormError> {
477            if self.poisoned {
478                return Err(FormError::Poisoned);
479            }
480            let r = f(self);
481            if r.is_err() {
482                self.poisoned = true;
483            }
484            r
485        }
486
487        fn head(
488            &mut self,
489            name: &str,
490            filename: Option<&str>,
491            content_type: Option<&str>,
492        ) -> Result<(), FormError> {
493            // Everything is checked before the first byte goes out.
494            head_len(&self.boundary, self.first, name, filename, content_type)?;
495            put_head(
496                &mut IoOut(&mut self.out),
497                &self.boundary,
498                self.first,
499                name,
500                filename,
501                content_type,
502            )?;
503            self.first = false;
504            // The data starts right after a CRLF: as if it had been seen.
505            self.k = 2;
506            self.m = 1;
507            Ok(())
508        }
509
510        /// Writes part data, scanning it for the delimiter, and for
511        /// `--boundary` after a lone CR or LF, when verifying: the parser
512        /// refuses both (D-113).
513        fn data(&mut self, bytes: &[u8]) -> Result<(), FormError> {
514            if self.verify {
515                let (k, m, found) = self.boundary.run(self.k, self.m, bytes);
516                if found != crate::multipart::boundary::Hit::None {
517                    return Err(FormError::syntax(
518                        SyntaxError::BoundaryInContent,
519                        Location::NONE,
520                    ));
521                }
522                self.k = k;
523                self.m = m;
524            }
525            IoOut(&mut self.out).put(bytes)
526        }
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    fn head(
535        first: bool,
536        name: &str,
537        filename: Option<&str>,
538        ct: Option<&str>,
539    ) -> Result<alloc::vec::Vec<u8>, FormError> {
540        let b = Boundary::new(b"X").unwrap();
541        let mut out = [0u8; 256];
542        let n = encode_part_head(&b, first, name, filename, ct, &mut out)?;
543        Ok(out[..n].to_vec())
544    }
545
546    #[test]
547    fn test_head_shape_and_escapes() {
548        assert_eq!(
549            head(false, "a\r\nb", Some("c\".txt"), Some("text/plain")).unwrap(),
550            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"
551        );
552        assert_eq!(
553            head(true, "t\tab", None, None).unwrap(),
554            b"--X\r\nContent-Disposition: form-data; name=\"t\tab\"\r\n\r\n"
555        );
556    }
557
558    #[test]
559    fn test_head_refusals() {
560        let refused = |r: Result<alloc::vec::Vec<u8>, FormError>| match r {
561            Err(FormError::Syntax { reason, .. }) => reason,
562            other => panic!("{other:?}"),
563        };
564        assert_eq!(refused(head(true, "", None, None)), SyntaxError::EmptyName);
565        assert_eq!(
566            refused(head(true, "a\0", None, None)),
567            SyntaxError::ControlInHeader
568        );
569        assert_eq!(
570            refused(head(true, "a\\b", None, None)),
571            SyntaxError::AmbiguousQuote
572        );
573        assert_eq!(
574            refused(head(true, "a%22", None, None)),
575            SyntaxError::AmbiguousQuote
576        );
577        assert_eq!(
578            refused(head(true, "a", Some("x\\"), None)),
579            SyntaxError::AmbiguousQuote
580        );
581        assert!(head(true, "a", Some("C:\\x\\y.txt"), None).is_ok());
582        assert_eq!(
583            refused(head(true, "a", None, Some("text/plain\r\nX: y"))),
584            SyntaxError::ControlInHeader
585        );
586        assert_eq!(
587            refused(head(true, "a", None, Some("not a type"))),
588            SyntaxError::InvalidContentType
589        );
590        assert_eq!(
591            refused(head(true, "a", None, Some("multipart/mixed; boundary=y"))),
592            SyntaxError::InvalidContentType
593        );
594        assert_eq!(
595            refused(head(true, "a", None, Some("text/plain; a=1; a=2"))),
596            SyntaxError::InvalidContentType
597        );
598    }
599
600    #[test]
601    fn test_head_buffer_too_small_writes_nothing() {
602        let b = Boundary::new(b"X").unwrap();
603        let mut out = [7u8; 10];
604        let e = encode_part_head(&b, true, "name", None, None, &mut out).unwrap_err();
605        assert_eq!(e, FormError::BufferTooSmall { needed: 52 });
606        assert!(out.iter().all(|&x| x == 7));
607    }
608}