diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index 8303b8fd27..6d64603017 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -196,6 +196,20 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { <#ty as #_form::FromForm<'r>>::push_value(#ctxt, __f.shift()); }))) ) + .inner_mapper(MapperBuild::new() + .with_output(|_, output| quote! { + fn push_value_with_bytes( + __c: &mut Self::Context, + __f: #_form::ValueField<'r>, + __bytes: &'r [u8] + ) { + #output + } + }) + .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { + <#ty as #_form::FromForm<'r>>::push_value_with_bytes(#ctxt, __f.shift(), __bytes); + }))) + ) .inner_mapper(MapperBuild::new() .try_input_map(|mapper, input| { let (ctxt_ty, gen) = context_type(input)?; diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 731b414227..6854285695 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -70,6 +70,7 @@ tempfile = "3" async-trait = "0.1.43" async-stream = "0.3.2" multer = { version = "3.1.0", features = ["tokio-io"] } +encoding_rs = "0.8" tokio-stream = { version = "0.1.6", features = ["signal", "time"] } cookie = { version = "0.18", features = ["percent-encode"] } futures = { version = "0.3.30", default-features = false, features = ["std"] } diff --git a/core/lib/src/data/capped.rs b/core/lib/src/data/capped.rs index 804a42d486..ba2fed34c6 100644 --- a/core/lib/src/data/capped.rs +++ b/core/lib/src/data/capped.rs @@ -230,6 +230,15 @@ macro_rules! impl_strict_from_form_field_from_capped { Ok(capped.value) } + fn from_value_with_bytes(f: ValueField<'v>, bytes: &'v [u8]) -> Result<'v, Self> { + let capped = as FromFormField<'v>>::from_value_with_bytes(f, bytes)?; + if !capped.is_complete() { + Err((None, Some(capped.n.written)))?; + } + + Ok(capped.value) + } + async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { let capped = as FromFormField<'v>>::from_data(field); let capped = capped.await?; diff --git a/core/lib/src/form/context.rs b/core/lib/src/form/context.rs index e91b281f8d..117b735635 100644 --- a/core/lib/src/form/context.rs +++ b/core/lib/src/form/context.rs @@ -371,6 +371,15 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Contextual<'v, T> { T::push_value(val_ctxt, field); } + fn push_value_with_bytes( + (ref mut val_ctxt, ctxt): &mut Self::Context, + field: ValueField<'v>, + bytes: &'v [u8] + ) { + ctxt.values.entry(field.name.source()).or_default().push(field.value); + T::push_value_with_bytes(val_ctxt, field, bytes); + } + async fn push_data((ref mut val_ctxt, ctxt): &mut Self::Context, field: DataField<'v, '_>) { ctxt.data_fields.insert(field.name.source()); T::push_data(val_ctxt, field).await; diff --git a/core/lib/src/form/field.rs b/core/lib/src/form/field.rs index 98c157d603..6e88c04cc8 100644 --- a/core/lib/src/form/field.rs +++ b/core/lib/src/form/field.rs @@ -8,6 +8,8 @@ use crate::fs::FileName; /// Rocket preprocesses all form fields into either [`ValueField`]s or /// [`DataField`]s. All fields from url-encoded forms, and fields without /// Content-Types from multipart forms, are preprocessed as a `ValueField`. +/// Multipart value fields also supply their original bytes through +/// [`FromForm::push_value_with_bytes()`](crate::form::FromForm::push_value_with_bytes). #[derive(Debug, Clone)] pub struct ValueField<'r> { /// The (decoded) name of the form field. diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index bcb2a2414b..2419a2c2f2 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -319,14 +319,17 @@ impl<'r, T: FromForm<'r>> FromData<'r> for Form { type Error = Errors<'r>; async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { - use either::Either; + use crate::form::parser::Field; let mut parser = try_outcome!(Parser::new(req, data).await); let mut context = T::init(Options::Lenient); while let Some(field) = parser.next().await { match field { - Ok(Either::Left(value)) => T::push_value(&mut context, value), - Ok(Either::Right(data)) => T::push_data(&mut context, data).await, + Ok(Field::Value(value)) => T::push_value(&mut context, value), + Ok(Field::ValueWithBytes(value, bytes)) => { + T::push_value_with_bytes(&mut context, value, bytes); + } + Ok(Field::Data(data)) => T::push_data(&mut context, data).await, Err(e) => T::push_error(&mut context, e), } } diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index 2f9d443d8d..0353469e25 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -203,7 +203,8 @@ use crate::http::uncased::AsUncased; /// /// `FromForm` describes a push-based parser for Rocket's [field wire format]. /// Fields are preprocessed into either [`ValueField`]s or [`DataField`]s which -/// are then pushed to the parser in [`FromForm::push_value()`] or +/// are then pushed to the parser in [`FromForm::push_value()`], +/// [`FromForm::push_value_with_bytes()`], or /// [`FromForm::push_data()`], respectively. Both url-encoded forms and /// multipart forms are supported. All url-encoded form fields are preprocessed /// as [`ValueField`]s. Multipart form fields with Content-Types are processed @@ -539,6 +540,26 @@ pub trait FromForm<'r>: Send + Sized { /// Processes the value field `field`. fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>); + /// Processes a multipart value field with its original body bytes. + /// + /// `field.value` contains the decoded text, while `bytes` contains the + /// original bytes before text decoding, without multipart headers or + /// delimiters. Rocket calls this method instead of `push_value` for + /// multipart value fields. URL-encoded and manually parsed value fields + /// continue to use `push_value`. + /// + /// The default implementation calls [`Self::push_value()`] once. Wrappers + /// should forward this method to their inner form guard to preserve raw + /// bytes for guards such as [`TempFile`](crate::fs::TempFile). Forwarding + /// only `push_value` continues to work, but passes only decoded text. + fn push_value_with_bytes( + ctxt: &mut Self::Context, + field: ValueField<'r>, + _bytes: &'r [u8] + ) { + Self::push_value(ctxt, field) + } + /// Processes the data field `field`. async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>); @@ -621,6 +642,10 @@ impl<'v, T: FromForm<'v> + 'v> FromForm<'v> for Vec { T::push_value(this.context(&field.name), field.shift()); } + fn push_value_with_bytes(this: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + T::push_value_with_bytes(this.context(&field.name), field.shift(), bytes); + } + async fn push_data(this: &mut Self::Context, field: DataField<'v, '_>) { T::push_data(this.context(&field.name), field.shift()).await } @@ -728,6 +753,14 @@ impl<'v, K, V> MapContext<'v, K, V> } } + fn push_value_with_bytes(&mut self, field: ValueField<'v>, bytes: &'v [u8]) { + match self.push(field.name) { + Some(Either::Left(ctxt)) => K::push_value_with_bytes(ctxt, field.shift(), bytes), + Some(Either::Right(ctxt)) => V::push_value_with_bytes(ctxt, field.shift(), bytes), + _ => {} + } + } + async fn push_data(&mut self, field: DataField<'v, '_>) { match self.push(field.name) { Some(Either::Left(ctxt)) => K::push_data(ctxt, field.shift()).await, @@ -782,6 +815,10 @@ impl<'v, K, V> FromForm<'v> for HashMap ctxt.push_value(field); } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + ctxt.push_value_with_bytes(field, bytes); + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { ctxt.push_data(field).await; } @@ -805,6 +842,10 @@ impl<'v, K, V> FromForm<'v> for BTreeMap ctxt.push_value(field); } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + ctxt.push_value_with_bytes(field, bytes); + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { ctxt.push_data(field).await; } @@ -826,6 +867,10 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Option { T::push_value(ctxt, field) } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + T::push_value_with_bytes(ctxt, field, bytes) + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { T::push_data(ctxt, field).await } @@ -847,6 +892,10 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Result<'v, T> { T::push_value(ctxt, field) } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + T::push_value_with_bytes(ctxt, field, bytes) + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { T::push_data(ctxt, field).await } @@ -898,6 +947,14 @@ impl<'v, A: FromForm<'v>, B: FromForm<'v>> FromForm<'v> for (A, B) { } } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + match ctxt.context(field.name) { + Ok(Either::Left(ctxt)) => A::push_value_with_bytes(ctxt, field.shift(), bytes), + Ok(Either::Right(ctxt)) => B::push_value_with_bytes(ctxt, field.shift(), bytes), + Err(e) => ctxt.errors.push(e), + } + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { match ctxt.context(field.name) { Ok(Either::Left(ctxt)) => A::push_data(ctxt, field.shift()).await, @@ -931,6 +988,10 @@ impl<'v, T: FromForm<'v> + Sync> FromForm<'v> for Arc { T::push_value(ctxt, field) } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + T::push_value_with_bytes(ctxt, field, bytes) + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { T::push_data(ctxt, field).await } @@ -966,6 +1027,14 @@ macro_rules! impl_via_proxy { >::push_value(ctxt, field) } + fn push_value_with_bytes( + ctxt: &mut Self::Context, + field: ValueField<'v>, + bytes: &'v [u8] + ) { + >::push_value_with_bytes(ctxt, field, bytes) + } + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { >::push_data(ctxt, field).await } diff --git a/core/lib/src/form/from_form_field.rs b/core/lib/src/form/from_form_field.rs index 2a7f5ab22b..7cf3e14335 100644 --- a/core/lib/src/form/from_form_field.rs +++ b/core/lib/src/form/from_form_field.rs @@ -188,6 +188,35 @@ pub trait FromFormField<'v>: Send + Sized { Err(field.unexpected())? } + /// Parses a multipart value field with its original body bytes. + /// + /// `field.value` contains the decoded text. `bytes` contains the original + /// bytes before text decoding, without multipart headers or delimiters. + /// The default implementation calls [`Self::from_value()`] once. Override + /// this method to preserve binary contents in multipart value fields. + /// URL-encoded and manually parsed values still use `from_value`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::{self, FromFormField, ValueField}; + /// + /// struct Bytes<'r>(&'r [u8]); + /// + /// impl<'r> FromFormField<'r> for Bytes<'r> { + /// fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> { + /// Ok(Bytes(field.value.as_bytes())) + /// } + /// + /// fn from_value_with_bytes(_: ValueField<'r>, bytes: &'r [u8]) -> form::Result<'r, Self> { + /// Ok(Bytes(bytes)) + /// } + /// } + /// ``` + fn from_value_with_bytes(field: ValueField<'v>, _bytes: &'v [u8]) -> Result<'v, Self> { + Self::from_value(field) + } + /// Parse a value of `T` from a form data field. /// /// The default implementation returns an error of @@ -256,6 +285,13 @@ impl<'v, T: FromFormField<'v>> FromForm<'v> for T { } } + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + if ctxt.should_push() { + ctxt.field_value = Some(field.value); + ctxt.push(field.name, Self::from_value_with_bytes(field, bytes)) + } + } + async fn push_data(ctxt: &mut FromFieldContext<'v, T>, field: DataField<'v, '_>) { if ctxt.should_push() { ctxt.push(field.name, Self::from_data(field).await); @@ -349,6 +385,10 @@ impl<'v> FromFormField<'v> for Capped<&'v [u8]> { Ok(Capped::from(field.value.as_bytes())) } + fn from_value_with_bytes(_: ValueField<'v>, bytes: &'v [u8]) -> Result<'v, Self> { + Ok(Capped::from(bytes)) + } + async fn from_data(f: DataField<'v, '_>) -> Result<'v, Self> { use crate::data::{Capped, Outcome, FromData}; diff --git a/core/lib/src/form/lenient.rs b/core/lib/src/form/lenient.rs index a07baaf441..6265eea4f1 100644 --- a/core/lib/src/form/lenient.rs +++ b/core/lib/src/form/lenient.rs @@ -87,6 +87,11 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Lenient { T::push_value(ctxt, field) } + #[inline(always)] + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + T::push_value_with_bytes(ctxt, field, bytes) + } + #[inline(always)] async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { T::push_data(ctxt, field).await diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs index be9130a4e1..bfc30b33da 100644 --- a/core/lib/src/form/parser.rs +++ b/core/lib/src/form/parser.rs @@ -1,5 +1,7 @@ +use std::borrow::Cow; + use multer::Multipart; -use either::Either; +use encoding_rs::{Encoding, UTF_8}; use crate::request::{Request, local_cache_once}; use crate::data::{Data, Limits, Outcome}; @@ -8,11 +10,16 @@ use crate::form::prelude::*; type Result<'r, T> = std::result::Result>; -type Field<'r, 'i> = Either, DataField<'r, 'i>>; +pub enum Field<'r, 'i> { + Value(ValueField<'r>), + ValueWithBytes(ValueField<'r>, &'r [u8]), + Data(DataField<'r, 'i>), +} pub struct MultipartParser<'r, 'i> { request: &'r Request<'i>, buffer: &'r SharedStack, + bytes: &'r SharedStack>, source: Multipart<'r>, done: bool, } @@ -77,6 +84,7 @@ impl<'r, 'i> Parser<'r, 'i> { Ok(Parser::Multipart(MultipartParser { request: req, buffer: local_cache_once!(req, SharedStack::new()), + bytes: local_cache_once!(req, SharedStack::new()), source: Multipart::with_reader_with_constraints(stream, boundary, constraints), done: false, })) @@ -85,7 +93,7 @@ impl<'r, 'i> Parser<'r, 'i> { pub async fn next(&mut self) -> Option>> { match self { Parser::Multipart(ref mut p) => p.next().await, - Parser::RawStr(ref mut p) => p.next().map(|f| Ok(Either::Left(f))) + Parser::RawStr(ref mut p) => p.next().map(|f| Ok(Field::Value(f))) } } } @@ -181,7 +189,7 @@ impl<'r, 'i> MultipartParser<'r, 'i> { } }; - Either::Right(DataField { + Field::Data(DataField { content_type, request: self.request, name: NameView::new(name), @@ -189,18 +197,24 @@ impl<'r, 'i> MultipartParser<'r, 'i> { data: Data::from(field), }) } else { - let (mut buf, len) = match field.name() { - Some(s) => (s.to_string(), s.len()), - None => (String::new(), 0) + let name = self.buffer.push(field.name().unwrap_or("")); + let encoding = field.content_type() + .and_then(|mime| mime.get_param("charset")) + .and_then(|charset| Encoding::for_label(charset.as_str().as_bytes())) + .unwrap_or(UTF_8); + let bytes = match field.bytes().await { + Ok(bytes) => self.bytes.push(bytes.to_vec()), + Err(e) => return Some(Err(e.into())), }; - match field.text().await { - Ok(text) => buf.push_str(&text), - Err(e) => return Some(Err(e.into())), + // Match multer's text decoding, including BOM handling, while + // retaining the original bytes for binary value guards. + let value = match encoding.decode(bytes).0 { + Cow::Borrowed(value) => value, + Cow::Owned(value) => self.buffer.push(value), }; - let name_val = self.buffer.push_split(buf, len); - Either::Left(ValueField::from(name_val)) + Field::ValueWithBytes(ValueField::from((name, value)), bytes) }; Some(Ok(field)) diff --git a/core/lib/src/form/strict.rs b/core/lib/src/form/strict.rs index 1c5b55acde..ad8d6f415b 100644 --- a/core/lib/src/form/strict.rs +++ b/core/lib/src/form/strict.rs @@ -102,6 +102,11 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Strict { T::push_value(ctxt, field) } + #[inline(always)] + fn push_value_with_bytes(ctxt: &mut Self::Context, field: ValueField<'v>, bytes: &'v [u8]) { + T::push_value_with_bytes(ctxt, field, bytes) + } + #[inline(always)] async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { T::push_data(ctxt, field).await diff --git a/core/lib/src/fs/temp_file.rs b/core/lib/src/fs/temp_file.rs index 0464dd46ce..ff4d59ae9d 100644 --- a/core/lib/src/fs/temp_file.rs +++ b/core/lib/src/fs/temp_file.rs @@ -21,6 +21,13 @@ use either::Either; /// when the `TempFile` handle is dropped unless it is persisted with /// [`TempFile::persist_to()`] or copied with [`TempFile::copy_to()`]. /// +/// Multipart value fields (those without a `Content-Type`) are buffered in +/// memory and preserve their original bytes, including invalid UTF-8 and byte +/// order marks. Like other value fields, they are subject to the `data-form` +/// limit instead of the individual `file` limit. Custom form wrappers must +/// forward [`FromForm::push_value_with_bytes()`](crate::form::FromForm::push_value_with_bytes) +/// to preserve these bytes when passing a value field to `TempFile`. +/// /// # Hazards /// /// Temporary files are cleaned by system file cleaners periodically. While an @@ -542,6 +549,11 @@ impl<'v> FromFormField<'v> for Capped> { Ok(Capped::new(TempFile::Buffered { content: field.value.as_bytes() }, n)) } + fn from_value_with_bytes(_: ValueField<'v>, bytes: &'v [u8]) -> Result> { + let n = N { written: bytes.len() as u64, complete: true }; + Ok(Capped::new(TempFile::Buffered { content: bytes }, n)) + } + async fn from_data( f: DataField<'v, '_> ) -> Result> { diff --git a/core/lib/tests/multipart-no-content-type-2934.rs b/core/lib/tests/multipart-no-content-type-2934.rs new file mode 100644 index 0000000000..04930cc7c2 --- /dev/null +++ b/core/lib/tests/multipart-no-content-type-2934.rs @@ -0,0 +1,371 @@ +#[macro_use] extern crate rocket; + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use rocket::data::{Capped, Limits, ToByteUnit}; +use rocket::form::{self, Contextual, DataField, Form, FromFormField, Lenient, Strict, ValueField}; +use rocket::fs::TempFile; +use rocket::http::{ContentType, Status}; +use rocket::local::blocking::Client; + +#[derive(FromForm)] +struct Upload<'r> { + file: TempFile<'r>, +} + +#[post("/", data = "")] +async fn upload(mut upload: Form>) -> Vec { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("received"); + upload.file.persist_to(&path).await.unwrap(); + std::fs::read(path).unwrap() +} + +fn check_upload(filename: bool, content_type: bool) { + let bytes: Vec = (0..=255).collect(); + check_bytes(filename, content_type, &bytes); +} + +fn check_bytes(filename: bool, content_type: bool, bytes: &[u8]) { + let client = Client::debug_with(routes![upload]).unwrap(); + let mut body = b"--boundary\r\nContent-Disposition: form-data; name=\"file\"".to_vec(); + if filename { + body.extend_from_slice(b"; filename=\"test.bin\""); + } + + body.extend_from_slice(b"\r\n"); + if content_type { + body.extend_from_slice(b"Content-Type: application/octet-stream\r\n"); + } + + body.extend_from_slice(b"\r\n"); + body.extend_from_slice(bytes); + body.extend_from_slice(b"\r\n--boundary--\r\n"); + let response = client.post("/") + .header(ContentType::new("multipart", "form-data").with_params([("boundary", "boundary")])) + .body(body) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_bytes().unwrap(), bytes); +} + +#[test] +fn filename_without_content_type() { + check_upload(true, false); +} + +#[test] +fn no_filename_or_content_type() { + check_upload(false, false); +} + +#[test] +fn filename_with_content_type() { + check_upload(true, true); +} + +#[test] +fn content_type_without_filename() { + check_upload(false, true); +} + +#[test] +fn utf8_bom_without_content_type() { + check_bytes(false, false, b"\xef\xbb\xbfhello"); +} + +#[test] +fn utf16_bom_without_content_type() { + check_bytes(false, false, b"\xff\xfeh\0i\0"); +} + +#[derive(FromForm)] +struct TextFields<'r> { + number: u32, + enabled: bool, + text: &'r str, +} + +#[post("/text", data = "")] +fn text(fields: Form>) -> String { + format!("{}:{}:{}", fields.number, fields.enabled, fields.text) +} + +#[test] +fn ordinary_headerless_text_fields() { + let client = Client::debug_with(routes![text]).unwrap(); + let body = [ + "--boundary", + "Content-Disposition: form-data; name=\"number\"", + "", + "42", + "--boundary", + "Content-Disposition: form-data; name=\"enabled\"", + "", + "yes", + "--boundary", + "Content-Disposition: form-data; name=\"text\"", + "", + "Grüße + %20", + "--boundary--", + "", + ].join("\r\n"); + let response = client.post("/text") + .header(ContentType::new("multipart", "form-data").with_params([("boundary", "boundary")])) + .body(body) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "42:true:Grüße + %20"); +} + +fn multipart(fields: &[(&str, &[u8])], content_type: bool) -> Vec { + let mut body = Vec::new(); + for (name, bytes) in fields { + body.extend_from_slice(format!("--boundary\r\n\ + Content-Disposition: form-data; name=\"{name}\"\r\n").as_bytes()); + if content_type { + body.extend_from_slice(b"Content-Type: application/octet-stream\r\n"); + } + + body.extend_from_slice(b"\r\n"); + body.extend_from_slice(bytes); + body.extend_from_slice(b"\r\n"); + } + + body.extend_from_slice(b"--boundary--\r\n"); + body +} + +fn multipart_type() -> ContentType { + ContentType::new("multipart", "form-data").with_params([("boundary", "boundary")]) +} + +#[test] +fn empty_and_text_uploads() { + for bytes in [b"".as_slice(), b"plain text", "Grüße".as_bytes()] { + check_bytes(false, false, bytes); + check_bytes(true, false, bytes); + } +} + +#[derive(FromForm)] +struct Composed<'r> { + files: Vec>, + map: HashMap>, + keys: BTreeMap<&'r [u8], &'r [u8]>, + pair: (TempFile<'r>, &'r [u8]), + optional: Option>, + result: form::Result<'r, TempFile<'r>>, + arc: Arc>, + strict: Strict>, + lenient: Lenient>, + capped: Capped>, + capped_bytes: Capped<&'r [u8]>, + range: std::ops::Range<&'r [u8]>, + context: Contextual<'r, Upload<'r>>, + #[field(name = uncased("renamed"))] + alias: &'r [u8], + decoded: &'r str, +} + +#[post("/composed", data = "
")] +async fn composed(form: Form>) { + use rocket::tokio::io::AsyncReadExt; + + let form = form.into_inner(); + let bytes = b"\xef\xbb\xbfhello"; + assert_eq!(form.decoded, "hello"); + assert_eq!(form.files.len(), 2); + assert_eq!(form.pair.1, bytes); + assert_eq!(form.keys.get(bytes.as_slice()), Some(&bytes.as_slice())); + assert_eq!(form.range.start, bytes); + assert_eq!(form.range.end, bytes); + assert_eq!(form.alias, bytes); + assert!(form.capped.is_complete()); + assert_eq!(form.capped.n.written, bytes.len() as u64); + assert!(form.capped_bytes.is_complete()); + assert_eq!(form.capped_bytes.value, bytes); + assert_eq!(form.context.context.field_value("context.file"), Some("hello")); + + let files = form.files.into_iter().chain([ + form.map.into_values().next().unwrap(), + form.pair.0, + form.optional.unwrap(), + form.result.unwrap(), + Arc::try_unwrap(form.arc).unwrap(), + form.strict.into_inner(), + form.lenient.into_inner(), + form.capped.value, + form.context.value.unwrap().file, + ]); + + // Read after all later parts have been parsed to exercise borrowed storage. + for file in files { + assert!(file.path().is_none()); + assert!(file.content_type().is_none()); + let mut actual = Vec::new(); + file.open().await.unwrap().read_to_end(&mut actual).await.unwrap(); + assert_eq!(actual, bytes); + } +} + +#[test] +fn nested_forms_preserve_bytes_and_decoded_text() { + let client = Client::debug_with(routes![composed]).unwrap(); + let names = [ + "decoded", "files[0]", "files[1]", "map[item]", "keys[k:0]", "keys[v:0]", + "pair.0", "pair.1", "optional", "result", "arc", "strict", "lenient", "capped", + "capped_bytes", "range.start", "range.end", "context.file", "ReNaMeD", + ]; + let fields: Vec<_> = names.iter() + .map(|name| (*name, b"\xef\xbb\xbfhello".as_slice())).collect(); + let response = client.post("/composed").header(multipart_type()) + .body(multipart(&fields, false)).dispatch(); + assert_eq!(response.status(), Status::Ok); +} + +struct LegacyField<'r>(&'r str); + +impl<'r> FromFormField<'r> for LegacyField<'r> { + fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> { + Ok(Self(field.value)) + } +} + +struct LegacyForm<'r>(Vec<&'r str>); + +#[rocket::async_trait] +impl<'r> form::FromForm<'r> for LegacyForm<'r> { + type Context = Vec<&'r str>; + + fn init(_: form::Options) -> Self::Context { + Vec::new() + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>) { + ctxt.push(field.value); + } + + async fn push_data(_: &mut Self::Context, _: DataField<'r, '_>) { + panic!("a multipart value must not become a data field"); + } + + fn finalize(ctxt: Self::Context) -> form::Result<'r, Self> { + Ok(Self(ctxt)) + } +} + +#[post("/legacy", data = "")] +fn legacy(form: Form>) -> String { + assert_eq!(form.0.len(), 1); + form.0[0].to_owned() +} + +#[post("/legacy-field", data = "")] +fn legacy_field(form: Form>) -> String { + form.0.to_owned() +} + +#[test] +fn existing_text_callbacks_keep_decoding_behavior() { + let client = Client::debug_with(routes![legacy, legacy_field]).unwrap(); + for (bytes, decoded) in [ + (b"\xff".as_slice(), "\u{fffd}"), + (b"\xef\xbb\xbfhello", "hello"), + (b"\xff\xfeh\0i\0", "hi"), + (b"a+%20", "a+%20"), + ] { + for path in ["/legacy", "/legacy-field"] { + let response = client.post(path).header(multipart_type()) + .body(multipart(&[("value", bytes)], false)).dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), decoded); + } + } + + let field = ValueField { name: form::name::NameView::new("file"), value: "manual" }; + assert_eq!(Form::<&[u8]>::parse_iter([field]).unwrap(), b"manual"); + let response = client.post("/legacy").header(ContentType::Form) + .body("value=a+%20").dispatch(); + assert_eq!(response.into_string().unwrap(), "a "); +} + +#[post("/strict", data = "")] +fn strict(form: Form>>) -> String { + form.file.len().to_string() +} + +#[post("/bytes", data = "")] +fn bytes(form: Form<&[u8]>) -> Vec { + form.into_inner().to_vec() +} + +#[post("/string", data = "")] +fn string(form: Form<&str>) -> String { + form.into_inner().to_owned() +} + +#[post("/capped", data = "")] +fn capped(form: Form>>) -> String { + format!("{}:{}", form.n.written, form.is_complete()) +} + +#[test] +fn limits_duplicates_and_cleanup() { + let directory = tempfile::tempdir().unwrap(); + let limits = Limits::default().limit("file", 2.bytes()) + .limit("string", 2.bytes()).limit("bytes", 2.bytes()); + let figment = rocket::Config::figment() + .merge(("limits", limits)).merge(("temp_dir", directory.path())); + let client = Client::debug(rocket::custom(figment) + .mount("/", routes![strict, bytes, string, capped])).unwrap(); + + for path in ["/strict", "/bytes", "/string"] { + for content_type in [false, true] { + let response = client.post(path).header(multipart_type()) + .body(multipart(&[("file", b"hello")], content_type)).dispatch(); + let expected = if content_type { Status::PayloadTooLarge } else { Status::Ok }; + assert_eq!(response.status(), expected, "{path}, content_type={content_type}"); + } + } + + for (content_type, expected) in [(false, "5:true"), (true, "2:false")] { + let response = client.post("/capped").header(multipart_type()) + .body(multipart(&[("file", b"hello")], content_type)).dispatch(); + assert_eq!(response.into_string().unwrap(), expected); + } + + let body = multipart(&[("file", b"a"), ("file", b"b")], false); + let response = client.post("/strict").header(multipart_type()).body(body).dispatch(); + assert_eq!(response.status(), Status::UnprocessableEntity); + // Explicit data parts create files even when parsing fails or is capped. + assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0); +} + +#[test] +fn whole_form_limit_and_malformed_body() { + let payload: Vec = (0..=255).collect(); + let body = multipart(&[("file", &payload)], false); + // An unread required field adds a Missing error, whose 422 status takes + // precedence over the parser's length error. + let cases = [(body.len(), Status::Ok), (body.len() - 1, Status::UnprocessableEntity)]; + for (limit, status) in cases { + let limits = Limits::default().limit("data-form", limit.bytes()); + let figment = rocket::Config::figment().merge(("limits", limits)); + let client = Client::debug(rocket::custom(figment).mount("/", routes![upload])).unwrap(); + let response = client.post("/").header(multipart_type()).body(body.clone()).dispatch(); + assert_eq!(response.status(), status); + if status == Status::Ok { + assert_eq!(response.into_bytes().unwrap(), payload); + } + } + + let client = Client::debug_with(routes![upload]).unwrap(); + let response = client.post("/").header(multipart_type()) + .body(b"--boundary\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\n\xff") + .dispatch(); + assert_eq!(response.status(), Status::UnprocessableEntity); +}