Skip to main content

http_multipart/
form.rs

1use core::{
2    pin::Pin,
3    task::{ready, Context, Poll},
4};
5
6use std::{borrow::Cow, io, vec};
7
8use bytes::{Bytes, BytesMut};
9use futures_core::stream::Stream;
10use http::header::HeaderValue;
11
12use super::MultipartError;
13
14type BoxedStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
15
16/// A builder for a `multipart/form-data` request body.
17///
18/// Add fields via [`Part`], then obtain the `Content-Type` header value with
19/// [`Form::content_type`] and the body stream with [`Form::into_stream`].
20///
21/// # Examples
22/// ```rust
23/// use http::Request;
24/// use http_multipart::{Form, Part};
25///
26/// // construct a form from parts
27/// let parts = vec![
28///     Part::text("username", "alice"),
29///     Part::binary("data", &b"hello"[..]).file_name("hello.bin"),
30/// ];
31/// let form = Form::new(parts);
32///
33/// // build a request with the form
34/// let req = Request::builder()
35///     .method("POST") // multipart is usually used with POST method
36///     .header("Content-Type", form.content_type()) // content type header must be set to form's content type
37///     .body(form) // form implements Stream, so it can be used directly as the body
38///     .unwrap();
39/// ```
40pub struct Form {
41    boundary: Box<[u8]>,
42    parts: vec::IntoIter<Part>,
43    state: StreamState,
44    buf: BytesMut,
45}
46
47/// A single field for [`Form`]
48pub struct Part {
49    name: String,
50    filename: Option<String>,
51    content_type: Cow<'static, str>,
52    body: BoxedStream,
53}
54
55impl Form {
56    /// Create a new form with an automatically generated boundary.
57    pub fn new(parts: Vec<Part>) -> Self {
58        Self::with_boundary(generate_boundary(), parts).unwrap()
59    }
60
61    /// Create a form with a caller-supplied boundary.
62    ///
63    /// Returns [`MultipartError::Boundary`] if `boundary` is empty or contains
64    /// `\r` / `\n` (which would break the wire format).
65    pub fn with_boundary(boundary: impl AsRef<[u8]>, parts: Vec<Part>) -> Result<Self, MultipartError> {
66        let b = boundary.as_ref();
67        if b.is_empty() || b.iter().any(|&c| c == b'\r' || c == b'\n') {
68            return Err(MultipartError::Boundary);
69        }
70        let mut prefixed = Vec::with_capacity(2 + b.len());
71        prefixed.extend_from_slice(b"--");
72        prefixed.extend_from_slice(b);
73        Ok(Self {
74            boundary: prefixed.into_boxed_slice(),
75            parts: parts.into_iter(),
76            state: StreamState::NextPart,
77            buf: BytesMut::new(),
78        })
79    }
80
81    /// The raw boundary bytes used by this form.
82    pub fn boundary(&self) -> &[u8] {
83        &self.boundary[2..]
84    }
85
86    /// The `Content-Type` header value that must be set on the outgoing request.
87    ///
88    /// Example: `multipart/form-data; boundary=0000000000001a2b`
89    pub fn content_type(&self) -> HeaderValue {
90        let boundary = self.boundary();
91        let mut v = BytesMut::with_capacity(30 + boundary.len());
92        v.extend_from_slice(b"multipart/form-data; boundary=");
93        v.extend_from_slice(boundary);
94        // Boundary is validated to be ASCII on construction, so this never panics.
95        HeaderValue::from_maybe_shared(v.freeze()).expect("boundary is valid ASCII")
96    }
97}
98
99impl Part {
100    /// plain-text field. field `Content-Type` is `text/plain; charset=utf-8`.
101    #[inline]
102    pub fn text(name: impl Into<String>, value: impl Into<String>) -> Self {
103        Self::binary(name, value.into()).content_type("text/plain; charset=utf-8")
104    }
105
106    /// fixed binary field. field `Content-Type` is `application/octet-stream`.
107    #[inline]
108    pub fn binary(name: impl Into<String>, body: impl Into<Bytes>) -> Self {
109        Self::stream(name, Once(Some(body.into())))
110    }
111
112    /// streaming binary field. field `Content-Type` is `application/octet-stream`.
113    ///
114    /// [`Stream`] trait is utilized for generic async streaming interface
115    pub fn stream<S>(name: impl Into<String>, stream: S) -> Self
116    where
117        S: Stream<Item = io::Result<Bytes>> + Send + 'static,
118    {
119        Self {
120            name: name.into(),
121            filename: None,
122            content_type: Cow::Borrowed("application/octet-stream"),
123            body: Box::pin(stream),
124        }
125    }
126
127    /// Attach a filename (sets the `filename` parameter in `Content-Disposition`).
128    pub fn file_name(mut self, filename: impl Into<String>) -> Self {
129        self.filename = Some(filename.into());
130        self
131    }
132
133    /// Override the `Content-Type` for this part.
134    pub fn content_type(mut self, ct: impl Into<Cow<'static, str>>) -> Self {
135        self.content_type = ct.into();
136        self
137    }
138
139    fn format_headers_into(&self, buf: &mut BytesMut) {
140        let (low, up) = self.body.size_hint();
141        let exact = Some(low) == up;
142
143        // Fixed bytes:
144        //   "Content-Disposition: form-data; name=\""  38
145        //   closing quote + CRLF                        3
146        //   "Content-Type: " + CRLF                    16
147        //   final blank CRLF                             2
148        //                                         total 59
149        let mut len = 59 + self.name.len() + self.content_type.len();
150        if let Some(fname) = &self.filename {
151            len += 13 + fname.len(); // "; filename=\"" (12) + closing quote (1)
152        }
153        if exact {
154            len += 38; // "Content-Length: " (16) + up to 20 digits + CRLF (2)
155        }
156        buf.reserve(len);
157
158        buf.extend_from_slice(b"Content-Disposition: form-data; name=\"");
159        buf.extend_from_slice(self.name.as_bytes());
160        buf.extend_from_slice(b"\"");
161
162        if let Some(fname) = &self.filename {
163            buf.extend_from_slice(b"; filename=\"");
164            buf.extend_from_slice(fname.as_bytes());
165            buf.extend_from_slice(b"\"");
166        }
167
168        buf.extend_from_slice(b"\r\n");
169
170        buf.extend_from_slice(b"Content-Type: ");
171        buf.extend_from_slice(self.content_type.as_bytes());
172        buf.extend_from_slice(b"\r\n");
173
174        if exact {
175            buf.extend_from_slice(b"Content-Length: ");
176            buf.extend_from_slice(format!("{low}").as_bytes());
177            buf.extend_from_slice(b"\r\n");
178        }
179
180        buf.extend_from_slice(b"\r\n");
181    }
182}
183
184struct Once(Option<Bytes>);
185
186impl Stream for Once {
187    type Item = io::Result<Bytes>;
188
189    #[inline]
190    fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
191        Poll::Ready(self.get_mut().0.take().map(Ok))
192    }
193
194    fn size_hint(&self) -> (usize, Option<usize>) {
195        let size = self.0.as_ref().map(|b| b.len()).unwrap_or(0);
196        (size, Some(size))
197    }
198}
199
200enum StreamState {
201    NextPart,
202    Body(BoxedStream),
203    Done,
204}
205
206impl Stream for Form {
207    type Item = io::Result<Bytes>;
208
209    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
210        let this = self.get_mut();
211        match &mut this.state {
212            StreamState::Done => Poll::Ready(None),
213            StreamState::NextPart => {
214                this.buf.reserve(this.boundary.len() + 4);
215                this.buf.extend_from_slice(&this.boundary);
216
217                this.state = match this.parts.next() {
218                    None => {
219                        this.buf.extend_from_slice(b"--\r\n");
220                        StreamState::Done
221                    }
222                    Some(part) => {
223                        this.buf.extend_from_slice(b"\r\n");
224                        part.format_headers_into(&mut this.buf);
225                        StreamState::Body(part.body)
226                    }
227                };
228
229                Poll::Ready(Some(Ok(this.buf.split().freeze())))
230            }
231            StreamState::Body(body) => {
232                let chunk = ready!(body.as_mut().poll_next(cx)).unwrap_or_else(|| {
233                    this.state = StreamState::NextPart;
234                    // the end of delimiter chunk is only 2 bytes.
235                    // downstream should try to buffer it to reduce fragmentation
236                    Ok(Bytes::from_static(b"\r\n"))
237                });
238
239                Poll::Ready(Some(chunk))
240            }
241        }
242    }
243}
244
245// Generate a boundary that is unique within the current process.
246//
247// The boundary is not cryptographically random.  For environments where the
248// boundary must not appear in the body (security-sensitive uploads), supply
249// your own random boundary via [`Form::with_boundary`].
250fn generate_boundary() -> Box<[u8]> {
251    use std::sync::atomic::{AtomicU64, Ordering};
252    static COUNTER: AtomicU64 = AtomicU64::new(0);
253    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
254    // XOR with a stack address for modest per-process entropy.
255    let salt = &n as *const u64 as u64;
256    let val = n ^ salt.wrapping_mul(0x9e3779b97f4a7c15);
257    format!("{val:016x}").into_bytes().into_boxed_slice()
258}
259
260#[cfg(test)]
261mod test {
262    use std::{convert::Infallible, pin::pin};
263
264    use bytes::Bytes;
265    use futures_util::{FutureExt, StreamExt};
266    use http::{header::CONTENT_TYPE, Method, Request};
267
268    use super::*;
269    use crate::multipart;
270
271    /// Drain a `Form` synchronously. Works because `Once` bodies are always `Poll::Ready`.
272    fn collect(mut form: Form) -> Vec<u8> {
273        let mut out = Vec::new();
274        loop {
275            match form.next().now_or_never() {
276                Some(Some(Ok(bytes))) => out.extend_from_slice(&bytes),
277                Some(None) => break,
278                Some(Some(Err(e))) => panic!("stream error: {e}"),
279                None => panic!("stream returned Poll::Pending unexpectedly"),
280            }
281        }
282        out
283    }
284
285    #[test]
286    fn empty_form() {
287        let form = Form::with_boundary("abc", vec![]).unwrap();
288        let body = collect(form);
289        assert_eq!(body, b"--abc--\r\n");
290    }
291
292    #[test]
293    fn single_text_field() {
294        // Part::text uses Once whose size_hint is exact, so Content-Length is emitted.
295        let form = Form::with_boundary("abc", vec![Part::text("field", "value")]).unwrap();
296        let body = collect(form);
297        assert_eq!(
298            body,
299            b"--abc\r\n\
300              Content-Disposition: form-data; name=\"field\"\r\n\
301              Content-Type: text/plain; charset=utf-8\r\n\
302              Content-Length: 5\r\n\
303              \r\n\
304              value\r\n\
305              --abc--\r\n"
306        );
307    }
308
309    #[test]
310    fn file_part() {
311        let part = Part::binary("upload", Bytes::from_static(b"data"))
312            .file_name("hello.bin")
313            .content_type("application/octet-stream");
314        let form = Form::with_boundary("abc", vec![part]).unwrap();
315        let body = collect(form);
316        assert_eq!(
317            body,
318            b"--abc\r\n\
319              Content-Disposition: form-data; name=\"upload\"; filename=\"hello.bin\"\r\n\
320              Content-Type: application/octet-stream\r\n\
321              Content-Length: 4\r\n\
322              \r\n\
323              data\r\n\
324              --abc--\r\n"
325        );
326    }
327
328    #[test]
329    fn roundtrip() {
330        // Encode with Form, then parse with Multipart and verify fields.
331        let parts = vec![
332            Part::text("name", "alice"),
333            Part::binary("file", Bytes::from_static(b"hello world"))
334                .file_name("hi.txt")
335                .content_type("text/plain"),
336        ];
337        let form = Form::with_boundary("testbound", parts).unwrap();
338
339        let content_type = form.content_type();
340        let body: Bytes = collect(form).into();
341
342        let mut req = Request::new(());
343        *req.method_mut() = Method::POST;
344        req.headers_mut().insert(CONTENT_TYPE, content_type);
345
346        let stream = futures_util::stream::once(async { Ok::<_, Infallible>(body) });
347        let mut mp = pin!(multipart(&req, stream).unwrap());
348
349        // Field 1: "name" = "alice"
350        {
351            let mut f1 = mp.try_next().now_or_never().unwrap().unwrap().unwrap();
352            assert_eq!(f1.name(), Some("name"));
353            assert_eq!(
354                f1.try_next().now_or_never().unwrap().unwrap().unwrap().as_ref(),
355                b"alice"
356            );
357            assert!(f1.try_next().now_or_never().unwrap().unwrap().is_none());
358        }
359
360        // Field 2: "file"
361        {
362            let mut f2 = mp.try_next().now_or_never().unwrap().unwrap().unwrap();
363            assert_eq!(f2.name(), Some("file"));
364            assert_eq!(f2.file_name(), Some("hi.txt"));
365            assert_eq!(f2.headers().get(CONTENT_TYPE).unwrap().as_bytes(), b"text/plain");
366            assert_eq!(
367                f2.try_next().now_or_never().unwrap().unwrap().unwrap().as_ref(),
368                b"hello world"
369            );
370            assert!(f2.try_next().now_or_never().unwrap().unwrap().is_none());
371        }
372
373        // No more fields.
374        assert!(mp.try_next().now_or_never().unwrap().unwrap().is_none());
375    }
376
377    #[test]
378    fn multi_part_delimiters() {
379        let form = Form::with_boundary(
380            "sep",
381            vec![Part::text("a", "1"), Part::text("b", "2"), Part::text("c", "3")],
382        )
383        .unwrap();
384        let body = collect(form);
385        // Each single-char value → Content-Length: 1.
386        let expected = b"--sep\r\n\
387            Content-Disposition: form-data; name=\"a\"\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 1\r\n\r\n1\r\n\
388            --sep\r\n\
389            Content-Disposition: form-data; name=\"b\"\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 1\r\n\r\n2\r\n\
390            --sep\r\n\
391            Content-Disposition: form-data; name=\"c\"\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 1\r\n\r\n3\r\n\
392            --sep--\r\n";
393        assert_eq!(body, expected);
394    }
395
396    #[test]
397    fn with_boundary_rejects_empty() {
398        assert!(matches!(Form::with_boundary("", vec![]), Err(MultipartError::Boundary)));
399    }
400
401    #[test]
402    fn with_boundary_rejects_newline() {
403        assert!(matches!(
404            Form::with_boundary("ab\ncd", vec![]),
405            Err(MultipartError::Boundary)
406        ));
407        assert!(matches!(
408            Form::with_boundary("ab\rcd", vec![]),
409            Err(MultipartError::Boundary)
410        ));
411    }
412
413    #[test]
414    fn content_type_header() {
415        let form = Form::with_boundary("myboundary", vec![]).unwrap();
416        assert_eq!(
417            form.content_type().as_bytes(),
418            b"multipart/form-data; boundary=myboundary"
419        );
420    }
421}