puzz_multipart/
lib.rs

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}