1#![forbid(unsafe_code)]
2
3use std::fmt;
4use std::pin::Pin;
5use std::task::{Context, Poll};
6
7use actix_http::error::PayloadError;
8use actix_http::header::HeaderMap as ActixHeaderMap;
9use bytes::Bytes;
10use futures_util::{Stream, TryStreamExt};
11use http::{header, HeaderMap};
12use pin_project_lite::pin_project;
13
14pin_project! {
15 pub struct Multipart {
16 #[pin]
17 inner: actix_multipart::Multipart,
18 }
19}
20
21impl Multipart {
22 pub fn new<S>(headers: &HeaderMap, stream: S) -> Result<Self, MultipartError>
23 where
24 S: Stream<Item = Result<Bytes, Box<dyn std::error::Error>>> + 'static,
25 {
26 Self::boundary(headers)?;
27
28 let content_type = headers.get(&header::CONTENT_TYPE).unwrap().to_owned();
29
30 let mut headers = ActixHeaderMap::with_capacity(1);
31 headers.append(header::CONTENT_TYPE, content_type);
32
33 let stream = stream.map_err(|_| PayloadError::Io(std::io::ErrorKind::Other.into()));
34
35 Ok(Self {
36 inner: actix_multipart::Multipart::new(&headers, stream),
37 })
38 }
39
40 pub(crate) fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
41 let m = headers
42 .get(&header::CONTENT_TYPE)
43 .ok_or(MultipartError::UnsupportedContentType)?
44 .to_str()
45 .ok()
46 .and_then(|content_type| content_type.parse::<mime::Mime>().ok())
47 .ok_or(MultipartError::UnsupportedContentType)?;
48
49 if !(m.type_() == mime::MULTIPART && m.subtype() == mime::FORM_DATA) {
50 return Err(MultipartError::UnsupportedContentType);
51 }
52
53 m.get_param(mime::BOUNDARY)
54 .map(|boundary| boundary.as_str().to_owned())
55 .ok_or(MultipartError::UnsupportedContentType)
56 }
57}
58
59impl Stream for Multipart {
60 type Item = Result<Field, MultipartError>;
61
62 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
63 match self.project().inner.poll_next(cx) {
64 Poll::Pending => Poll::Pending,
65 Poll::Ready(None) => Poll::Ready(None),
66 Poll::Ready(Some(Ok(field))) => Poll::Ready(Some(Ok(Field::from_actix(field)))),
67 Poll::Ready(Some(Err(err))) => {
68 Poll::Ready(Some(Err(MultipartError::Other(err.into()))))
69 }
70 }
71 }
72}
73
74impl fmt::Debug for Multipart {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.debug_struct("Multipart").finish()
77 }
78}
79
80pin_project! {
81 pub struct Field {
82 #[pin]
83 inner: actix_multipart::Field,
84 headers: HeaderMap,
85 }
86}
87
88impl Field {
89 fn from_actix(field: actix_multipart::Field) -> Self {
90 Self {
91 headers: field
92 .headers()
93 .into_iter()
94 .map(|(k, v)| (k.to_owned(), v.to_owned()))
95 .collect(),
96 inner: field,
97 }
98 }
99
100 pub fn name(&self) -> &str {
101 self.inner.name()
102 }
103
104 pub fn filename(&self) -> Option<&str> {
105 self.inner.content_disposition().get_filename()
106 }
107
108 pub fn content_type(&self) -> &mime::Mime {
109 self.inner.content_type()
110 }
111
112 pub fn headers(&self) -> &HeaderMap {
113 &self.headers
114 }
115}
116
117impl Stream for Field {
118 type Item = Result<Bytes, MultipartError>;
119
120 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
121 match self.project().inner.poll_next(cx) {
122 Poll::Pending => Poll::Pending,
123 Poll::Ready(None) => Poll::Ready(None),
124 Poll::Ready(Some(Ok(data))) => Poll::Ready(Some(Ok(data))),
125 Poll::Ready(Some(Err(err))) => {
126 Poll::Ready(Some(Err(MultipartError::Other(err.into()))))
127 }
128 }
129 }
130}
131
132impl fmt::Debug for Field {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 f.debug_struct("Field").finish()
135 }
136}
137
138#[derive(Debug)]
139pub enum MultipartError {
140 UnsupportedContentType,
141 Other(Box<dyn std::error::Error>),
142}
143
144impl fmt::Display for MultipartError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 MultipartError::UnsupportedContentType => f.write_str("unsupported content type"),
148 MultipartError::Other(e) => {
149 write!(f, "error parsing `multipart/form-data` request ({})", e)
150 }
151 }
152 }
153}
154
155impl std::error::Error for MultipartError {
156 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157 match self {
158 MultipartError::UnsupportedContentType => None,
159 MultipartError::Other(e) => Some(e.as_ref()),
160 }
161 }
162}