1use std::fs::OpenOptions;
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::path::{Component, Path, PathBuf};
11use std::str::FromStr;
12
13use bytes::Bytes;
14use garde::Validate;
15use http::header::CONTENT_TYPE;
16use http_body_util::BodyDataStream;
17use mime::Mime;
18use serde::de::DeserializeOwned;
19use tempfile::SpooledTempFile;
20
21use crate::error::{Error, Result};
22use crate::extract::{FromRequest, RequestContext};
23
24const DEFAULT_MAX_BODY_SIZE: usize = 16 * 1024 * 1024;
26const DEFAULT_MAX_FILE_SIZE: usize = 8 * 1024 * 1024;
28const DEFAULT_MAX_TEXT_FIELD_SIZE: usize = 1024 * 1024;
30const DEFAULT_MEMORY_THRESHOLD: usize = 1024 * 1024;
32const DEFAULT_MAX_FILES: usize = 32;
34const DEFAULT_MAX_FIELDS: usize = 1000;
39const SPOOL_FLUSH_THRESHOLD: usize = 256 * 1024;
42
43#[derive(Clone, Default)]
49pub struct UploadConfig {
50 max_body_size: Option<usize>,
51 max_file_size: Option<usize>,
52 max_text_field_size: Option<usize>,
53 memory_threshold: Option<usize>,
54 max_files: Option<usize>,
55 max_fields: Option<usize>,
56 temp_dir: Option<PathBuf>,
57}
58
59impl UploadConfig {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn max_body_size(mut self, bytes: usize) -> Self {
67 self.max_body_size = Some(bytes);
68 self
69 }
70
71 pub fn max_body_size_mb(self, mb: usize) -> Self {
73 self.max_body_size(mb * 1024 * 1024)
74 }
75
76 pub fn max_file_size(mut self, bytes: usize) -> Self {
78 self.max_file_size = Some(bytes);
79 self
80 }
81
82 pub fn max_file_size_mb(self, mb: usize) -> Self {
84 self.max_file_size(mb * 1024 * 1024)
85 }
86
87 pub fn max_text_field_size(mut self, bytes: usize) -> Self {
89 self.max_text_field_size = Some(bytes);
90 self
91 }
92
93 pub fn max_text_field_size_kb(self, kb: usize) -> Self {
95 self.max_text_field_size(kb * 1024)
96 }
97
98 pub fn memory_threshold(mut self, bytes: usize) -> Self {
100 self.memory_threshold = Some(bytes);
101 self
102 }
103
104 pub fn max_files(mut self, count: usize) -> Self {
106 self.max_files = Some(count);
107 self
108 }
109
110 pub fn max_fields(mut self, count: usize) -> Self {
115 self.max_fields = Some(count);
116 self
117 }
118
119 pub fn temp_dir(mut self, dir: impl Into<PathBuf>) -> Self {
121 self.temp_dir = Some(dir.into());
122 self
123 }
124
125 pub(crate) fn merge(self, base: &UploadConfig) -> Self {
127 Self {
128 max_body_size: self.max_body_size.or(base.max_body_size),
129 max_file_size: self.max_file_size.or(base.max_file_size),
130 max_text_field_size: self.max_text_field_size.or(base.max_text_field_size),
131 memory_threshold: self.memory_threshold.or(base.memory_threshold),
132 max_files: self.max_files.or(base.max_files),
133 max_fields: self.max_fields.or(base.max_fields),
134 temp_dir: self.temp_dir.or_else(|| base.temp_dir.clone()),
135 }
136 }
137
138 fn resolve(&self) -> ResolvedConfig {
140 ResolvedConfig {
141 max_body_size: self.max_body_size.unwrap_or(DEFAULT_MAX_BODY_SIZE),
142 max_file_size: self.max_file_size.unwrap_or(DEFAULT_MAX_FILE_SIZE),
143 max_text_field_size: self
144 .max_text_field_size
145 .unwrap_or(DEFAULT_MAX_TEXT_FIELD_SIZE),
146 memory_threshold: self.memory_threshold.unwrap_or(DEFAULT_MEMORY_THRESHOLD),
147 max_files: self.max_files.unwrap_or(DEFAULT_MAX_FILES),
148 max_fields: self.max_fields.unwrap_or(DEFAULT_MAX_FIELDS),
149 temp_dir: self.temp_dir.clone(),
150 }
151 }
152}
153
154struct ResolvedConfig {
156 max_body_size: usize,
157 max_file_size: usize,
158 max_text_field_size: usize,
159 memory_threshold: usize,
160 max_files: usize,
161 max_fields: usize,
162 temp_dir: Option<PathBuf>,
163}
164
165impl ResolvedConfig {
166 fn new_spool(&self) -> SpooledTempFile {
171 match &self.temp_dir {
172 Some(dir) => tempfile::spooled_tempfile_in(self.memory_threshold, dir),
173 None => SpooledTempFile::new(self.memory_threshold),
174 }
175 }
176}
177
178#[derive(Clone)]
180pub(crate) struct AppUploadConfig(pub(crate) UploadConfig);
181
182pub struct FileBytes {
187 bytes: Bytes,
188 filename: Option<String>,
189 content_type: Option<Mime>,
190}
191
192impl FileBytes {
193 pub(crate) fn new(bytes: Bytes, filename: Option<String>, content_type: Option<Mime>) -> Self {
194 Self {
195 bytes,
196 filename,
197 content_type,
198 }
199 }
200
201 pub fn len(&self) -> usize {
203 self.bytes.len()
204 }
205
206 pub fn is_empty(&self) -> bool {
208 self.bytes.is_empty()
209 }
210
211 pub fn bytes(&self) -> &[u8] {
213 &self.bytes
214 }
215
216 pub fn into_bytes(self) -> Bytes {
218 self.bytes
219 }
220
221 pub fn filename(&self) -> Option<&str> {
223 self.filename.as_deref()
224 }
225
226 pub fn content_type(&self) -> Option<&Mime> {
228 self.content_type.as_ref()
229 }
230}
231
232pub struct UploadFile {
237 filename: Option<String>,
238 content_type: Option<Mime>,
239 size: u64,
240 storage: Option<SpooledTempFile>,
241}
242
243impl UploadFile {
244 pub(crate) fn new(
245 filename: Option<String>,
246 content_type: Option<Mime>,
247 size: u64,
248 storage: SpooledTempFile,
249 ) -> Self {
250 Self {
251 filename,
252 content_type,
253 size,
254 storage: Some(storage),
255 }
256 }
257
258 pub fn filename(&self) -> Option<&str> {
260 self.filename.as_deref()
261 }
262
263 pub fn content_type(&self) -> Option<&Mime> {
265 self.content_type.as_ref()
266 }
267
268 pub fn size(&self) -> u64 {
270 self.size
271 }
272
273 async fn with_storage<F, R>(&mut self, op: F) -> Result<R>
275 where
276 F: FnOnce(SpooledTempFile) -> std::io::Result<(SpooledTempFile, R)> + Send + 'static,
277 R: Send + 'static,
278 {
279 let storage = self
280 .storage
281 .take()
282 .ok_or_else(|| Error::internal("upload file storage was already consumed"))?;
283 let (storage, result) = tokio::task::spawn_blocking(move || op(storage))
284 .await
285 .map_err(|error| Error::internal(format!("upload IO task failed: {error}")))?
286 .map_err(|error| Error::internal(format!("upload IO error: {error}")))?;
287 self.storage = Some(storage);
288 Ok(result)
289 }
290
291 pub async fn read(&mut self) -> Result<Bytes> {
293 self.with_storage(|mut storage| {
294 storage.seek(SeekFrom::Start(0))?;
295 let mut buffer = Vec::new();
296 storage.read_to_end(&mut buffer)?;
297 Ok((storage, Bytes::from(buffer)))
298 })
299 .await
300 }
301
302 pub async fn read_chunk(&mut self, size: usize) -> Result<Option<Bytes>> {
304 self.with_storage(move |mut storage| {
305 let mut buffer = vec![0u8; size];
306 let read = storage.read(&mut buffer)?;
307 buffer.truncate(read);
308 let chunk = (read != 0).then(|| Bytes::from(buffer));
309 Ok((storage, chunk))
310 })
311 .await
312 }
313
314 pub async fn seek_start(&mut self) -> Result<()> {
316 self.with_storage(|mut storage| {
317 storage.seek(SeekFrom::Start(0))?;
318 Ok((storage, ()))
319 })
320 .await
321 }
322
323 pub async fn save_to<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
325 let path = path.as_ref().to_path_buf();
326 validate_save_path(&path)?;
327 self.write_to_path(path).await
328 }
329
330 pub async fn save_to_dir<P: AsRef<Path>>(
332 &mut self,
333 dir: P,
334 file_name: impl AsRef<str>,
335 ) -> Result<PathBuf> {
336 let dir = dir.as_ref().to_path_buf();
337 let file_name = file_name.as_ref().to_owned();
338 let target = build_safe_target(&dir, &file_name)?;
339 ensure_target_is_not_symlink(&target)?;
340 self.write_to_path(target.clone()).await?;
341 Ok(target)
342 }
343
344 async fn write_to_path(&mut self, path: PathBuf) -> Result<()> {
345 self.with_storage(move |mut storage| {
346 storage.seek(SeekFrom::Start(0))?;
347 let mut output = OpenOptions::new()
348 .write(true)
349 .create_new(true)
350 .open(&path)?;
351 std::io::copy(&mut storage, &mut output)?;
352 Ok((storage, ()))
353 })
354 .await
355 }
356}
357
358fn build_safe_target(dir: &Path, file_name: &str) -> Result<PathBuf> {
359 let name_path = Path::new(file_name);
360 let mut components = name_path.components();
361 let Some(Component::Normal(_)) = components.next() else {
362 return Err(
363 Error::bad_request("upload destination filename is not allowed")
364 .with_code("UPLOAD_PATH_INVALID"),
365 );
366 };
367 if components.next().is_some() {
368 return Err(
369 Error::bad_request("upload destination filename is not allowed")
370 .with_code("UPLOAD_PATH_INVALID"),
371 );
372 }
373 Ok(dir.join(file_name))
374}
375
376fn validate_save_path(path: &Path) -> Result<()> {
377 if path.is_absolute() {
378 return Err(Error::bad_request("upload destination path is not allowed")
379 .with_code("UPLOAD_PATH_INVALID"));
380 }
381
382 if path
383 .components()
384 .any(|component| matches!(component, Component::ParentDir))
385 {
386 return Err(Error::bad_request("upload destination path is not allowed")
387 .with_code("UPLOAD_PATH_INVALID"));
388 }
389
390 ensure_target_is_not_symlink(path)?;
391 Ok(())
392}
393
394fn ensure_target_is_not_symlink(path: &Path) -> Result<()> {
395 if let Ok(metadata) = std::fs::symlink_metadata(path) {
396 if metadata.file_type().is_symlink() {
397 return Err(Error::bad_request("upload destination path is not allowed")
398 .with_code("UPLOAD_PATH_SYMLINK"));
399 }
400 }
401 Ok(())
402}
403
404pub struct Form<T>(pub T);
408
409impl<T> Form<T> {
410 pub fn into_inner(self) -> T {
412 self.0
413 }
414}
415
416impl<T> FromRequest for Form<T>
417where
418 T: DeserializeOwned + Validate<Context = ()> + Send,
419{
420 fn from_request(
421 ctx: &RequestContext,
422 ) -> impl std::future::Future<Output = Result<Self>> + Send {
423 let taken = ctx.take_body();
424 let limit = crate::extract::body::configured_body_limit(ctx);
425 async move {
426 let bytes = crate::extract::body::read_body_capped_with(taken?, limit).await?;
427 let value: T = serde_urlencoded::from_bytes(&bytes)
428 .map_err(|_| Error::unprocessable("request body is not a valid form"))?;
429 value.validate().map_err(Error::from_garde_report)?;
430 Ok(Form(value))
431 }
432 }
433}
434
435pub trait FromMultipart: Sized {
440 fn from_multipart(
442 form: &mut MultipartForm,
443 ) -> impl std::future::Future<Output = Result<Self>> + Send;
444
445 fn form_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
450 let _ = generator;
451 schemars::json_schema!({ "type": "object" })
452 }
453}
454
455pub struct Multipart<T>(pub T);
457
458impl<T> Multipart<T> {
459 pub fn into_inner(self) -> T {
461 self.0
462 }
463}
464
465impl<T> FromRequest for Multipart<T>
466where
467 T: FromMultipart + Send,
468{
469 async fn from_request(ctx: &RequestContext) -> Result<Self> {
470 let mut form = __parse_multipart(ctx, UploadConfig::new()).await?;
471 let value = T::from_multipart(&mut form).await?;
472 Ok(Multipart(value))
473 }
474}
475
476#[doc(hidden)]
480pub struct FileRule {
481 pub max_size: Option<usize>,
482 pub content_types: &'static [&'static str],
483 pub sniff: bool,
484}
485
486#[doc(hidden)]
488pub fn __validate_file_bytes(file: &FileBytes, rule: &FileRule) -> Result<()> {
489 check_size(file.len(), rule)?;
490 check_declared_type(file.content_type(), rule)?;
491 if rule.sniff {
492 check_sniffed_type(file.bytes(), rule)?;
493 }
494 Ok(())
495}
496
497#[doc(hidden)]
499pub async fn __validate_upload(file: &mut UploadFile, rule: &FileRule) -> Result<()> {
500 check_size(file.size() as usize, rule)?;
501 check_declared_type(file.content_type(), rule)?;
502 if rule.sniff {
503 file.seek_start().await?;
504 let prefix = file.read_chunk(512).await?.unwrap_or_default();
505 file.seek_start().await?;
506 check_sniffed_type(&prefix, rule)?;
507 }
508 Ok(())
509}
510
511fn check_size(size: usize, rule: &FileRule) -> Result<()> {
513 if let Some(max) = rule.max_size {
514 if size > max {
515 return Err(
516 Error::payload_too_large("uploaded file is too large").with_code("FILE_TOO_LARGE")
517 );
518 }
519 }
520 Ok(())
521}
522
523fn check_declared_type(declared: Option<&Mime>, rule: &FileRule) -> Result<()> {
525 if rule.content_types.is_empty() {
526 return Ok(());
527 }
528 let allowed = declared
529 .map(|mime| {
530 rule.content_types
531 .iter()
532 .any(|allowed| allowed.eq_ignore_ascii_case(mime.essence_str()))
533 })
534 .unwrap_or(false);
535 if !allowed {
536 return Err(Error::unprocessable("unsupported file content type")
537 .with_code("UNSUPPORTED_MEDIA_TYPE"));
538 }
539 Ok(())
540}
541
542fn check_sniffed_type(bytes: &[u8], rule: &FileRule) -> Result<()> {
544 if rule.content_types.is_empty() {
545 return Ok(());
546 }
547 if let Some(kind) = infer::get(bytes) {
548 let detected = kind.mime_type();
549 if !rule
550 .content_types
551 .iter()
552 .any(|allowed| allowed.eq_ignore_ascii_case(detected))
553 {
554 return Err(
555 Error::unprocessable("file content does not match the declared type")
556 .with_code("UNSUPPORTED_MEDIA_TYPE"),
557 );
558 }
559 }
560 Ok(())
561}
562
563struct FilePart {
565 name: String,
566 filename: Option<String>,
567 content_type: Option<Mime>,
568 storage: SpooledTempFile,
569 size: u64,
570}
571
572#[doc(hidden)]
577pub struct MultipartForm {
578 texts: Vec<(String, String)>,
579 files: Vec<FilePart>,
580}
581
582impl MultipartForm {
583 pub(crate) async fn parse(ctx: &RequestContext, config: &UploadConfig) -> Result<Self> {
585 let resolved = config.resolve();
586
587 let content_type = ctx
588 .headers()
589 .get(CONTENT_TYPE)
590 .and_then(|value| value.to_str().ok())
591 .ok_or_else(|| Error::bad_request("missing Content-Type for multipart form"))?;
592 let boundary = multer::parse_boundary(content_type)
593 .map_err(|_| Error::bad_request("invalid or missing multipart boundary"))?;
594
595 let body = ctx.take_body()?;
596 let mut multipart = multer::Multipart::new(BodyDataStream::new(body), boundary);
597
598 let mut texts = Vec::new();
599 let mut files = Vec::new();
600 let mut total: usize = 0;
601 let mut fields_seen: usize = 0;
602
603 while let Some(mut field) = multipart.next_field().await.map_err(parse_error)? {
604 fields_seen += 1;
607 if fields_seen > resolved.max_fields {
608 return Err(Error::unprocessable("too many form fields")
609 .with_code("TOO_MANY_FIELDS"));
610 }
611 let name = field.name().map(str::to_owned).unwrap_or_default();
612 let filename = field.file_name().map(str::to_owned);
613 let content_type = field.content_type().cloned();
614
615 if filename.is_some() {
616 if files.len() >= resolved.max_files {
617 return Err(
618 Error::bad_request("too many file fields").with_code("TOO_MANY_FILES")
619 );
620 }
621 let mut storage = resolved.new_spool();
622 let mut size: u64 = 0;
623 let mut buffer: Vec<u8> = Vec::new();
626 while let Some(chunk) = field.chunk().await.map_err(parse_error)? {
627 size += chunk.len() as u64;
628 total += chunk.len();
629 if size as usize > resolved.max_file_size {
630 return Err(Error::payload_too_large("uploaded file is too large")
631 .with_code("FILE_TOO_LARGE"));
632 }
633 if total > resolved.max_body_size {
634 return Err(Error::payload_too_large("request body is too large"));
635 }
636 buffer.extend_from_slice(&chunk);
637 if buffer.len() >= SPOOL_FLUSH_THRESHOLD {
638 storage = spool_flush(storage, std::mem::take(&mut buffer), false).await?;
639 }
640 }
641 storage = spool_flush(storage, buffer, true).await?;
643 files.push(FilePart {
644 name,
645 filename,
646 content_type,
647 storage,
648 size,
649 });
650 } else {
651 let text = field.text().await.map_err(parse_error)?;
652 total += text.len();
653 if total > resolved.max_body_size {
654 return Err(Error::payload_too_large("request body is too large"));
655 }
656 if text.len() > resolved.max_text_field_size {
657 return Err(Error::payload_too_large("form field is too large")
658 .with_code("FIELD_TOO_LARGE"));
659 }
660 texts.push((name, text));
661 }
662 }
663
664 Ok(Self { texts, files })
665 }
666
667 #[doc(hidden)]
669 pub fn take_form_value<T: FromStr>(&mut self, name: &str) -> Result<Option<T>> {
670 let Some(pos) = self.texts.iter().position(|(field, _)| field == name) else {
671 if self.files.iter().any(|file| file.name == name) {
672 return Err(Error::unprocessable(format!(
673 "form field `{name}` is a file, not a text value"
674 )));
675 }
676 return Ok(None);
677 };
678 let (_, value) = self.texts.remove(pos);
679 value
680 .parse::<T>()
681 .map(Some)
682 .map_err(|_| Error::unprocessable(format!("form field `{name}` has an invalid value")))
683 }
684
685 #[doc(hidden)]
687 pub fn take_form_values<T: FromStr>(&mut self, name: &str) -> Result<Vec<T>> {
688 let mut values = Vec::new();
689 let mut index = 0;
690 while index < self.texts.len() {
691 if self.texts[index].0 == name {
692 let (_, value) = self.texts.remove(index);
693 let parsed = value.parse::<T>().map_err(|_| {
694 Error::unprocessable(format!("form field `{name}` has an invalid value"))
695 })?;
696 values.push(parsed);
697 } else {
698 index += 1;
699 }
700 }
701 Ok(values)
702 }
703
704 #[doc(hidden)]
706 pub async fn take_file_bytes(&mut self, name: &str) -> Result<Option<FileBytes>> {
707 let Some(pos) = self.files.iter().position(|file| file.name == name) else {
708 if self.texts.iter().any(|(field, _)| field == name) {
709 return Err(Error::unprocessable(format!(
710 "form field `{name}` is a text value, not a file"
711 )));
712 }
713 return Ok(None);
714 };
715 Ok(Some(file_part_into_bytes(self.files.remove(pos)).await?))
716 }
717
718 #[doc(hidden)]
720 pub async fn take_file_bytes_list(&mut self, name: &str) -> Result<Vec<FileBytes>> {
721 let mut parts = Vec::new();
722 let mut index = 0;
723 while index < self.files.len() {
724 if self.files[index].name == name {
725 parts.push(self.files.remove(index));
726 } else {
727 index += 1;
728 }
729 }
730 let mut out = Vec::with_capacity(parts.len());
731 for part in parts {
732 out.push(file_part_into_bytes(part).await?);
733 }
734 Ok(out)
735 }
736
737 #[doc(hidden)]
739 pub fn take_upload_file(&mut self, name: &str) -> Option<UploadFile> {
740 let pos = self.files.iter().position(|file| file.name == name)?;
741 Some(file_part_into_upload(self.files.remove(pos)))
742 }
743
744 #[doc(hidden)]
746 pub fn take_upload_file_list(&mut self, name: &str) -> Vec<UploadFile> {
747 let mut out = Vec::new();
748 let mut index = 0;
749 while index < self.files.len() {
750 if self.files[index].name == name {
751 out.push(file_part_into_upload(self.files.remove(index)));
752 } else {
753 index += 1;
754 }
755 }
756 out
757 }
758}
759
760#[doc(hidden)]
765pub async fn __parse_multipart(ctx: &RequestContext, route: UploadConfig) -> Result<MultipartForm> {
766 let app_default = ctx
767 .state()
768 .get::<AppUploadConfig>()
769 .map(|config| config.0.clone())
770 .unwrap_or_default();
771 let config = route.merge(&app_default);
772 MultipartForm::parse(ctx, &config).await
773}
774
775async fn file_part_into_bytes(part: FilePart) -> Result<FileBytes> {
777 let FilePart {
778 filename,
779 content_type,
780 mut storage,
781 ..
782 } = part;
783 let bytes = tokio::task::spawn_blocking(move || {
784 storage.seek(SeekFrom::Start(0))?;
785 let mut buffer = Vec::new();
786 storage.read_to_end(&mut buffer)?;
787 Ok::<_, std::io::Error>(Bytes::from(buffer))
788 })
789 .await
790 .map_err(|error| Error::internal(format!("upload IO task failed: {error}")))?
791 .map_err(|error| Error::internal(format!("upload IO error: {error}")))?;
792 Ok(FileBytes::new(bytes, filename, content_type))
793}
794
795fn file_part_into_upload(part: FilePart) -> UploadFile {
797 UploadFile::new(part.filename, part.content_type, part.size, part.storage)
798}
799
800fn parse_error(error: multer::Error) -> Error {
802 Error::bad_request(format!("multipart parse error: {error}"))
803}
804
805async fn spool_flush(
811 mut storage: SpooledTempFile,
812 data: Vec<u8>,
813 rewind: bool,
814) -> Result<SpooledTempFile> {
815 tokio::task::spawn_blocking(move || -> std::io::Result<SpooledTempFile> {
816 if !data.is_empty() {
817 storage.write_all(&data)?;
818 }
819 if rewind {
820 storage.seek(SeekFrom::Start(0))?;
821 }
822 Ok(storage)
823 })
824 .await
825 .map_err(|error| Error::internal(format!("spool worker failed: {error}")))?
826 .map_err(|error| Error::internal(format!("spool write failed: {error}")))
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832 use schemars::SchemaGenerator;
833
834 fn spooled(data: &[u8]) -> SpooledTempFile {
835 let mut storage = SpooledTempFile::new(1024 * 1024);
836 storage.write_all(data).unwrap();
837 storage.seek(SeekFrom::Start(0)).unwrap();
838 storage
839 }
840
841 #[test]
842 fn file_bytes_reports_size_and_contents() {
843 let file = FileBytes::new(
844 Bytes::from_static(b"hello"),
845 Some("a.txt".to_owned()),
846 Some("text/plain".parse().unwrap()),
847 );
848 assert_eq!(file.len(), 5);
849 assert!(!file.is_empty());
850 assert_eq!(file.bytes(), b"hello");
851 assert_eq!(file.filename(), Some("a.txt"));
852 assert_eq!(
853 file.content_type().map(Mime::essence_str),
854 Some("text/plain")
855 );
856
857 let bytes = FileBytes::new(Bytes::from_static(b"hello"), None, None).into_bytes();
858 assert_eq!(bytes, Bytes::from_static(b"hello"));
859 }
860
861 #[test]
862 fn upload_config_builders_and_defaults_resolve() {
863 let dir = tempfile::tempdir().unwrap();
864 let config = UploadConfig::new()
865 .max_body_size(12)
866 .max_file_size(8)
867 .memory_threshold(4)
868 .max_files(2)
869 .temp_dir(dir.path());
870 let resolved = config.resolve();
871 assert_eq!(resolved.max_body_size, 12);
872 assert_eq!(resolved.max_file_size, 8);
873 assert_eq!(resolved.memory_threshold, 4);
874 assert_eq!(resolved.max_files, 2);
875 assert_eq!(config.temp_dir.as_deref(), Some(dir.path()));
876
877 let defaults = UploadConfig::new().resolve();
878 assert_eq!(defaults.max_body_size, DEFAULT_MAX_BODY_SIZE);
879 assert_eq!(defaults.max_file_size, DEFAULT_MAX_FILE_SIZE);
880 assert_eq!(defaults.memory_threshold, DEFAULT_MEMORY_THRESHOLD);
881 assert_eq!(defaults.max_files, DEFAULT_MAX_FILES);
882 }
883
884 #[tokio::test]
885 async fn upload_file_reads_and_saves() {
886 let mut file = UploadFile::new(Some("a.bin".to_owned()), None, 5, spooled(b"hello"));
887 assert_eq!(file.size(), 5);
888 assert_eq!(file.filename(), Some("a.bin"));
889 assert_eq!(file.read().await.unwrap(), Bytes::from_static(b"hello"));
890
891 let dir = tempfile::tempdir().unwrap();
892 let path = file.save_to_dir(dir.path(), "out.bin").await.unwrap();
893 assert_eq!(std::fs::read(&path).unwrap(), b"hello");
894 }
895
896 #[tokio::test]
897 async fn upload_file_reads_in_chunks() {
898 let mut file = UploadFile::new(None, None, 4, spooled(b"abcd"));
899 assert_eq!(
900 file.read_chunk(2).await.unwrap(),
901 Some(Bytes::from_static(b"ab"))
902 );
903 assert_eq!(
904 file.read_chunk(2).await.unwrap(),
905 Some(Bytes::from_static(b"cd"))
906 );
907 assert_eq!(file.read_chunk(2).await.unwrap(), None);
908 file.seek_start().await.unwrap();
909 assert_eq!(file.read().await.unwrap(), Bytes::from_static(b"abcd"));
910 }
911
912 #[tokio::test]
913 async fn upload_validation_covers_sniff_and_declared_type_paths() {
914 let mut file = UploadFile::new(
915 Some("a.png".to_owned()),
916 Some("image/png".parse().unwrap()),
917 16,
918 spooled(b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR"),
919 );
920 let rule = FileRule {
921 max_size: Some(1024),
922 content_types: &["image/png"],
923 sniff: true,
924 };
925 __validate_upload(&mut file, &rule).await.unwrap();
926
927 let mut wrong = UploadFile::new(
928 Some("a.txt".to_owned()),
929 Some("text/plain".parse().unwrap()),
930 2,
931 spooled(b"hi"),
932 );
933 let error = __validate_upload(&mut wrong, &rule).await.unwrap_err();
934 assert_eq!(error.code(), "UNSUPPORTED_MEDIA_TYPE");
935 }
936
937 #[tokio::test]
938 async fn upload_file_reports_consumed_storage() {
939 let mut file = UploadFile {
940 filename: None,
941 content_type: None,
942 size: 0,
943 storage: None,
944 };
945 let error = file.read().await.unwrap_err();
946 assert_eq!(error.message(), "upload file storage was already consumed");
947 }
948
949 #[test]
950 fn config_merge_prefers_route_over_app() {
951 let app = UploadConfig::new().max_file_size_mb(10).max_files(5);
952 let route = UploadConfig::new().max_file_size_mb(50);
953 let merged = route.merge(&app);
954 assert_eq!(merged.resolve().max_file_size, 50 * 1024 * 1024);
955 assert_eq!(merged.resolve().max_files, 5);
956 }
957
958 #[test]
959 fn default_form_schema_is_permissive_object() {
960 let mut generator = SchemaGenerator::default();
961 let schema = TokenForm::form_schema(&mut generator);
962 let schema_json = serde_json::to_value(&schema).unwrap();
963 assert_eq!(schema_json["type"], "object");
964 }
965
966 use crate::extract::PathParams;
967 use crate::state::StateMap;
968 use std::sync::Arc;
969
970 fn ctx_with(content_type: &str, body: &[u8]) -> RequestContext {
971 let head = http::Request::builder()
972 .header(CONTENT_TYPE, content_type)
973 .body(())
974 .unwrap()
975 .into_parts()
976 .0;
977 let body = crate::body::box_body(http_body_util::Full::new(Bytes::copy_from_slice(body)));
978 RequestContext::new(head, PathParams::new(), Arc::new(StateMap::new()), body)
979 }
980
981 #[derive(serde::Deserialize, garde::Validate)]
982 struct Login {
983 #[garde(length(min = 1))]
984 username: String,
985 #[garde(skip)]
986 password: String,
987 }
988
989 #[tokio::test]
990 async fn form_parses_urlencoded_body() {
991 let ctx = ctx_with(
992 "application/x-www-form-urlencoded",
993 b"username=ada&password=secret",
994 );
995 let form = Form::<Login>::from_request(&ctx).await.unwrap();
996 assert_eq!(form.0.username, "ada");
997 assert_eq!(form.0.password, "secret");
998 let login = form.into_inner();
999 assert_eq!(login.username, "ada");
1000 }
1001
1002 struct TokenForm {
1003 token: String,
1004 }
1005
1006 impl FromMultipart for TokenForm {
1007 async fn from_multipart(form: &mut MultipartForm) -> Result<Self> {
1008 let token = form
1009 .take_form_value::<String>("token")?
1010 .ok_or_else(|| Error::unprocessable("missing token"))?;
1011 Ok(TokenForm { token })
1012 }
1013 }
1014
1015 #[tokio::test]
1016 async fn multipart_binds_a_text_field_and_a_file() {
1017 let body = "--X\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nabc123\r\n\
1018 --X\r\nContent-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\n\
1019 Content-Type: text/plain\r\n\r\nhello\r\n--X--\r\n";
1020 let ctx = ctx_with("multipart/form-data; boundary=X", body.as_bytes());
1021
1022 let bound = Multipart::<TokenForm>::from_request(&ctx).await.unwrap();
1024 assert_eq!(bound.0.token, "abc123");
1025 assert_eq!(bound.into_inner().token, "abc123");
1026 }
1027
1028 #[tokio::test(flavor = "current_thread")]
1029 async fn multipart_spool_flush_does_not_block_the_runtime() {
1030 use std::sync::atomic::{AtomicUsize, Ordering};
1031 use std::sync::Arc;
1032 use std::time::Duration;
1033
1034 let body = format!(
1035 "--X\r\nContent-Disposition: form-data; name=\"file\"; filename=\"big.bin\"\r\n\
1036 Content-Type: application/octet-stream\r\n\r\n{}\r\n--X--\r\n",
1037 "a".repeat(SPOOL_FLUSH_THRESHOLD * 2)
1038 );
1039 let ctx = ctx_with("multipart/form-data; boundary=X", body.as_bytes());
1040 let ticks = Arc::new(AtomicUsize::new(0));
1041 let tick_counter = ticks.clone();
1042 let ticker = tokio::spawn(async move {
1043 loop {
1044 tick_counter.fetch_add(1, Ordering::Relaxed);
1045 tokio::time::sleep(Duration::from_millis(1)).await;
1046 }
1047 });
1048
1049 let mut form = __parse_multipart(
1050 &ctx,
1051 UploadConfig::new()
1052 .memory_threshold(1)
1053 .max_file_size(body.len() + 1024),
1054 )
1055 .await
1056 .unwrap();
1057 assert!(form.take_upload_file("file").is_some());
1058 ticker.abort();
1059 assert!(ticks.load(Ordering::Relaxed) > 0);
1060 }
1061
1062 #[test]
1063 fn file_validation_enforces_size_type_and_sniff() {
1064 let png_bytes = Bytes::from_static(b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR");
1065 let png = FileBytes::new(
1066 png_bytes,
1067 Some("a.png".to_owned()),
1068 Some("image/png".parse().unwrap()),
1069 );
1070 let rule = FileRule {
1071 max_size: Some(1024),
1072 content_types: &["image/png"],
1073 sniff: true,
1074 };
1075 assert!(__validate_file_bytes(&png, &rule).is_ok());
1076
1077 let txt = FileBytes::new(
1079 Bytes::from_static(b"hi"),
1080 None,
1081 Some("text/plain".parse().unwrap()),
1082 );
1083 let only_png = FileRule {
1084 max_size: None,
1085 content_types: &["image/png"],
1086 sniff: false,
1087 };
1088 assert_eq!(
1089 __validate_file_bytes(&txt, &only_png).err().unwrap().code(),
1090 "UNSUPPORTED_MEDIA_TYPE"
1091 );
1092
1093 let big = FileBytes::new(Bytes::from(vec![0u8; 100]), None, None);
1095 let small_limit = FileRule {
1096 max_size: Some(10),
1097 content_types: &[],
1098 sniff: false,
1099 };
1100 assert_eq!(
1101 __validate_file_bytes(&big, &small_limit)
1102 .err()
1103 .unwrap()
1104 .code(),
1105 "FILE_TOO_LARGE"
1106 );
1107
1108 let fake = FileBytes::new(
1110 Bytes::from_static(b"GIF89a...."),
1111 None,
1112 Some("image/png".parse().unwrap()),
1113 );
1114 assert!(__validate_file_bytes(&fake, &rule).is_err());
1115
1116 let unrestricted = FileRule {
1117 max_size: None,
1118 content_types: &[],
1119 sniff: true,
1120 };
1121 assert!(__validate_file_bytes(&txt, &unrestricted).is_ok());
1122 }
1123
1124 #[tokio::test]
1125 async fn multipart_form_takes_files_and_values() {
1126 let body = "--X\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\nhi\r\n\
1127 --X\r\nContent-Disposition: form-data; name=\"count\"\r\n\r\n1\r\n\
1128 --X\r\nContent-Disposition: form-data; name=\"count\"\r\n\r\n2\r\n\
1129 --X\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.bin\"\r\n\r\nDATA\r\n\
1130 --X\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"b.bin\"\r\n\r\nMORE\r\n--X--\r\n";
1131 let ctx = ctx_with("multipart/form-data; boundary=X", body.as_bytes());
1132 let mut form = __parse_multipart(&ctx, UploadConfig::new()).await.unwrap();
1133
1134 let file = form
1135 .take_file_bytes("doc")
1136 .await
1137 .unwrap()
1138 .expect("file present");
1139 assert_eq!(file.bytes(), b"DATA");
1140 assert_eq!(file.filename(), Some("a.bin"));
1141 assert_eq!(
1142 form.take_form_value::<String>("note").unwrap(),
1143 Some("hi".to_owned())
1144 );
1145 assert_eq!(form.take_form_value::<String>("missing").unwrap(), None);
1146 assert_eq!(form.take_form_values::<u32>("count").unwrap(), vec![1, 2]);
1147 assert_eq!(
1148 form.take_form_values::<u32>("count").unwrap(),
1149 Vec::<u32>::new()
1150 );
1151
1152 let remaining = form.take_file_bytes_list("doc").await.unwrap();
1153 assert_eq!(remaining.len(), 1);
1154 assert_eq!(remaining[0].bytes(), b"MORE");
1155 assert!(form.take_file_bytes("doc").await.unwrap().is_none());
1156 assert!(form.take_upload_file("doc").is_none());
1157 assert!(form.take_upload_file_list("doc").is_empty());
1158 }
1159
1160 #[tokio::test]
1161 async fn multipart_form_rejects_invalid_values_and_limits() {
1162 let body = "--X\r\nContent-Disposition: form-data; name=\"count\"\r\n\r\nabc\r\n--X--\r\n";
1163 let ctx = ctx_with("multipart/form-data; boundary=X", body.as_bytes());
1164 let mut form = __parse_multipart(&ctx, UploadConfig::new()).await.unwrap();
1165 assert_eq!(
1166 form.take_form_value::<u32>("count").unwrap_err().kind(),
1167 crate::error::ErrorKind::Unprocessable
1168 );
1169
1170 let too_many = "--X\r\nContent-Disposition: form-data; name=\"a\"; filename=\"a.txt\"\r\n\r\nA\r\n\
1171 --X\r\nContent-Disposition: form-data; name=\"b\"; filename=\"b.txt\"\r\n\r\nB\r\n--X--\r\n";
1172 let ctx = ctx_with("multipart/form-data; boundary=X", too_many.as_bytes());
1173 let error = match __parse_multipart(&ctx, UploadConfig::new().max_files(1)).await {
1174 Ok(_) => panic!("expected too many files error"),
1175 Err(error) => error,
1176 };
1177 assert_eq!(error.code(), "TOO_MANY_FILES");
1178
1179 let oversized_text =
1180 "--X\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\nhello\r\n--X--\r\n";
1181 let ctx = ctx_with("multipart/form-data; boundary=X", oversized_text.as_bytes());
1182 let error = match __parse_multipart(&ctx, UploadConfig::new().max_body_size(3)).await {
1183 Ok(_) => panic!("expected payload too large"),
1184 Err(error) => error,
1185 };
1186 assert_eq!(error.kind(), crate::error::ErrorKind::PayloadTooLarge);
1187
1188 let oversized_file = "--X\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.txt\"\r\n\r\nhello\r\n--X--\r\n";
1189 let ctx = ctx_with("multipart/form-data; boundary=X", oversized_file.as_bytes());
1190 let error = match __parse_multipart(&ctx, UploadConfig::new().max_file_size(3)).await {
1191 Ok(_) => panic!("expected file too large"),
1192 Err(error) => error,
1193 };
1194 assert_eq!(error.code(), "FILE_TOO_LARGE");
1195 }
1196
1197 #[tokio::test]
1198 async fn multipart_parse_reports_content_type_errors() {
1199 let request = http::Request::builder().body(()).unwrap().into_parts().0;
1200 let body = crate::body::box_body(http_body_util::Full::new(Bytes::new()));
1201 let ctx = RequestContext::new(request, PathParams::new(), Arc::new(StateMap::new()), body);
1202 let error = match MultipartForm::parse(&ctx, &UploadConfig::new()).await {
1203 Ok(_) => panic!("expected missing content type"),
1204 Err(error) => error,
1205 };
1206 assert_eq!(error.message(), "missing Content-Type for multipart form");
1207
1208 let ctx = ctx_with("multipart/form-data", b"");
1209 let error = match MultipartForm::parse(&ctx, &UploadConfig::new()).await {
1210 Ok(_) => panic!("expected invalid boundary"),
1211 Err(error) => error,
1212 };
1213 assert_eq!(error.message(), "invalid or missing multipart boundary");
1214 }
1215
1216 #[tokio::test]
1217 async fn parse_multipart_merges_route_and_app_config() {
1218 let body = "--X\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.txt\"\r\n\r\nhello\r\n--X--\r\n";
1219 let head = http::Request::builder()
1220 .header(CONTENT_TYPE, "multipart/form-data; boundary=X")
1221 .body(())
1222 .unwrap()
1223 .into_parts()
1224 .0;
1225 let mut state = StateMap::new();
1226 state.insert(AppUploadConfig(UploadConfig::new().max_file_size(3)));
1227 let body = crate::body::box_body(http_body_util::Full::new(Bytes::copy_from_slice(
1228 body.as_bytes(),
1229 )));
1230 let ctx = RequestContext::new(head, PathParams::new(), Arc::new(state), body);
1231 let mut form = __parse_multipart(&ctx, UploadConfig::new().max_file_size(10))
1232 .await
1233 .unwrap();
1234 assert!(form.take_upload_file("doc").is_some());
1235 }
1236
1237 #[test]
1238 fn parse_error_includes_multipart_context() {
1239 let error = parse_error(multer::Error::IncompleteStream);
1240 assert!(error.message().starts_with("multipart parse error:"));
1241 }
1242}