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
16pub struct Form {
41 boundary: Box<[u8]>,
42 parts: vec::IntoIter<Part>,
43 state: StreamState,
44 buf: BytesMut,
45}
46
47pub struct Part {
49 name: String,
50 filename: Option<String>,
51 content_type: Cow<'static, str>,
52 body: BoxedStream,
53}
54
55impl Form {
56 pub fn new(parts: Vec<Part>) -> Self {
58 Self::with_boundary(generate_boundary(), parts).unwrap()
59 }
60
61 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 pub fn boundary(&self) -> &[u8] {
83 &self.boundary[2..]
84 }
85
86 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 HeaderValue::from_maybe_shared(v.freeze()).expect("boundary is valid ASCII")
96 }
97}
98
99impl Part {
100 #[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 #[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 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 pub fn file_name(mut self, filename: impl Into<String>) -> Self {
129 self.filename = Some(filename.into());
130 self
131 }
132
133 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 let mut len = 59 + self.name.len() + self.content_type.len();
150 if let Some(fname) = &self.filename {
151 len += 13 + fname.len(); }
153 if exact {
154 len += 38; }
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 Ok(Bytes::from_static(b"\r\n"))
237 });
238
239 Poll::Ready(Some(chunk))
240 }
241 }
242 }
243}
244
245fn 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 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 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 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 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 {
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 {
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 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 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}