1use crate::error::{Error, Result};
4use crate::request::Request;
5use bytes::Bytes;
6use serde::de::DeserializeOwned;
7use std::collections::HashMap;
8use std::path::{Component, Path, PathBuf};
9
10#[derive(Debug, Clone)]
15pub struct Upload {
16 pub field: String,
17 pub filename: Option<String>,
18 pub content_type: Option<String>,
19 pub data: Bytes,
20}
21
22impl Upload {
23 pub fn size(&self) -> usize {
25 self.data.len()
26 }
27
28 pub fn extension(&self) -> Option<String> {
30 self.filename
31 .as_deref()
32 .and_then(|n| Path::new(n).extension())
33 .and_then(|e| e.to_str())
34 .map(|e| e.to_ascii_lowercase())
35 }
36
37 pub fn mime(&self) -> Option<&str> {
39 self.content_type
40 .as_deref()
41 .map(str::trim)
42 .filter(|s| !s.is_empty())
43 }
44
45 pub fn mime_type(&self) -> Option<String> {
47 self.mime()
48 .map(|m| m.split(';').next().unwrap_or(m).trim().to_ascii_lowercase())
49 .filter(|s| !s.is_empty())
50 }
51
52 pub fn validate(&self, rules: &UploadRules) -> Result<()> {
54 rules.check(self)
55 }
56
57 pub async fn save(&self, path: impl AsRef<Path>) -> Result<()> {
59 let path = path.as_ref();
60 if let Some(parent) = path.parent() {
61 if !parent.as_os_str().is_empty() {
62 tokio::fs::create_dir_all(parent)
63 .await
64 .map_err(|e| Error::Internal(e.to_string()))?;
65 }
66 }
67 tokio::fs::write(path, &self.data)
68 .await
69 .map_err(|e| Error::Internal(e.to_string()))
70 }
71
72 pub async fn save_in(&self, dir: impl AsRef<Path>, filename: &str) -> Result<PathBuf> {
74 let name = Path::new(filename);
75 if !is_safe_relative(name) {
76 return Err(Error::BadRequest("unsafe upload filename".into()));
77 }
78 let dest = dir.as_ref().join(name);
79 self.save(&dest).await?;
80 Ok(dest)
81 }
82
83 pub fn suggested_name(&self) -> &str {
85 self.filename
86 .as_deref()
87 .filter(|s| !s.is_empty())
88 .unwrap_or(self.field.as_str())
89 }
90}
91
92#[derive(Debug, Clone, Default)]
94pub struct UploadRules {
95 max_bytes: Option<usize>,
96 extensions: Vec<String>,
97 mimes: Vec<String>,
98}
99
100impl UploadRules {
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn max_bytes(mut self, n: usize) -> Self {
106 self.max_bytes = Some(n);
107 self
108 }
109
110 pub fn extensions<I, S>(mut self, exts: I) -> Self
111 where
112 I: IntoIterator<Item = S>,
113 S: AsRef<str>,
114 {
115 self.extensions = exts
116 .into_iter()
117 .map(|s| s.as_ref().trim_start_matches('.').to_ascii_lowercase())
118 .filter(|s| !s.is_empty())
119 .collect();
120 self
121 }
122
123 pub fn mimes<I, S>(mut self, mimes: I) -> Self
124 where
125 I: IntoIterator<Item = S>,
126 S: AsRef<str>,
127 {
128 self.mimes = mimes
129 .into_iter()
130 .map(|s| s.as_ref().trim().to_ascii_lowercase())
131 .filter(|s| !s.is_empty())
132 .collect();
133 self
134 }
135
136 fn check(&self, upload: &Upload) -> Result<()> {
137 if upload.size() == 0 {
138 return Err(Error::BadRequest("empty file".into()));
139 }
140 if let Some(max) = self.max_bytes {
141 if upload.size() > max {
142 return Err(Error::BadRequest(format!(
143 "file too large (max {max} bytes)"
144 )));
145 }
146 }
147 if !self.extensions.is_empty() {
148 let ext = upload
149 .extension()
150 .ok_or_else(|| Error::BadRequest("file extension required".into()))?;
151 if !self.extensions.iter().any(|e| e == &ext) {
152 return Err(Error::BadRequest(format!("invalid file extension `{ext}`")));
153 }
154 }
155 if !self.mimes.is_empty() {
156 let mime = upload
157 .mime_type()
158 .ok_or_else(|| Error::BadRequest("file content-type required".into()))?;
159 if !self.mimes.iter().any(|m| m == &mime) {
160 return Err(Error::BadRequest(format!("invalid content-type `{mime}`")));
161 }
162 }
163 Ok(())
164 }
165}
166
167#[derive(Debug, Clone, Default)]
169pub struct FormData {
170 texts: HashMap<String, Vec<String>>,
171 files: HashMap<String, Vec<Upload>>,
172}
173
174impl FormData {
175 pub fn get(&self, name: &str) -> Option<&str> {
176 self.texts.get(name)?.first().map(String::as_str)
177 }
178
179 pub fn get_all(&self, name: &str) -> &[String] {
180 self.texts.get(name).map(Vec::as_slice).unwrap_or(&[])
181 }
182
183 pub fn file(&self, name: &str) -> Option<&Upload> {
184 self.files.get(name)?.first()
185 }
186
187 pub fn files(&self, name: &str) -> &[Upload] {
188 self.files.get(name).map(Vec::as_slice).unwrap_or(&[])
189 }
190
191 pub fn text_map(&self) -> &HashMap<String, Vec<String>> {
192 &self.texts
193 }
194
195 pub fn file_map(&self) -> &HashMap<String, Vec<Upload>> {
196 &self.files
197 }
198
199 fn push_text(&mut self, name: String, value: String) {
200 self.texts.entry(name).or_default().push(value);
201 }
202
203 #[cfg(feature = "multipart")]
204 fn push_file(&mut self, upload: Upload) {
205 self.files
206 .entry(upload.field.clone())
207 .or_default()
208 .push(upload);
209 }
210
211 fn first_values(&self) -> HashMap<String, String> {
213 self.texts
214 .iter()
215 .filter_map(|(k, v)| v.first().cloned().map(|val| (k.clone(), val)))
216 .collect()
217 }
218}
219
220impl Request {
221 pub async fn input(&mut self) -> Result<&FormData> {
223 if self.get::<FormData>().is_some() {
224 return Ok(self.get::<FormData>().expect("FormData"));
225 }
226 let parsed = parse_form_data(self).await?;
227 self.set(parsed);
228 Ok(self.get::<FormData>().expect("FormData"))
229 }
230
231 pub async fn form<T: DeserializeOwned>(&mut self) -> Result<T> {
233 let data = self.input().await?;
234 let map = data.first_values();
235 let encoded = serde_urlencoded::to_string(&map)
236 .map_err(|e| Error::BadRequest(format!("form encode: {e}")))?;
237 serde_urlencoded::from_str(&encoded)
238 .map_err(|e| Error::BadRequest(format!("form error: {e}")))
239 }
240}
241
242async fn parse_form_data(req: &mut Request) -> Result<FormData> {
243 let ct = req.content_type().unwrap_or("").to_ascii_lowercase();
244 if ct.starts_with("multipart/") {
245 #[cfg(feature = "multipart")]
246 {
247 return parse_multipart(req).await;
248 }
249 #[cfg(not(feature = "multipart"))]
250 {
251 return Err(Error::BadRequest(
252 "multipart body requires the `multipart` feature".into(),
253 ));
254 }
255 }
256
257 let bytes = req.collect_body("form").await?;
259 let mut data = FormData::default();
260 if bytes.is_empty() {
261 return Ok(data);
262 }
263 let pairs: Vec<(String, String)> = serde_urlencoded::from_bytes(&bytes)
264 .map_err(|e| Error::BadRequest(format!("form error: {e}")))?;
265 for (k, v) in pairs {
266 data.push_text(k, v);
267 }
268 Ok(data)
269}
270
271#[cfg(feature = "multipart")]
272async fn parse_multipart(req: &mut Request) -> Result<FormData> {
273 use bytes::BytesMut;
274 use futures_util::stream;
275 use http_body_util::BodyExt;
276 use multer::Multipart;
277
278 let ct = req
279 .header("content-type")
280 .ok_or_else(|| Error::BadRequest("missing content-type".into()))?
281 .to_string();
282 let boundary = multer::parse_boundary(&ct)
283 .map_err(|e| Error::BadRequest(format!("multipart boundary: {e}")))?;
284
285 let limit = req.body_limit();
286 let mut body = req.into_body_stream_as("multipart")?;
287 let mut collected = BytesMut::new();
288 while let Some(frame) = body.frame().await {
289 let frame = frame.map_err(|e| Error::BadRequest(format!("multipart: {e}")))?;
290 if let Ok(chunk) = frame.into_data() {
291 if collected.len().saturating_add(chunk.len()) > limit {
292 return Err(Error::PayloadTooLarge);
293 }
294 collected.extend_from_slice(&chunk);
295 }
296 }
297 let bytes = collected.freeze();
298 req.body = crate::request::ReqBody::Bytes(bytes.clone());
300
301 let stream = stream::once(async move { Ok::<_, std::io::Error>(bytes) });
302 let mut mp = Multipart::new(stream, boundary);
303 let mut data = FormData::default();
304 while let Some(field) = mp
305 .next_field()
306 .await
307 .map_err(|e| Error::BadRequest(format!("multipart: {e}")))?
308 {
309 let name = field.name().unwrap_or("").to_string();
310 let filename = field.file_name().map(str::to_string);
311 let content_type = field.content_type().map(|m| m.to_string());
312 let part = field
313 .bytes()
314 .await
315 .map_err(|e| Error::BadRequest(format!("multipart field: {e}")))?;
316 if filename.is_some() {
317 data.push_file(Upload {
318 field: name,
319 filename,
320 content_type,
321 data: part,
322 });
323 } else {
324 let s = String::from_utf8_lossy(&part).into_owned();
325 data.push_text(name, s);
326 }
327 }
328 Ok(data)
329}
330
331fn is_safe_relative(path: &Path) -> bool {
332 !path.as_os_str().is_empty() && path.components().all(|c| matches!(c, Component::Normal(_)))
333}
334
335#[cfg(test)]
336mod upload_rules_tests {
337 use super::*;
338 use bytes::Bytes;
339
340 fn upload(name: &str, ct: Option<&str>, data: &'static [u8]) -> Upload {
341 Upload {
342 field: "file".into(),
343 filename: Some(name.into()),
344 content_type: ct.map(str::to_owned),
345 data: Bytes::from_static(data),
346 }
347 }
348
349 #[test]
350 fn helpers_extension_mime_size() {
351 let u = upload("Photo.PNG", Some("image/png; charset=binary"), b"abc");
352 assert_eq!(u.size(), 3);
353 assert_eq!(u.extension().as_deref(), Some("png"));
354 assert_eq!(u.mime_type().as_deref(), Some("image/png"));
355 }
356
357 #[test]
358 fn rejects_empty_and_oversized() {
359 let empty = upload("a.txt", None, b"");
360 assert!(empty.validate(&UploadRules::new()).is_err());
361
362 let big = upload("a.txt", None, b"hello");
363 assert!(big.validate(&UploadRules::new().max_bytes(4)).is_err());
364 assert!(big.validate(&UploadRules::new().max_bytes(5)).is_ok());
365 }
366
367 #[test]
368 fn extensions_and_mimes() {
369 let u = upload("a.JPG", Some("image/jpeg"), b"x");
370 assert!(u
371 .validate(&UploadRules::new().extensions(["png", "jpg"]))
372 .is_ok());
373 assert!(u.validate(&UploadRules::new().extensions(["png"])).is_err());
374 assert!(u
375 .validate(&UploadRules::new().mimes(["image/jpeg"]))
376 .is_ok());
377 assert!(u
378 .validate(&UploadRules::new().mimes(["image/png"]))
379 .is_err());
380 }
381}
382
383#[cfg(all(test, feature = "multipart"))]
384mod tests {
385 use super::*;
386 use crate::Request;
387 use bytes::Bytes;
388 use http::Method;
389
390 fn multipart_body(boundary: &str, parts: &str) -> Bytes {
391 Bytes::from(format!("--{boundary}\r\n{parts}--{boundary}--\r\n"))
392 }
393
394 fn multipart_req(boundary: &str, parts: &str) -> Request {
395 Request::builder()
396 .method(Method::POST)
397 .path("/upload")
398 .header(
399 "content-type",
400 format!("multipart/form-data; boundary={boundary}"),
401 )
402 .body(multipart_body(boundary, parts))
403 .build()
404 }
405
406 #[tokio::test]
407 async fn parses_text_and_file_fields() {
408 let boundary = "----sovaBound";
409 let parts = concat!(
410 "Content-Disposition: form-data; name=\"title\"\r\n\r\n",
411 "hello\r\n",
412 "------sovaBound\r\n",
413 "Content-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\n",
414 "Content-Type: text/plain\r\n\r\n",
415 "file-bytes\r\n",
416 );
417 let mut req = multipart_req(boundary, parts);
418 let data = req.input().await.unwrap();
419 assert_eq!(data.get("title"), Some("hello"));
420 let file = data.file("file").unwrap();
421 assert_eq!(file.filename.as_deref(), Some("a.txt"));
422 assert_eq!(file.data.as_ref(), b"file-bytes");
423 }
424
425 #[tokio::test]
426 async fn urlencoded_form_via_input() {
427 let mut req = Request::builder()
428 .method(Method::POST)
429 .path("/")
430 .header("content-type", "application/x-www-form-urlencoded")
431 .body("name=Ada&age=1")
432 .build();
433 #[derive(serde::Deserialize, Debug, PartialEq)]
434 struct Body {
435 name: String,
436 age: u32,
437 }
438 let body: Body = req.form().await.unwrap();
439 assert_eq!(
440 body,
441 Body {
442 name: "Ada".into(),
443 age: 1
444 }
445 );
446 }
447
448 #[tokio::test]
449 async fn missing_boundary_is_bad_request() {
450 let mut req = Request::builder()
451 .method(Method::POST)
452 .path("/")
453 .header("content-type", "multipart/form-data")
454 .body("x")
455 .build();
456 let err = req.input().await.unwrap_err();
457 assert!(matches!(err, Error::BadRequest(_)));
458 }
459
460 #[tokio::test]
461 async fn oversize_body_is_413() {
462 let boundary = "b";
463 let big = "x".repeat(64);
464 let parts = format!("Content-Disposition: form-data; name=\"f\"\r\n\r\n{big}\r\n");
465 let mut req = Request::builder()
466 .method(Method::POST)
467 .path("/")
468 .header(
469 "content-type",
470 format!("multipart/form-data; boundary={boundary}"),
471 )
472 .body(multipart_body(boundary, &parts))
473 .body_limit(16)
474 .build();
475 let err = req.input().await.unwrap_err();
476 assert!(matches!(err, Error::PayloadTooLarge), "got {err:?}");
477 }
478
479 #[tokio::test]
480 async fn broken_delimiter_is_bad_request() {
481 let mut req = Request::builder()
482 .method(Method::POST)
483 .path("/")
484 .header("content-type", "multipart/form-data; boundary=abc")
485 .body("not-a-multipart-body")
486 .build();
487 let err = req.input().await.unwrap_err();
488 assert!(matches!(err, Error::BadRequest(_)), "got {err:?}");
489 }
490}