actix_easy_multipart/
text.rs

1//! Deserializes a field from plain text.
2use crate::bytes::Bytes;
3use crate::{field_mime, FieldReader, Limits};
4use actix_multipart::Field;
5use actix_web::http::StatusCode;
6use actix_web::{web, Error, HttpRequest, ResponseError};
7use derive_more::{Deref, DerefMut, Display, Error};
8use futures_core::future::LocalBoxFuture;
9use futures_util::FutureExt;
10use serde::de::DeserializeOwned;
11use std::sync::Arc;
12
13/// Deserialize from plain text.
14///
15/// Internally this uses [`serde_plain`] for deserialization, which supports primitive types
16/// including strings, numbers, and simple enums.
17#[derive(Debug, Deref, DerefMut)]
18pub struct Text<T: DeserializeOwned>(pub T);
19
20impl<T: DeserializeOwned> Text<T> {
21    pub fn into_inner(self) -> T {
22        self.0
23    }
24}
25
26impl<'t, T: DeserializeOwned + 'static> FieldReader<'t> for Text<T> {
27    type Future = LocalBoxFuture<'t, Result<Self, crate::Error>>;
28
29    fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
30        async move {
31            let config = TextConfig::from_req(req);
32            let field_name = field.name().to_owned();
33
34            if config.validate_content_type {
35                let valid = if let Some(mime) = field_mime(&field) {
36                    mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN)
37                } else {
38                    // https://www.rfc-editor.org/rfc/rfc7578#section-4.4
39                    // content type defaults to text/plain, so None should be considered valid
40                    true
41                };
42                if !valid && config.validate_content_type {
43                    return Err(crate::Error::Field {
44                        field_name,
45                        source: config.map_error(req, TextError::ContentType),
46                    });
47                }
48            }
49
50            let bytes = Bytes::read_field(req, field, limits).await?;
51
52            let text =
53                std::str::from_utf8(bytes.data.as_ref()).map_err(|e| crate::Error::Field {
54                    field_name: field_name.clone(),
55                    source: config.map_error(req, TextError::Utf8Error(e)),
56                })?;
57
58            Ok(Text(serde_plain::from_str(text).map_err(|e| {
59                crate::Error::Field {
60                    field_name,
61                    source: config.map_error(req, TextError::Deserialize(e)),
62                }
63            })?))
64        }
65        .boxed_local()
66    }
67}
68
69#[derive(Debug, Display, Error)]
70#[non_exhaustive]
71pub enum TextError {
72    /// Utf8 error
73    #[display(fmt = "Utf8 decoding error: {}", _0)]
74    Utf8Error(std::str::Utf8Error),
75
76    /// Deserialize error
77    #[display(fmt = "Plain text deserialize error: {}", _0)]
78    Deserialize(serde_plain::Error),
79
80    /// Content type error
81    #[display(fmt = "Content type error")]
82    ContentType,
83}
84
85impl ResponseError for TextError {
86    fn status_code(&self) -> StatusCode {
87        StatusCode::BAD_REQUEST
88    }
89}
90
91/// Configuration for the [`Text`] field reader.
92#[derive(Clone)]
93pub struct TextConfig {
94    err_handler: Option<Arc<dyn Fn(TextError, &HttpRequest) -> Error + Send + Sync>>,
95    validate_content_type: bool,
96}
97
98const DEFAULT_CONFIG: TextConfig = TextConfig {
99    err_handler: None,
100    validate_content_type: true,
101};
102
103impl TextConfig {
104    pub fn error_handler<F>(mut self, f: F) -> Self
105    where
106        F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static,
107    {
108        self.err_handler = Some(Arc::new(f));
109        self
110    }
111
112    /// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
113    /// back to the default payload config.
114    fn from_req(req: &HttpRequest) -> &Self {
115        req.app_data::<Self>()
116            .or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
117            .unwrap_or(&DEFAULT_CONFIG)
118    }
119
120    fn map_error(&self, req: &HttpRequest, err: TextError) -> Error {
121        if let Some(err_handler) = self.err_handler.as_ref() {
122            (*err_handler)(err, req)
123        } else {
124            err.into()
125        }
126    }
127
128    /// Sets whether or not the field must have a valid `Content-Type` header to be parsed.
129    /// Note that an empty `Content-Type` is also accepted, as the multipart specification defines
130    /// `text/plain` as the default for text fields.
131    pub fn validate_content_type(mut self, validate_content_type: bool) -> Self {
132        self.validate_content_type = validate_content_type;
133        self
134    }
135}
136
137impl Default for TextConfig {
138    fn default() -> Self {
139        DEFAULT_CONFIG
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use crate::tests::send_form;
146    use crate::text::{Text, TextConfig};
147    use crate::MultipartForm;
148    use actix_multipart_rfc7578::client::multipart;
149    use actix_web::http::StatusCode;
150    use actix_web::{web, App, HttpResponse, Responder};
151    use std::io::Cursor;
152
153    #[derive(MultipartForm)]
154    struct TextForm {
155        number: Text<i32>,
156    }
157
158    async fn test_text_route(form: MultipartForm<TextForm>) -> impl Responder {
159        assert_eq!(*form.number, 1025);
160        HttpResponse::Ok().finish()
161    }
162
163    #[actix_rt::test]
164    async fn test_content_type_validation() {
165        let srv = actix_test::start(|| {
166            App::new()
167                .route("/", web::post().to(test_text_route))
168                .app_data(TextConfig::default().validate_content_type(true))
169        });
170
171        // Deny because wrong content type
172        let bytes = Cursor::new("1025");
173        let mut form = multipart::Form::default();
174        form.add_reader_file_with_mime("number", bytes, "", mime::APPLICATION_OCTET_STREAM);
175        let response = send_form(&srv, form, "/").await;
176        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
177
178        // Allow because correct content type
179        let bytes = Cursor::new("1025");
180        let mut form = multipart::Form::default();
181        form.add_reader_file_with_mime("number", bytes, "", mime::TEXT_PLAIN);
182        let response = send_form(&srv, form, "/").await;
183        assert_eq!(response.status(), StatusCode::OK);
184    }
185}