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    if crate::kind::has_empty_parameter(raw) {
98        // The parser refuses it (D-136); the writer never sends what it
99        // would not read.
100        return Err(invalid());
101    }
102    let mt = parse_net::media::parse(raw, &crate::kind::HEADER_LIMITS).map_err(|_| invalid())?;
103    if crate::kind::repeated_name(&mt.params()).is_some()
104        || mt.type_().eq_ignore_ascii_case(b"multipart")
105    {
106        return Err(invalid());
107    }
108    Ok(())
109}
110
111/// The exact length of a part head, after checking every input.
112fn head_len(
113    boundary: &Boundary,
114    first: bool,
115    name: &str,
116    filename: Option<&str>,
117    content_type: Option<&str>,
118) -> Result<usize, FormError> {
119    // A name or file name holding `name=` is refused by the parser (D-136):
120    // escaping cannot add or remove one, so the plain text is checked.
121    if crate::kind::holds_name_assignment(name.as_bytes())
122        || filename.is_some_and(|f| crate::kind::holds_name_assignment(f.as_bytes()))
123    {
124        return Err(syntax(SyntaxError::MalformedParameter));
125    }
126    let mut len = if first { 0usize } else { 2 };
127    len = len
128        .saturating_add(2)
129        .saturating_add(boundary.as_bytes().len())
130        .saturating_add(2)
131        .saturating_add(DISPOSITION.len())
132        .saturating_add(escaped_len(name, true)?);
133    if let Some(f) = filename {
134        len = len
135            .saturating_add(FILENAME.len())
136            .saturating_add(escaped_len(f, false)?);
137    }
138    // The closing quote and CRLF of the disposition line.
139    len = len.saturating_add(3);
140    if let Some(ct) = content_type {
141        check_content_type(ct)?;
142        len = len
143            .saturating_add(CONTENT_TYPE.len())
144            .saturating_add(ct.len())
145            .saturating_add(2);
146    }
147    Ok(len.saturating_add(2))
148}
149
150/// Writes a head that `head_len` accepted.
151fn put_head(
152    out: &mut impl Out,
153    boundary: &Boundary,
154    first: bool,
155    name: &str,
156    filename: Option<&str>,
157    content_type: Option<&str>,
158) -> Result<(), FormError> {
159    if !first {
160        out.put(b"\r\n")?;
161    }
162    out.put(b"--")?;
163    out.put(boundary.as_bytes())?;
164    out.put(b"\r\n")?;
165    out.put(DISPOSITION)?;
166    put_escaped(out, name)?;
167    if let Some(f) = filename {
168        out.put(FILENAME)?;
169        put_escaped(out, f)?;
170    }
171    out.put(b"\"\r\n")?;
172    if let Some(ct) = content_type {
173        out.put(CONTENT_TYPE)?;
174        out.put(ct.as_bytes())?;
175        out.put(b"\r\n")?;
176    }
177    out.put(b"\r\n")
178}
179
180/// Writes one part's head into `out` without allocating: the delimiter
181/// (with its leading CRLF unless `first`), the `Content-Disposition` line
182/// with browser escaping, the optional `Content-Type` line, and the blank
183/// line. Returns the bytes written.
184///
185/// After the last part's data the caller writes the close delimiter:
186/// [`Boundary::delimiter`] followed by `--\r\n`; for a body with no parts,
187/// `--`, the boundary, and `--\r\n`.
188///
189/// # Errors
190///
191/// `BufferTooSmall { needed }` (nothing is written);
192/// `Syntax { EmptyName | ControlInHeader | AmbiguousQuote | InvalidContentType
193/// | MalformedParameter }` (`MalformedParameter`: a name or file name that
194/// holds `name=`, which the parser refuses, D-136).
195///
196/// # Examples
197///
198/// ```
199/// let b = form_net::Boundary::new(b"X")?;
200/// let mut out = [0u8; 128];
201/// let n = form_net::encode_part_head(&b, true, "a\"b", None, None, &mut out)?;
202/// assert_eq!(&out[..n], b"--X\r\nContent-Disposition: form-data; name=\"a%22b\"\r\n\r\n");
203/// # Ok::<(), form_net::FormError>(())
204/// ```
205pub fn encode_part_head(
206    boundary: &Boundary,
207    first: bool,
208    name: &str,
209    filename: Option<&str>,
210    content_type: Option<&str>,
211    out: &mut [u8],
212) -> Result<usize, FormError> {
213    let needed = head_len(boundary, first, name, filename, content_type)?;
214    if out.len() < needed {
215        return Err(FormError::BufferTooSmall { needed });
216    }
217    let mut sink = SliceOut { out, pos: 0 };
218    put_head(&mut sink, boundary, first, name, filename, content_type)?;
219    Ok(sink.pos)
220}
221
222#[cfg(feature = "std")]
223pub use writer_std::MultipartWriter;
224
225#[cfg(feature = "std")]
226mod writer_std {
227    use alloc::string::String;
228    use core::fmt;
229    use std::io::{Read, Write};
230
231    use super::{Out, head_len, put_head};
232    use crate::error::{EncodingError, FormError, Location, SyntaxError};
233    use crate::multipart::boundary::Boundary;
234
235    struct IoOut<'w, W: Write>(&'w mut W);
236
237    impl<W: Write> Out for IoOut<'_, W> {
238        fn put(&mut self, bytes: &[u8]) -> Result<(), FormError> {
239            self.0
240                .write_all(bytes)
241                .map_err(|e| FormError::Io { kind: e.kind() })
242        }
243    }
244
245    /// Writes a `multipart/form-data` body as browsers do.
246    ///
247    /// Names and file names are escaped by the browser rule; every input
248    /// that could read back differently is refused; text line breaks become
249    /// CRLF. With a caller boundary, data is scanned for the delimiter
250    /// across chunk splits and refused if it contains one. So every body the
251    /// writer produces parses back with form-net to the same fields.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// let mut body = Vec::new();
257    /// // `MultipartWriter::new` (feature `random`) generates the boundary.
258    /// let boundary = form_net::Boundary::new(b"----form-net-example")?;
259    /// let mut w = form_net::MultipartWriter::with_boundary(&mut body, boundary);
260    /// w.text("title", "Holiday")?;
261    /// w.file("photo", "beach.jpg", Some("image/jpeg"), &b"\xff\xd8 jpeg"[..])?;
262    /// let content_type = w.content_type();
263    /// w.finish()?;
264    ///
265    /// let mp = form_net::parse_multipart(content_type.as_bytes(), &body)?;
266    /// assert_eq!(mp.field("title"), Some("Holiday"));
267    /// assert_eq!(mp.file("photo").map(|p| p.bytes()), Some(&b"\xff\xd8 jpeg"[..]));
268    /// # Ok::<(), form_net::FormError>(())
269    /// ```
270    pub struct MultipartWriter<W: Write> {
271        out: W,
272        boundary: Boundary,
273        verify: bool,
274        first: bool,
275        in_file: bool,
276        poisoned: bool,
277        /// The delimiter matcher's state over the current part's data.
278        k: usize,
279        /// The bare-break matcher's state (D-113).
280        m: usize,
281        /// Text normalisation: the last byte written was a CR.
282        pending_cr: bool,
283    }
284
285    impl<W: Write> fmt::Debug for MultipartWriter<W> {
286        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287            f.debug_struct("MultipartWriter")
288                .field("boundary", &self.boundary)
289                .field("in_file", &self.in_file)
290                .finish_non_exhaustive()
291        }
292    }
293
294    impl<W: Write> MultipartWriter<W> {
295        /// A writer with a generated boundary.
296        ///
297        /// # Errors
298        ///
299        /// `Random` when the OS random source fails.
300        ///
301        /// # Examples
302        ///
303        /// ```
304        /// let mut w = form_net::MultipartWriter::new(Vec::new())?;
305        /// w.text("title", "Holiday")?;
306        /// let content_type = w.content_type();
307        /// let body = w.finish()?;
308        /// let mp = form_net::parse_multipart(content_type.as_bytes(), &body)?;
309        /// assert_eq!(mp.field("title"), Some("Holiday"));
310        /// # Ok::<(), form_net::FormError>(())
311        /// ```
312        #[cfg(feature = "random")]
313        #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
314        pub fn new(out: W) -> Result<Self, FormError> {
315            let mut w = Self::with_boundary(out, Boundary::generate()?);
316            // 128 random bits: a collision with the data is negligible.
317            w.verify = false;
318            Ok(w)
319        }
320
321        /// A writer with the caller's boundary; data is scanned for it.
322        ///
323        /// # Examples
324        ///
325        /// ```
326        /// use form_net::{Boundary, MultipartWriter};
327        ///
328        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
329        /// w.text("a", "1")?;
330        /// let body = w.finish()?;
331        /// assert_eq!(body, b"--X\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\n1\r\n--X--\r\n");
332        /// # Ok::<(), form_net::FormError>(())
333        /// ```
334        pub fn with_boundary(out: W, boundary: Boundary) -> Self {
335            MultipartWriter {
336                out,
337                boundary,
338                verify: true,
339                first: true,
340                in_file: false,
341                poisoned: false,
342                k: 2,
343                m: 1,
344                pending_cr: false,
345            }
346        }
347
348        /// The boundary.
349        ///
350        /// # Examples
351        ///
352        /// ```
353        /// use form_net::{Boundary, MultipartWriter};
354        ///
355        /// let w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"form-net-x")?);
356        /// assert_eq!(w.boundary().as_bytes(), b"form-net-x");
357        /// # Ok::<(), form_net::FormError>(())
358        /// ```
359        pub const fn boundary(&self) -> &Boundary {
360            &self.boundary
361        }
362
363        /// The `Content-Type` header value to send:
364        /// `multipart/form-data; boundary=...`, quoted when the boundary
365        /// holds a character that is not a token character.
366        ///
367        /// # Examples
368        ///
369        /// ```
370        /// use form_net::{Boundary, MultipartWriter};
371        ///
372        /// let w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"abc")?);
373        /// assert_eq!(w.content_type(), "multipart/form-data; boundary=abc");
374        /// // A space is not a token character: the boundary is quoted.
375        /// let w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"a b")?);
376        /// assert_eq!(w.content_type(), "multipart/form-data; boundary=\"a b\"");
377        /// # Ok::<(), form_net::FormError>(())
378        /// ```
379        #[must_use]
380        pub fn content_type(&self) -> String {
381            let b = self.boundary.as_bytes();
382            let token = b.iter().all(|&c| parse_net::class::is_tchar(c));
383            let b = core::str::from_utf8(b).unwrap_or("");
384            if token {
385                alloc::format!("multipart/form-data; boundary={b}")
386            } else {
387                alloc::format!("multipart/form-data; boundary=\"{b}\"")
388            }
389        }
390
391        /// Turns the delimiter scan on or off. It is on for a caller
392        /// boundary and off for a generated one.
393        ///
394        /// # Examples
395        ///
396        /// ```
397        /// use form_net::{Boundary, MultipartWriter};
398        ///
399        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
400        /// w.verify_boundary(true);
401        /// let err = w.text("a", "1\r\n--X").unwrap_err();
402        /// assert_eq!(err.code(), "syntax.boundary_in_content");
403        /// # Ok::<(), form_net::FormError>(())
404        /// ```
405        pub fn verify_boundary(&mut self, on: bool) {
406            self.verify = on;
407        }
408
409        /// Writes a text field. Line breaks in `value` become CRLF. A refused
410        /// head writes nothing; after any error the output is incomplete and
411        /// must be discarded.
412        ///
413        /// # Errors
414        ///
415        /// The head errors of [`encode_part_head`](crate::encode_part_head);
416        /// `Syntax { BoundaryInContent }`; `Encoding { UnsupportedCharset }`
417        /// for a `_charset_` field naming anything but UTF-8 or US-ASCII
418        /// (the text after it would not read back); `Io`; `Poisoned`.
419        ///
420        /// # Examples
421        ///
422        /// ```
423        /// use form_net::{Boundary, MultipartWriter};
424        ///
425        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
426        /// w.text("note", "line 1\nline 2")?;
427        /// let body = w.finish()?;
428        /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", &body)?;
429        /// assert_eq!(mp.field("note"), Some("line 1\r\nline 2"));
430        /// # Ok::<(), form_net::FormError>(())
431        /// ```
432        pub fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
433            self.guard(|w| {
434                if name == "_charset_"
435                    && !(value.eq_ignore_ascii_case("utf-8")
436                        || value.eq_ignore_ascii_case("us-ascii"))
437                {
438                    return Err(FormError::encoding(
439                        EncodingError::UnsupportedCharset,
440                        Location::NONE,
441                    ));
442                }
443                w.head(name, None, None)?;
444                w.in_file = false;
445                w.pending_cr = false;
446                let bytes = value.as_bytes();
447                let mut from = 0usize;
448                for (i, &b) in bytes.iter().enumerate() {
449                    match b {
450                        b'\r' => {
451                            w.data(bytes.get(from..i).unwrap_or(&[]))?;
452                            w.data(b"\r\n")?;
453                            w.pending_cr = true;
454                            from = i.saturating_add(1);
455                        }
456                        b'\n' => {
457                            w.data(bytes.get(from..i).unwrap_or(&[]))?;
458                            if !w.pending_cr {
459                                w.data(b"\r\n")?;
460                            }
461                            w.pending_cr = false;
462                            from = i.saturating_add(1);
463                        }
464                        _ => w.pending_cr = false,
465                    }
466                }
467                w.data(bytes.get(from..).unwrap_or(&[]))
468            })
469        }
470
471        /// Starts a file part; its data follows through
472        /// [`file_data`](Self::file_data).
473        ///
474        /// # Errors
475        ///
476        /// As [`text`](Self::text), without the charset rule.
477        ///
478        /// # Examples
479        ///
480        /// ```
481        /// use form_net::{Boundary, MultipartWriter};
482        ///
483        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
484        /// w.file_start("doc", "a.txt", Some("text/plain"))?;
485        /// w.file_data(b"hello")?;
486        /// let body = w.finish()?;
487        /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", &body)?;
488        /// assert_eq!(mp.file("doc").map(|p| p.bytes()), Some(&b"hello"[..]));
489        /// # Ok::<(), form_net::FormError>(())
490        /// ```
491        pub fn file_start(
492            &mut self,
493            name: &str,
494            filename: &str,
495            content_type: Option<&str>,
496        ) -> Result<(), FormError> {
497            self.guard(|w| {
498                w.head(name, Some(filename), content_type)?;
499                w.in_file = true;
500                Ok(())
501            })
502        }
503
504        /// More data of the current file part.
505        ///
506        /// # Errors
507        ///
508        /// `Syntax { BoundaryInContent }`; `Io`; `Poisoned` outside a file
509        /// part.
510        ///
511        /// # Examples
512        ///
513        /// ```
514        /// use form_net::{Boundary, MultipartWriter};
515        ///
516        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
517        /// w.file_start("log", "log.txt", None)?;
518        /// for chunk in [&b"part one, "[..], b"part two"] {
519        ///     w.file_data(chunk)?;
520        /// }
521        /// let body = w.finish()?;
522        /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", &body)?;
523        /// assert_eq!(mp.file("log").map(|p| p.bytes()), Some(&b"part one, part two"[..]));
524        /// # Ok::<(), form_net::FormError>(())
525        /// ```
526        pub fn file_data(&mut self, chunk: &[u8]) -> Result<(), FormError> {
527            if !self.in_file {
528                self.poisoned = true;
529                return Err(FormError::Poisoned);
530            }
531            self.guard(|w| w.data(chunk))
532        }
533
534        /// A whole file part read from `data`. Returns its length.
535        ///
536        /// # Errors
537        ///
538        /// As [`file_start`](Self::file_start) and
539        /// [`file_data`](Self::file_data); `Io` from `data`.
540        ///
541        /// # Examples
542        ///
543        /// ```
544        /// use form_net::{Boundary, MultipartWriter};
545        ///
546        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
547        /// let len = w.file("photo", "beach.jpg", Some("image/jpeg"), &b"\xff\xd8 jpeg"[..])?;
548        /// assert_eq!(len, 7);
549        /// let body = w.finish()?;
550        /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", &body)?;
551        /// assert_eq!(mp.file("photo").map(|p| p.bytes()), Some(&b"\xff\xd8 jpeg"[..]));
552        /// # Ok::<(), form_net::FormError>(())
553        /// ```
554        pub fn file<R: Read>(
555            &mut self,
556            name: &str,
557            filename: &str,
558            content_type: Option<&str>,
559            mut data: R,
560        ) -> Result<u64, FormError> {
561            self.file_start(name, filename, content_type)?;
562            let mut buf = [0u8; 8192];
563            let mut total = 0u64;
564            let r = loop {
565                let n = match data.read(&mut buf) {
566                    Ok(0) => break Ok(total),
567                    Ok(n) => n,
568                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
569                    Err(e) => {
570                        self.poisoned = true;
571                        break Err(FormError::Io { kind: e.kind() });
572                    }
573                };
574                if let Err(e) = self.file_data(buf.get(..n).unwrap_or(&[])) {
575                    break Err(e);
576                }
577                total = total.saturating_add(n as u64);
578            };
579            crate::wipe::wipe(&mut buf);
580            r
581        }
582
583        /// Writes the close delimiter and returns the writer.
584        ///
585        /// # Errors
586        ///
587        /// `Io`; `Poisoned`.
588        ///
589        /// # Examples
590        ///
591        /// ```
592        /// use form_net::{Boundary, MultipartWriter};
593        ///
594        /// let mut w = MultipartWriter::with_boundary(Vec::new(), Boundary::new(b"X")?);
595        /// w.text("a", "1")?;
596        /// let body = w.finish()?;
597        /// assert!(body.ends_with(b"\r\n--X--\r\n"));
598        /// # Ok::<(), form_net::FormError>(())
599        /// ```
600        pub fn finish(mut self) -> Result<W, FormError> {
601            self.guard(|w| {
602                let mut out = IoOut(&mut w.out);
603                if !w.first {
604                    out.put(b"\r\n")?;
605                }
606                out.put(b"--")?;
607                out.put(w.boundary.as_bytes())?;
608                out.put(b"--\r\n")?;
609                w.out.flush().map_err(|e| FormError::Io { kind: e.kind() })
610            })?;
611            Ok(self.out)
612        }
613
614        /// Runs `f`; any error poisons the writer.
615        fn guard(
616            &mut self,
617            f: impl FnOnce(&mut Self) -> Result<(), FormError>,
618        ) -> Result<(), FormError> {
619            if self.poisoned {
620                return Err(FormError::Poisoned);
621            }
622            let r = f(self);
623            if r.is_err() {
624                self.poisoned = true;
625            }
626            r
627        }
628
629        fn head(
630            &mut self,
631            name: &str,
632            filename: Option<&str>,
633            content_type: Option<&str>,
634        ) -> Result<(), FormError> {
635            // Everything is checked before the first byte goes out.
636            head_len(&self.boundary, self.first, name, filename, content_type)?;
637            put_head(
638                &mut IoOut(&mut self.out),
639                &self.boundary,
640                self.first,
641                name,
642                filename,
643                content_type,
644            )?;
645            self.first = false;
646            // The data starts right after a CRLF: as if it had been seen.
647            self.k = 2;
648            self.m = 1;
649            Ok(())
650        }
651
652        /// Writes part data, scanning it for the delimiter, and for
653        /// `--boundary` after a lone CR or LF, when verifying: the parser
654        /// refuses both (D-113).
655        fn data(&mut self, bytes: &[u8]) -> Result<(), FormError> {
656            if self.verify {
657                let (k, m, found) = self.boundary.run(self.k, self.m, bytes);
658                if found != crate::multipart::boundary::Hit::None {
659                    return Err(FormError::syntax(
660                        SyntaxError::BoundaryInContent,
661                        Location::NONE,
662                    ));
663                }
664                self.k = k;
665                self.m = m;
666            }
667            IoOut(&mut self.out).put(bytes)
668        }
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    fn head(
677        first: bool,
678        name: &str,
679        filename: Option<&str>,
680        ct: Option<&str>,
681    ) -> Result<alloc::vec::Vec<u8>, FormError> {
682        let b = Boundary::new(b"X").unwrap();
683        let mut out = [0u8; 256];
684        let n = encode_part_head(&b, first, name, filename, ct, &mut out)?;
685        Ok(out[..n].to_vec())
686    }
687
688    #[test]
689    fn test_head_shape_and_escapes() {
690        assert_eq!(
691            head(false, "a\r\nb", Some("c\".txt"), Some("text/plain")).unwrap(),
692            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"
693        );
694        assert_eq!(
695            head(true, "t\tab", None, None).unwrap(),
696            b"--X\r\nContent-Disposition: form-data; name=\"t\tab\"\r\n\r\n"
697        );
698    }
699
700    #[test]
701    fn test_head_refusals() {
702        let refused = |r: Result<alloc::vec::Vec<u8>, FormError>| match r {
703            Err(FormError::Syntax { reason, .. }) => reason,
704            other => panic!("{other:?}"),
705        };
706        assert_eq!(refused(head(true, "", None, None)), SyntaxError::EmptyName);
707        assert_eq!(
708            refused(head(true, "a\0", None, None)),
709            SyntaxError::ControlInHeader
710        );
711        assert_eq!(
712            refused(head(true, "a\\b", None, None)),
713            SyntaxError::AmbiguousQuote
714        );
715        assert_eq!(
716            refused(head(true, "a%22", None, None)),
717            SyntaxError::AmbiguousQuote
718        );
719        assert_eq!(
720            refused(head(true, "a", Some("x\\"), None)),
721            SyntaxError::AmbiguousQuote
722        );
723        assert!(head(true, "a", Some("C:\\x\\y.txt"), None).is_ok());
724        assert_eq!(
725            refused(head(true, "a", None, Some("text/plain\r\nX: y"))),
726            SyntaxError::ControlInHeader
727        );
728        assert_eq!(
729            refused(head(true, "a", None, Some("not a type"))),
730            SyntaxError::InvalidContentType
731        );
732        assert_eq!(
733            refused(head(true, "a", None, Some("multipart/mixed; boundary=y"))),
734            SyntaxError::InvalidContentType
735        );
736        assert_eq!(
737            refused(head(true, "a", None, Some("text/plain; a=1; a=2"))),
738            SyntaxError::InvalidContentType
739        );
740    }
741
742    #[test]
743    fn test_head_buffer_too_small_writes_nothing() {
744        let b = Boundary::new(b"X").unwrap();
745        let mut out = [7u8; 10];
746        let e = encode_part_head(&b, true, "name", None, None, &mut out).unwrap_err();
747        assert_eq!(e, FormError::BufferTooSmall { needed: 52 });
748        assert!(out.iter().all(|&x| x == 7));
749    }
750}