1use assert_impl::assert_impl;
2use mime::Mime;
3use once_cell::sync::Lazy;
4use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
5use qiniu_http::{
6 header::{HeaderName, IntoHeaderName, CONTENT_TYPE},
7 HeaderMap, HeaderValue,
8};
9use qiniu_utils::{smallstr::SmallString, wrap_smallstr};
10use rand::random;
11use regex::Regex;
12use serde::{
13 de::{Deserialize, Deserializer, Error, Visitor},
14 ser::{Serialize, Serializer},
15};
16use smallvec::SmallVec;
17use std::{
18 borrow::{Borrow, BorrowMut, Cow},
19 collections::VecDeque,
20 ffi::OsStr,
21 fmt,
22 iter::FromIterator,
23 ops::{Deref, DerefMut, Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo},
24};
25
26#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct FileName {
29 inner: SmallString<[u8; 64]>,
30}
31wrap_smallstr!(FileName);
32
33#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct FieldName {
36 inner: SmallString<[u8; 16]>,
37}
38wrap_smallstr!(FieldName);
39
40#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41struct Boundary {
42 inner: SmallString<[u8; 32]>,
43}
44wrap_smallstr!(Boundary);
45
46type HeaderBuffer = SmallVec<[u8; 256]>;
47
48#[derive(Debug)]
101pub struct Multipart<P> {
102 boundary: Boundary,
103 fields: VecDeque<(FieldName, P)>,
104}
105
106#[derive(Debug)]
108pub struct Part<B> {
109 meta: PartMetadata,
110 body: B,
111}
112
113impl<P> Default for Multipart<P> {
114 #[inline]
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120impl<P> Multipart<P> {
121 #[inline]
123 pub fn new() -> Self {
124 Self {
125 boundary: gen_boundary(),
126 fields: Default::default(),
127 }
128 }
129
130 pub(super) fn boundary(&self) -> &str {
131 &self.boundary
132 }
133
134 #[inline]
136 #[must_use]
137 pub fn add_part(mut self, name: impl Into<FieldName>, part: P) -> Self {
138 self.fields.push_back((name.into(), part));
139 self
140 }
141}
142
143impl<P: Sync + Send> Multipart<P> {
144 #[allow(dead_code)]
145 fn ignore() {
146 assert_impl!(Send: Self);
147 assert_impl!(Sync: Self);
148 }
149}
150
151#[derive(Default, Debug)]
153pub struct PartMetadata {
154 headers: HeaderMap,
155 file_name: Option<FileName>,
156}
157
158impl PartMetadata {
159 #[inline]
161 #[must_use]
162 pub fn mime(self, mime: Mime) -> Self {
163 self.add_header(CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap())
164 }
165
166 #[inline]
168 #[must_use]
169 pub fn add_header(mut self, name: impl IntoHeaderName, value: impl Into<HeaderValue>) -> Self {
170 self.headers.insert(name, value.into());
171 self
172 }
173
174 #[inline]
176 #[must_use]
177 pub fn file_name(mut self, file_name: impl Into<FileName>) -> Self {
178 self.file_name = Some(file_name.into());
179 self
180 }
181}
182
183impl Extend<(HeaderName, HeaderValue)> for PartMetadata {
184 #[inline]
185 fn extend<T: IntoIterator<Item = (HeaderName, HeaderValue)>>(&mut self, iter: T) {
186 self.headers.extend(iter)
187 }
188}
189
190impl Extend<(Option<HeaderName>, HeaderValue)> for PartMetadata {
191 #[inline]
192 fn extend<T: IntoIterator<Item = (Option<HeaderName>, HeaderValue)>>(&mut self, iter: T) {
193 self.headers.extend(iter)
194 }
195}
196
197impl<B> Part<B> {
198 #[inline]
200 #[must_use]
201 pub fn metadata(mut self, metadata: PartMetadata) -> Self {
202 self.meta = metadata;
203 self
204 }
205}
206
207impl<B: Sync + Send> Part<B> {
208 #[allow(dead_code)]
209 fn ignore() {
210 assert_impl!(Send: Self);
211 assert_impl!(Sync: Self);
212 }
213}
214
215mod sync_part {
216 use super::*;
217 use std::{
218 fmt::{self, Debug},
219 fs::File,
220 io::{Cursor, Read, Result as IoResult},
221 mem::take,
222 path::Path,
223 };
224
225 enum SyncPartBodyInner<'a> {
226 Bytes(Cursor<Cow<'a, [u8]>>),
227 Stream(Box<dyn Read + 'a>),
228 }
229
230 impl Debug for SyncPartBodyInner<'_> {
231 #[inline]
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 match self {
234 Self::Bytes(bytes) => f.debug_tuple("Bytes").field(bytes).finish(),
235 Self::Stream(_) => f.debug_tuple("Stream").finish(),
236 }
237 }
238 }
239
240 #[derive(Debug)]
242 pub struct SyncPartBody<'a>(SyncPartBodyInner<'a>);
243
244 pub type SyncPart<'a> = Part<SyncPartBody<'a>>;
246
247 impl<'a> SyncPart<'a> {
248 #[inline]
250 #[must_use]
251 pub fn text(value: impl Into<Cow<'a, str>>) -> Self {
252 let bytes = match value.into() {
253 Cow::Borrowed(str) => Cow::Borrowed(str.as_bytes()),
254 Cow::Owned(string) => Cow::Owned(string.into_bytes()),
255 };
256 Self {
257 body: SyncPartBody(SyncPartBodyInner::Bytes(Cursor::new(bytes))),
258 meta: Default::default(),
259 }
260 }
261
262 #[inline]
264 #[must_use]
265 pub fn bytes(value: impl Into<Cow<'a, [u8]>>) -> Self {
266 Self {
267 body: SyncPartBody(SyncPartBodyInner::Bytes(Cursor::new(value.into()))),
268 meta: Default::default(),
269 }
270 }
271
272 #[inline]
274 #[must_use]
275 pub fn stream(value: impl Read + 'a) -> Self {
276 Self {
277 body: SyncPartBody(SyncPartBodyInner::Stream(Box::new(value))),
278 meta: Default::default(),
279 }
280 }
281
282 pub fn file_path<S: AsRef<OsStr> + ?Sized>(path: &S) -> IoResult<Self> {
284 let path = Path::new(path);
285 let file = File::open(path)?;
286 let mut metadata = PartMetadata::default().mime(mime_guess::from_path(path).first_or_octet_stream());
287 if let Some(file_name) = path.file_name() {
288 let file_name = match file_name.to_string_lossy() {
289 Cow::Borrowed(str) => FileName::from(str),
290 Cow::Owned(string) => FileName::from(string),
291 };
292 metadata = metadata.file_name(file_name);
293 }
294 Ok(SyncPart::stream(file).metadata(metadata))
295 }
296 }
297
298 pub type SyncMultipart<'a> = Multipart<SyncPart<'a>>;
300
301 impl<'a> SyncMultipart<'a> {
302 pub(in super::super) fn into_read(mut self) -> Box<dyn Read + 'a> {
303 if self.fields.is_empty() {
304 return Box::new(Cursor::new([]));
305 }
306
307 let (name, part) = self.fields.pop_front().unwrap();
308 let chain = Box::new(self.part_stream(&name, part)) as Box<dyn Read + 'a>;
309 let fields = take(&mut self.fields);
310 Box::new(
311 fields
312 .into_iter()
313 .fold(chain, |readable, (name, part)| {
314 Box::new(readable.chain(self.part_stream(&name, part))) as Box<dyn Read + 'a>
315 })
316 .chain(Cursor::new(b"--"))
317 .chain(Cursor::new(self.boundary.to_owned()))
318 .chain(Cursor::new(b"--\r\n")),
319 )
320 }
321
322 fn part_stream(&self, name: &str, part: SyncPart<'a>) -> impl Read + 'a {
323 Cursor::new(b"--")
324 .chain(Cursor::new(self.boundary.to_owned()))
325 .chain(Cursor::new(b"\r\n"))
326 .chain(Cursor::new(encode_headers(name, &part.meta)))
327 .chain(Cursor::new(b"\r\n\r\n"))
328 .chain(part.body)
329 .chain(Cursor::new(b"\r\n"))
330 }
331 }
332
333 impl Read for SyncPartBody<'_> {
334 #[inline]
335 fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
336 match &mut self.0 {
337 SyncPartBodyInner::Bytes(bytes) => bytes.read(buf),
338 SyncPartBodyInner::Stream(stream) => stream.read(buf),
339 }
340 }
341 }
342}
343pub use sync_part::{SyncMultipart, SyncPart, SyncPartBody};
344
345#[cfg(feature = "async")]
346mod async_part {
347 use super::*;
348 use async_std::{fs::File, path::Path};
349 use futures::io::{AsyncRead, AsyncReadExt, Cursor};
350 use std::{
351 fmt::{self, Debug},
352 io::Result as IoResult,
353 mem::take,
354 pin::Pin,
355 task::{Context, Poll},
356 };
357
358 type AsyncStream<'a> = Box<dyn AsyncRead + Send + Unpin + 'a>;
359
360 enum AsyncPartBodyInner<'a> {
361 Bytes(Cursor<Cow<'a, [u8]>>),
362 Stream(AsyncStream<'a>),
363 }
364
365 impl Debug for AsyncPartBodyInner<'_> {
366 #[inline]
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 match self {
369 Self::Bytes(bytes) => f.debug_tuple("Bytes").field(bytes).finish(),
370 Self::Stream(_) => f.debug_tuple("Stream").finish(),
371 }
372 }
373 }
374
375 #[derive(Debug)]
377 #[cfg_attr(feature = "docs", doc(cfg(feature = "async")))]
378 pub struct AsyncPartBody<'a>(AsyncPartBodyInner<'a>);
379
380 #[cfg_attr(feature = "docs", doc(cfg(feature = "async")))]
382 pub type AsyncPart<'a> = Part<AsyncPartBody<'a>>;
383
384 impl<'a> AsyncPart<'a> {
385 #[inline]
387 #[must_use]
388 pub fn text(value: impl Into<Cow<'a, str>>) -> Self {
389 let bytes = match value.into() {
390 Cow::Borrowed(slice) => Cow::Borrowed(slice.as_bytes()),
391 Cow::Owned(string) => Cow::Owned(string.into_bytes()),
392 };
393 Self {
394 body: AsyncPartBody(AsyncPartBodyInner::Bytes(Cursor::new(bytes))),
395 meta: Default::default(),
396 }
397 }
398
399 #[inline]
401 #[must_use]
402 pub fn bytes(value: impl Into<Cow<'a, [u8]>>) -> Self {
403 Self {
404 body: AsyncPartBody(AsyncPartBodyInner::Bytes(Cursor::new(value.into()))),
405 meta: Default::default(),
406 }
407 }
408
409 #[inline]
411 #[must_use]
412 pub fn stream(value: impl AsyncRead + Send + Unpin + 'a) -> Self {
413 Self {
414 body: AsyncPartBody(AsyncPartBodyInner::Stream(Box::new(value))),
415 meta: Default::default(),
416 }
417 }
418
419 pub async fn file_path<S: AsRef<OsStr> + ?Sized>(path: &S) -> IoResult<AsyncPart<'a>> {
421 let path = Path::new(path);
422 let file = File::open(&path).await?;
423 let mut metadata = PartMetadata::default().mime(mime_guess::from_path(path).first_or_octet_stream());
424 if let Some(file_name) = path.file_name() {
425 let file_name = match file_name.to_string_lossy() {
426 Cow::Borrowed(str) => FileName::from(str),
427 Cow::Owned(string) => FileName::from(string),
428 };
429 metadata = metadata.file_name(file_name);
430 }
431 Ok(AsyncPart::stream(file).metadata(metadata))
432 }
433 }
434
435 #[cfg_attr(feature = "docs", doc(cfg(feature = "async")))]
437 pub type AsyncMultipart<'a> = Multipart<AsyncPart<'a>>;
438
439 impl<'a> AsyncMultipart<'a> {
440 pub(in super::super) fn into_async_read(mut self) -> Box<dyn AsyncRead + Send + Unpin + 'a> {
441 if self.fields.is_empty() {
442 return Box::new(Cursor::new([]));
443 }
444
445 let (name, part) = self.fields.pop_front().unwrap();
446 let chain = Box::new(self.part_stream(&name, part)) as Box<dyn AsyncRead + Send + Unpin + 'a>;
447 let fields = take(&mut self.fields);
448 Box::new(
449 fields
450 .into_iter()
451 .fold(chain, |readable, (name, part)| {
452 Box::new(readable.chain(self.part_stream(&name, part)))
453 as Box<dyn AsyncRead + Send + Unpin + 'a>
454 })
455 .chain(Cursor::new(b"--"))
456 .chain(Cursor::new(self.boundary.to_owned()))
457 .chain(Cursor::new(b"--\r\n")),
458 )
459 }
460
461 fn part_stream(&self, name: &str, part: AsyncPart<'a>) -> impl AsyncRead + Send + Unpin + 'a {
462 Cursor::new(b"--")
463 .chain(Cursor::new(self.boundary.to_owned()))
464 .chain(Cursor::new(b"\r\n"))
465 .chain(Cursor::new(encode_headers(name, &part.meta)))
466 .chain(Cursor::new(b"\r\n\r\n"))
467 .chain(part.body)
468 .chain(Cursor::new(b"\r\n"))
469 }
470 }
471
472 impl AsyncRead for AsyncPartBody<'_> {
473 #[inline]
474 fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<IoResult<usize>> {
475 match &mut self.0 {
476 AsyncPartBodyInner::Bytes(bytes) => Pin::new(bytes).poll_read(cx, buf),
477 AsyncPartBodyInner::Stream(stream) => Pin::new(stream).poll_read(cx, buf),
478 }
479 }
480 }
481}
482
483#[cfg(feature = "async")]
484pub use async_part::{AsyncMultipart, AsyncPart, AsyncPartBody};
485
486fn gen_boundary() -> Boundary {
487 use std::fmt::Write;
488
489 let mut b = Boundary::with_capacity(32);
490 write!(b, "{:016x}{:016x}", random::<u64>(), random::<u64>()).unwrap();
491 b
492}
493
494fn encode_headers(name: &str, field: &PartMetadata) -> HeaderBuffer {
495 let mut buf = HeaderBuffer::from_slice(b"content-disposition: form-data; ");
496 buf.extend_from_slice(&format_parameter("name", name));
497 if let Some(file_name) = field.file_name.as_ref() {
498 buf.extend_from_slice(b"; ");
499 buf.extend_from_slice(format_file_name(file_name).as_bytes());
500 }
501 for (name, value) in field.headers.iter() {
502 buf.extend_from_slice(b"\r\n");
503 buf.extend_from_slice(name.as_str().as_bytes());
504 buf.extend_from_slice(b": ");
505 buf.extend_from_slice(value.as_bytes());
506 }
507 buf
508}
509
510fn format_file_name(filename: &str) -> FileName {
511 static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\\\|\"|\r|\n").unwrap());
512 let mut formatted = FileName::from("filename=\"");
513 let mut last_match = 0;
514 for m in REGEX.find_iter(filename) {
515 let begin = m.start();
516 let end = m.end();
517 formatted.push_str(&filename[last_match..begin]);
518 match &filename[begin..end] {
519 "\\" => formatted.push_str("\\\\"),
520 "\"" => formatted.push_str("\\\""),
521 "\r" => formatted.push_str("\\\r"),
522 "\n" => formatted.push_str("\\\n"),
523 _ => unreachable!(),
524 }
525 last_match = end;
526 }
527 formatted.push_str(&filename[last_match..]);
528 formatted.push_str("\"");
529 formatted
530}
531
532const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
533 .add(b' ')
534 .add(b'"')
535 .add(b'<')
536 .add(b'>')
537 .add(b'`')
538 .add(b'#')
539 .add(b'?')
540 .add(b'{')
541 .add(b'}')
542 .add(b'/')
543 .add(b'%');
544
545fn format_parameter(name: &str, value: &str) -> HeaderBuffer {
546 let legal_value = {
547 let mut buf = HeaderBuffer::new();
548 for chunk in utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET) {
549 buf.extend_from_slice(chunk.as_bytes());
550 }
551 buf
552 };
553 let mut formatted = HeaderBuffer::from_slice(name.as_bytes());
554 if value.len() == legal_value.len() {
555 formatted.extend_from_slice(b"=\"");
556 formatted.extend_from_slice(value.as_bytes());
557 formatted.extend_from_slice(b"\"");
558 } else {
559 formatted.extend_from_slice(b"*=utf-8''");
560 formatted.extend_from_slice(&legal_value);
561 };
562 formatted
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use mime::{APPLICATION_JSON, IMAGE_BMP};
569 use std::{
570 fs::File,
571 io::{Cursor, Result as IoResult, Write},
572 };
573 use tempfile::tempdir;
574
575 #[test]
576 fn test_gen_boundary() {
577 env_logger::builder().is_test(true).try_init().ok();
578
579 for _ in 0..5 {
580 assert_eq!(gen_boundary().len(), 32);
581 }
582 }
583
584 #[test]
585 fn test_header_percent_encoding() {
586 env_logger::builder().is_test(true).try_init().ok();
587
588 let name = "start%'\"\r\nßend";
589 let metadata = PartMetadata {
590 headers: {
591 let mut headers = HeaderMap::default();
592 headers.insert(CONTENT_TYPE, HeaderValue::from_str(APPLICATION_JSON.as_ref()).unwrap());
593 headers
594 },
595 file_name: Some(name.into()),
596 };
597
598 assert_eq!(
599 encode_headers(name, &metadata).as_ref(),
600 "content-disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend; filename=\"start%'\\\"\\\r\\\nßend\"\r\ncontent-type: application/json".as_bytes()
601 );
602 }
603
604 #[test]
605 fn test_multipart_into_read() -> IoResult<()> {
606 env_logger::builder().is_test(true).try_init().ok();
607
608 let tempdir = tempdir()?;
609 let temp_file_path = tempdir.path().join("fake-file.json");
610 let mut file = File::create(&temp_file_path)?;
611 file.write_all(b"{\"a\":\"b\"}\n")?;
612 drop(file);
613
614 let mut multipart = SyncMultipart::new()
615 .add_part("bytes1", SyncPart::bytes(b"part1".as_slice()))
616 .add_part("text1", SyncPart::text("value1"))
617 .add_part(
618 "text2",
619 SyncPart::text("value1").metadata(PartMetadata::default().mime(IMAGE_BMP)),
620 )
621 .add_part("reader1", SyncPart::stream(Cursor::new(b"value1")))
622 .add_part("reader2", SyncPart::file_path(&temp_file_path)?);
623 multipart.boundary = "boundary".into();
624
625 const EXPECTED: &str = "--boundary\r\n\
626 content-disposition: form-data; name=\"bytes1\"\r\n\r\n\
627 part1\r\n\
628 --boundary\r\n\
629 content-disposition: form-data; name=\"text1\"\r\n\r\n\
630 value1\r\n\
631 --boundary\r\n\
632 content-disposition: form-data; name=\"text2\"\r\n\
633 content-type: image/bmp\r\n\r\n\
634 value1\r\n\
635 --boundary\r\n\
636 content-disposition: form-data; name=\"reader1\"\r\n\r\n\
637 value1\r\n\
638 --boundary\r\n\
639 content-disposition: form-data; name=\"reader2\"; filename=\"fake-file.json\"\r\n\
640 content-type: application/json\r\n\r\n\
641 {\"a\":\"b\"}\n\r\n\
642 --boundary--\
643 \r\n";
644
645 let mut actual = String::new();
646 multipart.into_read().read_to_string(&mut actual)?;
647 assert_eq!(EXPECTED, actual);
648
649 tempdir.close()?;
650 Ok(())
651 }
652
653 #[cfg(feature = "async")]
654 #[async_std::test]
655 async fn test_multipart_into_async_read() -> IoResult<()> {
656 use async_std::{
657 fs::File,
658 io::{Cursor as AsyncCursor, ReadExt, WriteExt},
659 };
660
661 env_logger::builder().is_test(true).try_init().ok();
662
663 let tempdir = tempdir()?;
664 let temp_file_path = tempdir.path().join("fake-file.json");
665 let mut file = File::create(&temp_file_path).await?;
666 file.write_all(b"{\"a\":\"b\"}\n").await?;
667 file.flush().await?;
668 drop(file);
669
670 let mut multipart = AsyncMultipart::new()
671 .add_part("bytes1", AsyncPart::bytes(b"part1".as_slice()))
672 .add_part("text1", AsyncPart::text("value1"))
673 .add_part(
674 "text2",
675 AsyncPart::text("value1").metadata(PartMetadata::default().mime(IMAGE_BMP)),
676 )
677 .add_part("reader1", AsyncPart::stream(AsyncCursor::new(b"value1")))
678 .add_part("reader2", AsyncPart::file_path(&temp_file_path).await?);
679 multipart.boundary = "boundary".into();
680
681 const EXPECTED: &str = "--boundary\r\n\
682 content-disposition: form-data; name=\"bytes1\"\r\n\r\n\
683 part1\r\n\
684 --boundary\r\n\
685 content-disposition: form-data; name=\"text1\"\r\n\r\n\
686 value1\r\n\
687 --boundary\r\n\
688 content-disposition: form-data; name=\"text2\"\r\n\
689 content-type: image/bmp\r\n\r\n\
690 value1\r\n\
691 --boundary\r\n\
692 content-disposition: form-data; name=\"reader1\"\r\n\r\n\
693 value1\r\n\
694 --boundary\r\n\
695 content-disposition: form-data; name=\"reader2\"; filename=\"fake-file.json\"\r\n\
696 content-type: application/json\r\n\r\n\
697 {\"a\":\"b\"}\n\r\n\
698 --boundary--\
699 \r\n";
700
701 let mut actual = String::new();
702 multipart.into_async_read().read_to_string(&mut actual).await?;
703 assert_eq!(EXPECTED, actual);
704
705 tempdir.close()?;
706 Ok(())
707 }
708}