From 398a044eb05fbdcfe879a91de31d4601f3ae8dc8 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 3 Mar 2021 21:59:11 -0800 Subject: [PATCH] Complete forms documentation. Improve 'validate'. * Add a `msg!()` macro to easily change a field validation message. * Allow a field to refer to itself via `self.field`. * Improve the various field validation traits. --- contrib/lib/src/json.rs | 1 + core/codegen/src/derive/form_field.rs | 176 +++- core/codegen/src/derive/from_form.rs | 9 +- core/lib/src/form/error.rs | 1199 +++++++++++++++++-------- core/lib/src/form/field.rs | 192 +++- core/lib/src/form/form.rs | 43 +- core/lib/src/form/from_form.rs | 6 +- core/lib/src/form/from_form_field.rs | 21 +- core/lib/src/form/mod.rs | 747 +++++++-------- core/lib/src/form/name.rs | 65 +- core/lib/src/form/options.rs | 6 + core/lib/src/form/parser.rs | 2 + core/lib/src/form/validate.rs | 749 +++++++++++---- site/guide/4-requests.md | 25 +- 14 files changed, 2241 insertions(+), 1000 deletions(-) diff --git a/contrib/lib/src/json.rs b/contrib/lib/src/json.rs index 7c37a976..fbb0786f 100644 --- a/contrib/lib/src/json.rs +++ b/contrib/lib/src/json.rs @@ -445,4 +445,5 @@ macro_rules! json { }; } +#[doc(inline)] pub use json; diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index d1698a63..779736a7 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -1,7 +1,10 @@ use devise::{*, ext::{TypeExt, SpanDiagnosticExt}}; +use syn::visit_mut::VisitMut; +use syn::visit::Visit; + use crate::exports::*; -use crate::proc_macro2::Span; +use crate::proc_macro2::{Span, TokenStream, TokenTree}; use crate::name::Name; pub struct FormField { @@ -86,62 +89,139 @@ impl FieldExt for Field<'_> { fn name_view(&self) -> Result { let field_name = self.field_name()?; - Ok(syn::parse_quote!(#_form::NameBuf::from((__c.__parent, #field_name)))) + let name_view = quote_spanned! { self.span() => + #_form::NameBuf::from((__c.__parent, #field_name)) + }; + + Ok(syn::parse2(name_view).unwrap()) + } +} + +struct RecordMemberAccesses(Vec); + +impl<'a> Visit<'a> for RecordMemberAccesses { + fn visit_expr_field(&mut self, i: &syn::ExprField) { + if let syn::Expr::Path(e) = &*i.base { + if e.path.is_ident("self") { + self.0.push(i.member.clone()); + } + } + + syn::visit::visit_expr_field(self, i); + } +} + +struct ValidationMutator<'a> { + field: &'a syn::Ident, + parent: &'a syn::Ident, + local: bool, + visited: bool, +} + +impl ValidationMutator<'_> { + fn visit_token_stream(&mut self, tt: TokenStream) -> TokenStream { + use quote::{ToTokens, TokenStreamExt}; + use TokenTree::*; + + let mut iter = tt.into_iter(); + let mut stream = TokenStream::new(); + while let Some(tt) = iter.next() { + match tt { + Ident(s3lf) if s3lf == "self" => { + match (iter.next(), iter.next()) { + (Some(Punct(p)), Some(Ident(i))) if p.as_char() == '.' => { + let field = syn::parse_quote!(#s3lf #p #i); + let mut expr = syn::Expr::Field(field); + self.visit_expr_mut(&mut expr); + expr.to_tokens(&mut stream); + }, + (tt1, tt2) => stream.append_all(&[Some(Ident(s3lf)), tt1, tt2]), + } + }, + TokenTree::Group(group) => { + let tt = self.visit_token_stream(group.stream()); + let mut new = proc_macro2::Group::new(group.delimiter(), tt); + new.set_span(group.span()); + let group = TokenTree::Group(new); + stream.append(group); + } + tt => stream.append(tt), + } + } + + stream + } +} + +impl VisitMut for ValidationMutator<'_> { + fn visit_expr_call_mut(&mut self, call: &mut syn::ExprCall) { + syn::visit_mut::visit_expr_call_mut(self, call); + + // Only modify the first call we see. + if self.visited { return; } + + let (parent, field) = (self.parent, self.field); + let form_field = match self.local { + true => syn::parse2(quote_spanned!(field.span() => &#field)).unwrap(), + false => syn::parse2(quote_spanned!(field.span() => &#parent.#field)).unwrap(), + }; + + call.args.insert(0, form_field); + self.visited = true; + } + + fn visit_ident_mut(&mut self, i: &mut syn::Ident) { + if !self.local && i == "self" { + *i = self.parent.clone(); + } + } + + fn visit_macro_mut(&mut self, mac: &mut syn::Macro) { + mac.tokens = self.visit_token_stream(mac.tokens.clone()); + syn::visit_mut::visit_macro_mut(self, mac); + } + + fn visit_expr_mut(&mut self, i: &mut syn::Expr) { + // If this is a local, replace accesses of `self.field` with `field`. + if let syn::Expr::Field(e) = i { + if let syn::Expr::Path(e) = &*e.base { + if e.path.is_ident("self") && self.local { + let new_expr = &self.field; + *i = syn::parse_quote!(#new_expr); + } + } + } + + return syn::visit_mut::visit_expr_mut(self, i); } } pub fn validators<'v>( field: Field<'v>, - out: &'v syn::Ident, + parent: &'v syn::Ident, // field ident (if local) or form ident (if !local) local: bool, ) -> Result + 'v> { - use syn::visit_mut::VisitMut; - - struct ValidationMutator<'a> { - field: syn::Expr, - self_expr: syn::Expr, - form: &'a syn::Ident, - visited: bool, - rec: bool, - } - - impl<'a> ValidationMutator<'a> { - fn new(field: &'a syn::Ident, form: &'a syn::Ident) -> Self { - let self_expr = syn::parse_quote!(&#form.#field); - let field = syn::parse_quote!(&#field); - ValidationMutator { field, self_expr, form, visited: false, rec: false } - } - } - - impl VisitMut for ValidationMutator<'_> { - fn visit_expr_call_mut(&mut self, call: &mut syn::ExprCall) { - syn::visit_mut::visit_expr_call_mut(self, call); - - let ident = if self.rec { &self.self_expr } else { &self.field }; - if !self.visited { - call.args.insert(0, ident.clone()); - self.visited = true; - } - } - - fn visit_ident_mut(&mut self, i: &mut syn::Ident) { - if i == "self" { - *i = self.form.clone(); - self.rec = true; - } - - syn::visit_mut::visit_ident_mut(self, i); - } - } - Ok(FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)? .into_iter() .filter_map(|a| a.validate) - .map(move |mut expr| { - let mut mutator = ValidationMutator::new(field.ident(), out); - mutator.visit_expr_mut(&mut expr); - (expr, mutator.rec) + .map(move |expr| { + let mut members = RecordMemberAccesses(vec![]); + members.visit_expr(&expr); + + let field_ident = field.ident(); + let is_local_validation = members.0.iter() + .all(|member| match member { + syn::Member::Named(i) => i == field_ident, + _ => false + }); + + (expr, is_local_validation) }) - .filter(move |(_, global)| local != *global) - .map(|(e, _)| e)) + .filter(move |(_, is_local)| *is_local == local) + .map(move |(mut expr, _)| { + let field = field.ident(); + let mut v = ValidationMutator { parent, local, field, visited: false }; + v.visit_expr_mut(&mut expr); + expr + })) } diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index 82283d4d..f3762660 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -51,6 +51,7 @@ fn context_type(input: Input<'_>) -> (TokenStream, Option) { pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { DeriveGenerator::build_for(input, quote!(impl<'__f> #_form::FromForm<'__f>)) + // NOTE: If support is widened, fix `FieldExt::ident()` `expect()`. .support(Support::NamedStruct | Support::Lifetime | Support::Type) .replace_generic(0, 0) .type_bound(quote!(#_form::FromForm<'__f> + '__f)) @@ -189,13 +190,6 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { .split2(); Ok(quote_spanned! { fields.span() => - // if __c.__parent.is_none() { - // let _e = #_form::Error::from(#_form::ErrorKind::Missing) - // .with_entity(#_form::Entity::Form); - // - // return #_Err(_e.into()); - // } - #(let #ident = match #finalize_field { #_ok(#ident) => #_some(#ident), #_err(_e) => { __c.__errors.extend(_e); #_none } @@ -231,7 +225,6 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { .unwrap_or_else(|| <#ty as #_form::FromForm<'__f>>::default() .ok_or_else(|| #_form::ErrorKind::Missing.into()) ) - // <#ty as #_form::FromForm<'__f>>::finalize(__c.#ident) .and_then(|#ident| { let mut _es = #_form::Errors::new(); #(if let #_err(_e) = #validator { _es.extend(_e); })* diff --git a/core/lib/src/form/error.rs b/core/lib/src/form/error.rs index 3a18e514..97e90df4 100644 --- a/core/lib/src/form/error.rs +++ b/core/lib/src/form/error.rs @@ -1,3 +1,5 @@ +//! Form error types. + use std::{fmt, io}; use std::num::{ParseIntError, ParseFloatError}; use std::str::{Utf8Error, ParseBoolError}; @@ -11,20 +13,105 @@ use crate::form::name::{NameBuf, Name}; use crate::data::ByteUnit; /// A collection of [`Error`]s. +/// +/// `Errors` is a thin wrapper around a `Vec` with convenient methods for +/// modifying the internal `Error`s. It `Deref`s and `DerefMut`s to +/// `Vec` for transparent access to the underlying vector. +/// +/// # Matching Errors to Fields +/// +/// To find the errors that correspond to a given field, use +/// [`Error::is_for()`]. For example, to get all of the errors that correspond +/// to the field `foo.bar`, you might write the following: +/// +/// ```rust +/// use rocket::form::Errors; +/// +/// let errors = Errors::new(); +/// let errors_for_foo = errors.iter().filter(|e| e.is_for("foo.bar")); +/// ``` +/// +/// ## Contructing +/// +/// An `Errors` can be constructed from anything that an `Error` can be +/// constructed from. This includes [`Error`], [`ErrorKind`], and all of the +/// types an `ErrorKind` can be constructed from. See +/// [`ErrorKind`](ErrorKind#constructing) for the full list. +/// +/// ```rust +/// use rocket::form; +/// +/// fn at_most_10() -> form::Result<'static, usize> { +/// // Using `From => ErrorKind::Int => Errors`. +/// let i: usize = "foo".parse()?; +/// +/// if i > 10 { +/// // `(Option, Option) => ErrorKind::OutOfRange => Errors` +/// return Err((None, Some(10isize)).into()); +/// } +/// +/// Ok(i) +/// } +/// ``` #[derive(Default, Debug, PartialEq, Serialize)] #[serde(transparent)] pub struct Errors<'v>(Vec>); -impl crate::http::ext::IntoOwned for Errors<'_> { - type Owned = Errors<'static>; - - fn into_owned(self) -> Self::Owned { - Errors(self.0.into_owned()) - } -} - /// A form error, potentially tied to a specific form field. /// +/// An `Error` is returned by [`FromForm`], [`FromFormField`], and +/// [`validate`](crate::form::validate) procedures, typically as a collection of +/// [`Errors`]. It potentially identifies a specific field that triggered the +/// error via [`Error::name`] and the field's value via [`Error::value`]. +/// +/// An `Error` can occur because of a field's value that failed to parse or +/// because other parts of a field or form were malformed; the [`Error::entity`] +/// identifies the part of the form that resulted in the error. +/// +/// # Contructing +/// +/// An `Error` can be constructed from anything that an [`ErrorKind`] can be +/// constructed from. See [`ErrorKind`](ErrorKind#constructing). +/// +/// ```rust +/// use rocket::form::Error; +/// +/// fn at_most_10() -> Result> { +/// // Using `From => ErrorKind::Int`. +/// let i: usize = "foo".parse()?; +/// +/// if i > 10 { +/// // `From<(Option, Option)> => ErrorKind::OutOfRange` +/// return Err((None, Some(10isize)).into()); +/// } +/// +/// Ok(i) +/// } +/// ``` +/// +/// # Setting Field Metadata +/// +/// When implementing [`FromFormField`], nothing has to be done for a field's +/// metadata to be set: the blanket [`FromForm`] implementation sets it +/// automatically. +/// +/// When constructed from an `ErrorKind`, the entity is set to +/// [`Entity::default_for()`] by default. Ocassionally, the error's `entity` may +/// need to be set manually. Return what would be useful to the end-consumer. +/// +/// # Matching Errors to Fields +/// +/// To determine whether an error corresponds to a given field, use +/// [`Error::is_for()`]. For example, to get all of the errors that correspond +/// to the field `foo.bar`, you might write the following: +/// +/// ```rust +/// use rocket::form::Errors; +/// +/// let errors = Errors::new(); +/// let errors_for_foo = errors.iter().filter(|e| e.is_for("foo.bar")); +/// ``` +/// /// # Serialization /// /// When a value of this type is serialized, a `struct` or map with the @@ -48,6 +135,588 @@ pub struct Error<'v> { pub entity: Entity, } +/// The kind of form error that occured. +/// +/// ## Constructing +/// +/// An `ErrorKind` can be constructed directly or via a `From` of the following +/// types: +/// +/// * `(Option, Option)` => [`ErrorKind::InvalidLength`] +/// * `(Option, Option)` => [`ErrorKind::InvalidLength`] +/// * `(Option, Option)` => [`ErrorKind::OutOfRange`] +/// * `&[Cow<'_, str>]` or `Vec>` => [`ErrorKind::InvalidChoice`] +/// * [`Utf8Error`] => [`ErrorKind::Utf8`] +/// * [`ParseIntError`] => [`ErrorKind::Int`] +/// * [`ParseFloatError`] => [`ErrorKind::Float`] +/// * [`ParseBoolError`] => [`ErrorKind::Bool`] +/// * [`AddrParseError`] => [`ErrorKind::Addr`] +/// * [`io::Error`] => [`ErrorKind::Io`] +/// * `Box [`ErrorKind::Custom`] +#[derive(Debug)] +pub enum ErrorKind<'v> { + /// The value's length, in bytes, was outside the range `[min, max]`. + InvalidLength { + /// The minimum length required, inclusive. + min: Option, + /// The maximum length required, inclusize. + max: Option, + }, + /// The value wasn't one of the valid `choices`. + InvalidChoice { + /// The choices that were expected. + choices: Cow<'v, [Cow<'v, str>]>, + }, + /// The integer value was outside the range `[start, end]`. + OutOfRange { + /// The start of the acceptable range, inclusive. + start: Option, + /// The end of the acceptable range, inclusive. + end: Option, + }, + /// A custom validation routine failed with message `.0`. + Validation(Cow<'v, str>), + /// One entity was expected but more than one was received. + Duplicate, + /// An entity was expected but was not received. + Missing, + /// An unexpected entity was received. + Unexpected, + /// An unknown entity was received. + Unknown, + /// A custom error occurred. + Custom(Box), + /// An error while parsing a multipart form occurred. + Multipart(multer::Error), + /// A string was invalid UTF-8. + Utf8(Utf8Error), + /// A value failed to parse as an integer. + Int(ParseIntError), + /// A value failed to parse as a boolean. + Bool(ParseBoolError), + /// A value failed to parse as a float. + Float(ParseFloatError), + /// A value failed to parse as an IP or socket address. + Addr(AddrParseError), + /// An I/O error occurred. + Io(io::Error), +} + +/// The erranous form entity or form component. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Entity { + /// The form itself. + Form, + /// A field. + Field, + /// A [`ValueField`](crate::form::ValueField). + ValueField, + /// A [`DataField`](crate::form::DataField). + DataField, + /// A field name. + Name, + /// A field value. + Value, + /// A field name key. + Key, + /// A field name key index at index `.0`. + Index(usize), +} + +impl<'v> Errors<'v> { + /// Create an empty collection of errors. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::Errors; + /// + /// let errors = Errors::new(); + /// assert!(errors.is_empty()); + /// ``` + pub fn new() -> Self { + Errors(vec![]) + } + + /// Consumes `self` and returns a new `Errors` with each field name set to + /// `name` if it was not already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Errors, ErrorKind}; + /// + /// let mut errors = Errors::from(ErrorKind::Missing); + /// assert!(errors[0].name.is_none()); + /// + /// let mut errors = errors.with_name("foo"); + /// assert_eq!(errors[0].name.as_ref().unwrap(), "foo"); + /// + /// errors.push(ErrorKind::Duplicate.into()); + /// let errors = errors.with_name("bar"); + /// assert_eq!(errors[0].name.as_ref().unwrap(), "foo"); + /// assert_eq!(errors[1].name.as_ref().unwrap(), "bar"); + /// ``` + pub fn with_name>>(mut self, name: N) -> Self { + self.set_name(name); + self + } + + /// Set the field name of each error in `self` to `name` if it is not + /// already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Errors, ErrorKind}; + /// + /// let mut errors = Errors::from(ErrorKind::Missing); + /// assert!(errors[0].name.is_none()); + /// + /// errors.set_name("foo"); + /// assert_eq!(errors[0].name.as_ref().unwrap(), "foo"); + /// + /// errors.push(ErrorKind::Duplicate.into()); + /// let errors = errors.with_name("bar"); + /// assert_eq!(errors[0].name.as_ref().unwrap(), "foo"); + /// assert_eq!(errors[1].name.as_ref().unwrap(), "bar"); + /// ``` + pub fn set_name>>(&mut self, name: N) { + let name = name.into(); + for error in self.iter_mut() { + if error.name.is_none() { + error.set_name(name.clone()); + } + } + } + + /// Consumes `self` and returns a new `Errors` with each field value set to + /// `value` if it was not already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Errors, ErrorKind}; + /// + /// let mut errors = Errors::from(ErrorKind::Missing); + /// assert!(errors[0].value.is_none()); + /// + /// let mut errors = errors.with_value("foo"); + /// assert_eq!(errors[0].value.as_ref().unwrap(), "foo"); + /// + /// errors.push(ErrorKind::Duplicate.into()); + /// let errors = errors.with_value("bar"); + /// assert_eq!(errors[0].value.as_ref().unwrap(), "foo"); + /// assert_eq!(errors[1].value.as_ref().unwrap(), "bar"); + /// ``` + pub fn with_value(mut self, value: &'v str) -> Self { + self.set_value(value); + self + } + + /// Set the field value of each error in `self` to `value` if it is not + /// already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Errors, ErrorKind}; + /// + /// let mut errors = Errors::from(ErrorKind::Missing); + /// assert!(errors[0].value.is_none()); + /// + /// errors.set_value("foo"); + /// assert_eq!(errors[0].value.as_ref().unwrap(), "foo"); + /// + /// errors.push(ErrorKind::Duplicate.into()); + /// let errors = errors.with_value("bar"); + /// assert_eq!(errors[0].value.as_ref().unwrap(), "foo"); + /// assert_eq!(errors[1].value.as_ref().unwrap(), "bar"); + /// ``` + pub fn set_value(&mut self, value: &'v str) { + self.iter_mut().for_each(|e| e.set_value(value)); + } + + /// Returns the highest [`Error::status()`] of all of the errors in `self` + /// or [`Status::InternalServerError`] if `self` is empty. This is the + /// status that is set by the [`Form`] data guard on failure. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, Errors, ErrorKind}; + /// use rocket::http::Status; + /// + /// let mut errors = Errors::new(); + /// assert_eq!(errors.status(), Status::InternalServerError); + /// + /// errors.push(Error::from((None, Some(10u64)))); + /// assert_eq!(errors.status(), Status::PayloadTooLarge); + /// + /// errors.push(Error::from(ErrorKind::Missing)); + /// assert_eq!(errors.status(), Status::UnprocessableEntity); + /// ``` + pub fn status(&self) -> Status { + let max = self.iter().map(|e| e.status()).max(); + max.unwrap_or(Status::InternalServerError) + } +} + +impl crate::http::ext::IntoOwned for Errors<'_> { + type Owned = Errors<'static>; + + fn into_owned(self) -> Self::Owned { + Errors(self.0.into_owned()) + } +} + +impl fmt::Display for Errors<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} errors:", self.len())?; + for error in self.iter() { + write!(f, "\n{}", error)?; + } + + Ok(()) + } +} + +impl<'v> std::ops::Deref for Errors<'v> { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'v> std::ops::DerefMut for Errors<'v> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'v, T: Into>> From for Errors<'v> { + #[inline(always)] + fn from(e: T) -> Self { + Errors(vec![e.into()]) + } +} + +impl<'v> From>> for Errors<'v> { + #[inline(always)] + fn from(v: Vec>) -> Self { + Errors(v) + } +} + +impl<'v> IntoIterator for Errors<'v> { + type Item = Error<'v>; + + type IntoIter = > as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'v> Error<'v> { + /// Creates a new `Error` with `ErrorKind::Custom`. + /// + /// For validation errors, use [`Error::validation()`]. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::Error; + /// + /// fn from_fmt(error: std::fmt::Error) -> Error<'static> { + /// Error::custom(error) + /// } + /// ``` + pub fn custom(error: E) -> Self + where E: std::error::Error + Send + 'static + { + (Box::new(error) as Box).into() + } + + /// Creates a new `Error` with `ErrorKind::Validation` and message `msg`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind, Entity}; + /// + /// let error = Error::validation("invalid foo: need bar"); + /// assert!(matches!(error.kind, ErrorKind::Validation(_))); + /// assert_eq!(error.entity, Entity::Value); + /// ``` + pub fn validation>>(msg: S) -> Self { + ErrorKind::Validation(msg.into()).into() + } + + /// Consumes `self` and returns a new `Error` with the entity set to + /// `entity`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind, Entity}; + /// + /// let error = Error::from(ErrorKind::Missing); + /// assert_eq!(error.entity, Entity::Field); + /// + /// let error = error.with_entity(Entity::Key); + /// assert_eq!(error.entity, Entity::Key); + /// ``` + pub fn with_entity(mut self, entity: Entity) -> Self { + self.set_entity(entity); + self + } + + /// Sets the error's entity to `entity.` + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind, Entity}; + /// + /// let mut error = Error::from(ErrorKind::Missing); + /// assert_eq!(error.entity, Entity::Field); + /// + /// error.set_entity(Entity::Key); + /// assert_eq!(error.entity, Entity::Key); + /// ``` + pub fn set_entity(&mut self, entity: Entity) { + self.entity = entity; + } + + /// Consumes `self` and returns a new `Error` with the field name set to + /// `name` if it was not already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind}; + /// + /// let error = Error::from(ErrorKind::Missing); + /// assert!(error.name.is_none()); + /// + /// let error = error.with_name("foo"); + /// assert_eq!(error.name.as_ref().unwrap(), "foo"); + /// + /// let error = error.with_name("bar"); + /// assert_eq!(error.name.as_ref().unwrap(), "foo"); + /// ``` + pub fn with_name>>(mut self, name: N) -> Self { + self.set_name(name); + self + } + + /// Sets the field name of `self` to `name` if it is not already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind}; + /// + /// let mut error = Error::from(ErrorKind::Missing); + /// assert!(error.name.is_none()); + /// + /// error.set_name("foo"); + /// assert_eq!(error.name.as_ref().unwrap(), "foo"); + /// + /// let error = error.with_name("bar"); + /// assert_eq!(error.name.as_ref().unwrap(), "foo"); + /// ``` + pub fn set_name>>(&mut self, name: N) { + if self.name.is_none() { + self.name = Some(name.into()); + } + } + + /// Consumes `self` and returns a new `Error` with the value set to `value` + /// if it was not already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind}; + /// + /// let error = Error::from(ErrorKind::Missing); + /// assert!(error.value.is_none()); + /// + /// let error = error.with_value("foo"); + /// assert_eq!(error.value.as_ref().unwrap(), "foo"); + /// + /// let error = error.with_value("bar"); + /// assert_eq!(error.value.as_ref().unwrap(), "foo"); + /// ``` + pub fn with_value(mut self, value: &'v str) -> Self { + self.set_value(value); + self + } + + /// Set the field value of `self` to `value` if it is not already set. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind}; + /// + /// let mut error = Error::from(ErrorKind::Missing); + /// assert!(error.value.is_none()); + /// + /// error.set_value("foo"); + /// assert_eq!(error.value.as_ref().unwrap(), "foo"); + /// + /// error.set_value("bar"); + /// assert_eq!(error.value.as_ref().unwrap(), "foo"); + /// ``` + pub fn set_value(&mut self, value: &'v str) { + if self.value.is_none() { + self.value = Some(value.into()); + } + } + + /// Returns `true` if this error applies to a field named `name`. **This is + /// _different_ than simply comparing `name`.** + /// + /// Unlike [`Error::is_for_exactly()`], this method returns `true` if the + /// error's field name is a **prefix of `name`**. This is typically what is + /// desired as errors apply to a field and its children: `a.b` applies to + /// the nested fields `a.b.c`, `a.b.d` and so on. + /// + /// Returns `false` if `self` has no field name. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::Error; + /// + /// // returns `false` without a field name + /// let error = Error::validation("bad `foo`"); + /// assert!(!error.is_for_exactly("a.b")); + /// + /// // `a.b` is a prefix all of these field names + /// let error = error.with_name("a.b"); + /// assert!(error.is_for("a.b")); + /// assert!(error.is_for("a[b]")); + /// assert!(error.is_for("a.b.c")); + /// assert!(error.is_for("a.b[c]")); + /// assert!(error.is_for("a.b.c[d]")); + /// assert!(error.is_for("a.b.c.d.foo")); + /// + /// // ...but not of these. + /// assert!(!error.is_for("a.c")); + /// assert!(!error.is_for("a")); + /// ``` + pub fn is_for>(&self, name: N) -> bool { + self.name.as_ref().map(|e_name| { + if e_name.is_empty() != name.as_ref().is_empty() { + return false; + } + + let mut e_keys = e_name.keys(); + let mut n_keys = name.as_ref().keys(); + loop { + match (e_keys.next(), n_keys.next()) { + (Some(e), Some(n)) if e == n => continue, + (Some(_), Some(_)) => return false, + (Some(_), None) => return false, + (None, _) => break, + } + } + + true + }) + .unwrap_or(false) + } + + /// Returns `true` if this error applies to exactly the field named `name`. + /// Returns `false` if `self` has no field name. + /// + /// Unlike [`Error::is_for()`], this method returns `true` only when the + /// error's field name is exactly `name`. This is _not_ typically what is + /// desired. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::Error; + /// + /// // returns `false` without a field name + /// let error = Error::validation("bad `foo`"); + /// assert!(!error.is_for_exactly("a.b")); + /// + /// let error = error.with_name("a.b"); + /// assert!(error.is_for_exactly("a.b")); + /// assert!(error.is_for_exactly("a[b]")); + /// + /// // does not return `true` when the name is a prefix + /// assert!(!error.is_for_exactly("a.b.c")); + /// assert!(!error.is_for_exactly("a.b[c]")); + /// assert!(!error.is_for_exactly("a.b.c[d]")); + /// assert!(!error.is_for_exactly("a.b.c.d.foo")); + /// + /// // does not return `true` when the name is different + /// assert!(!error.is_for("a.c")); + /// assert!(!error.is_for("a")); + /// ``` + pub fn is_for_exactly>(&self, name: N) -> bool { + self.name.as_ref() + .map(|n| name.as_ref() == n) + .unwrap_or(false) + } + + /// Returns the most reasonable `Status` associated with this error. These + /// are: + /// + /// * **`PayloadTooLarge`** if the error kind is: + /// - `InvalidLength` with min of `None` + /// - `Multpart(FieldSizeExceeded | StreamSizeExceeded)` + /// * **`InternalServerError`** if the error kind is: + /// - `Unknown` + /// * **`BadRequest`** if the error kind is: + /// - `Io` with an `entity` of `Form` + /// * **`UnprocessableEntity`** otherwise + /// + /// # Example + /// + /// ```rust + /// use rocket::form::error::{Error, ErrorKind, Entity}; + /// use rocket::http::Status; + /// + /// let error = Error::validation("bad `foo`"); + /// assert_eq!(error.status(), Status::UnprocessableEntity); + /// + /// let error = Error::from((None, Some(10u64))); + /// assert_eq!(error.status(), Status::PayloadTooLarge); + /// + /// let error = Error::from(ErrorKind::Unknown); + /// assert_eq!(error.status(), Status::InternalServerError); + /// + /// // default entity for `io::Error` is `Form`. + /// let error = Error::from(std::io::Error::last_os_error()); + /// assert_eq!(error.status(), Status::BadRequest); + /// + /// let error = error.with_entity(Entity::Value); + /// assert_eq!(error.status(), Status::UnprocessableEntity); + /// ``` + pub fn status(&self) -> Status { + use ErrorKind::*; + use multer::Error::*; + + match self.kind { + InvalidLength { min: None, .. } + | Multipart(FieldSizeExceeded { .. }) + | Multipart(StreamSizeExceeded { .. }) => Status::PayloadTooLarge, + Unknown => Status::InternalServerError, + Io(_) | _ if self.entity == Entity::Form => Status::BadRequest, + _ => Status::UnprocessableEntity + } + } +} + impl<'v> Serialize for Error<'v> { fn serialize(&self, ser: S) -> Result { let mut err = ser.serialize_struct("Error", 3)?; @@ -72,32 +741,106 @@ impl crate::http::ext::IntoOwned for Error<'_> { } } -#[derive(Debug)] -pub enum ErrorKind<'v> { - InvalidLength { - min: Option, - max: Option, - }, - InvalidChoice { - choices: Cow<'v, [Cow<'v, str>]>, - }, - OutOfRange { - start: Option, - end: Option, - }, - Validation(Cow<'v, str>), - Duplicate, - Missing, - Unexpected, - Unknown, - Custom(Box), - Multipart(multer::Error), - Utf8(Utf8Error), - Int(ParseIntError), - Bool(ParseBoolError), - Float(ParseFloatError), - Addr(AddrParseError), - Io(io::Error), +impl<'v> std::ops::Deref for Error<'v> { + type Target = ErrorKind<'v>; + + fn deref(&self) -> &Self::Target { + &self.kind + } +} + +impl fmt::Display for Error<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.kind.fmt(f) + } +} + +impl<'v, T: Into>> From for Error<'v> { + #[inline(always)] + fn from(k: T) -> Self { + let kind = k.into(); + let entity = Entity::default_for(&kind); + Error { name: None, value: None, kind, entity } + } +} + +impl<'a> From for Error<'a> { + fn from(error: multer::Error) -> Self { + use multer::Error::*; + use self::ErrorKind::*; + + let incomplete = Error::from(InvalidLength { min: None, max: None }); + match error { + UnknownField { field_name: Some(name) } => Error::from(Unexpected).with_name(name), + UnknownField { field_name: None } => Error::from(Unexpected), + FieldSizeExceeded { limit, field_name } => { + let e = Error::from((None, Some(limit))); + match field_name { + Some(name) => e.with_name(name), + None => e + } + }, + StreamSizeExceeded { limit } => { + Error::from((None, Some(limit))).with_entity(Entity::Form) + } + IncompleteFieldData { field_name: Some(name) } => incomplete.with_name(name), + IncompleteFieldData { field_name: None } => incomplete, + IncompleteStream | IncompleteHeaders => incomplete.with_entity(Entity::Form), + e => Error::from(ErrorKind::Multipart(e)) + } + } +} + +impl fmt::Display for ErrorKind<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorKind::InvalidLength { min, max } => { + match (min, max) { + (None, None) => write!(f, "unexpected or incomplete")?, + (None, Some(k)) => write!(f, "length cannot exceed {}", k)?, + (Some(1), None) => write!(f, "value cannot be empty")?, + (Some(k), None) => write!(f, "length must be at least {}", k)?, + (Some(i), Some(j)) => write!(f, "length must be between {} and {}", i, j)?, + } + } + ErrorKind::InvalidChoice { choices } => { + match choices.as_ref() { + &[] => write!(f, "invalid choice")?, + &[ref choice] => write!(f, "expected {}", choice)?, + _ => { + write!(f, "expected one of ")?; + for (i, choice) in choices.iter().enumerate() { + if i != 0 { write!(f, ", ")?; } + write!(f, "`{}`", choice)?; + } + } + } + } + ErrorKind::OutOfRange { start, end } => { + match (start, end) { + (None, None) => write!(f, "value is out of range")?, + (None, Some(k)) => write!(f, "value cannot exceed {}", k)?, + (Some(k), None) => write!(f, "value must be at least {}", k)?, + (Some(i), Some(j)) => write!(f, "value must be between {} and {}", i, j)?, + } + } + ErrorKind::Validation(msg) => msg.fmt(f)?, + ErrorKind::Duplicate => "duplicate".fmt(f)?, + ErrorKind::Missing => "missing".fmt(f)?, + ErrorKind::Unexpected => "unexpected".fmt(f)?, + ErrorKind::Unknown => "unknown internal error".fmt(f)?, + ErrorKind::Custom(e) => e.fmt(f)?, + ErrorKind::Multipart(e) => write!(f, "invalid multipart: {}", e)?, + ErrorKind::Utf8(e) => write!(f, "invalid UTF-8: {}", e)?, + ErrorKind::Int(e) => write!(f, "invalid integer: {}", e)?, + ErrorKind::Bool(e) => write!(f, "invalid boolean: {}", e)?, + ErrorKind::Float(e) => write!(f, "invalid float: {}", e)?, + ErrorKind::Addr(e) => write!(f, "invalid address: {}", e)?, + ErrorKind::Io(e) => write!(f, "i/o error: {}", e)?, + } + + Ok(()) + } } impl crate::http::ext::IntoOwned for ErrorKind<'_> { @@ -132,256 +875,6 @@ impl crate::http::ext::IntoOwned for ErrorKind<'_> { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Entity { - Form, - Field, - ValueField, - DataField, - Name, - Value, - Key, - Indices, - Index(usize), -} - -impl<'v> Errors<'v> { - pub fn new() -> Self { - Errors(vec![]) - } - - pub fn with_name>>(mut self, name: N) -> Self { - self.set_name(name); - self - } - - pub fn set_name>>(&mut self, name: N) { - let name = name.into(); - for error in self.iter_mut() { - if error.name.is_none() { - error.set_name(name.clone()); - } - } - } - - pub fn with_value(mut self, value: &'v str) -> Self { - self.set_value(value); - self - } - - pub fn set_value(&mut self, value: &'v str) { - self.iter_mut().for_each(|e| e.set_value(value)); - } - - pub fn status(&self) -> Status { - match &*self.0 { - &[] => Status::InternalServerError, - &[ref error] => error.status(), - &[ref e1, ref errors@..] => errors.iter() - .map(|e| e.status()) - .max() - .unwrap_or_else(|| e1.status()), - } - } -} - -impl<'v> Error<'v> { - pub fn custom(error: E) -> Self - where E: std::error::Error + Send + 'static - { - (Box::new(error) as Box).into() - } - - pub fn validation>>(msg: S) -> Self { - ErrorKind::Validation(msg.into()).into() - } - - pub fn with_entity(mut self, entity: Entity) -> Self { - self.set_entity(entity); - self - } - - pub fn set_entity(&mut self, entity: Entity) { - self.entity = entity; - } - - pub fn with_name>>(mut self, name: N) -> Self { - self.set_name(name); - self - } - - pub fn set_name>>(&mut self, name: N) { - if self.name.is_none() { - self.name = Some(name.into()); - } - } - - pub fn with_value(mut self, value: &'v str) -> Self { - self.set_value(value); - self - } - - pub fn set_value(&mut self, value: &'v str) { - if self.value.is_none() { - self.value = Some(value.into()); - } - } - - pub fn is_for_exactly>(&self, name: N) -> bool { - self.name.as_ref() - .map(|n| name.as_ref() == n) - .unwrap_or(false) - } - - pub fn is_for>(&self, name: N) -> bool { - self.name.as_ref().map(|e_name| { - if e_name.is_empty() != name.as_ref().is_empty() { - return false; - } - - let mut e_keys = e_name.keys(); - let mut n_keys = name.as_ref().keys(); - loop { - match (e_keys.next(), n_keys.next()) { - (Some(e), Some(n)) if e == n => continue, - (Some(_), Some(_)) => return false, - (Some(_), None) => return false, - (None, _) => break, - } - } - - true - }) - .unwrap_or(false) - } - - pub fn status(&self) -> Status { - use ErrorKind::*; - use multer::Error::*; - - match self.kind { - InvalidLength { min: None, .. } - | Multipart(FieldSizeExceeded { .. }) - | Multipart(StreamSizeExceeded { .. }) - => Status::PayloadTooLarge, - Unknown => Status::InternalServerError, - Io(_) | _ if self.entity == Entity::Form => Status::BadRequest, - _ => Status::UnprocessableEntity - } - } -} - -impl<'v> ErrorKind<'v> { - pub fn default_entity(&self) -> Entity { - match self { - | ErrorKind::InvalidLength { .. } - | ErrorKind::InvalidChoice { .. } - | ErrorKind::OutOfRange {.. } - | ErrorKind::Validation {.. } - | ErrorKind::Utf8(_) - | ErrorKind::Int(_) - | ErrorKind::Float(_) - | ErrorKind::Bool(_) - | ErrorKind::Custom(_) - | ErrorKind::Addr(_) => Entity::Value, - - | ErrorKind::Duplicate - | ErrorKind::Missing - | ErrorKind::Unknown - | ErrorKind::Unexpected => Entity::Field, - - | ErrorKind::Multipart(_) - | ErrorKind::Io(_) => Entity::Form, - } - } -} - -impl fmt::Display for ErrorKind<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ErrorKind::InvalidLength { min, max } => { - match (min, max) { - (None, None) => write!(f, "unexpected or incomplete")?, - (None, Some(k)) => write!(f, "length cannot exceed {}", k)?, - (Some(1), None) => write!(f, "value cannot be empty")?, - (Some(k), None) => write!(f, "length must be at least {}", k)?, - (Some(i), Some(j)) => write!(f, "length must be between {} and {}", i, j)?, - } - } - ErrorKind::InvalidChoice { choices } => { - match choices.as_ref() { - &[] => write!(f, "invalid choice")?, - &[ref choice] => write!(f, "expected {}", choice)?, - _ => { - write!(f, "expected one of ")?; - for (i, choice) in choices.iter().enumerate() { - if i != 0 { write!(f, ", ")?; } - write!(f, "`{}`", choice)?; - } - } - } - } - ErrorKind::OutOfRange { start, end } => { - match (start, end) { - (None, None) => write!(f, "out of range")?, - (None, Some(k)) => write!(f, "value cannot exceed {}", k)?, - (Some(k), None) => write!(f, "value must be at least {}", k)?, - (Some(i), Some(j)) => write!(f, "value must be between {} and {}", i, j)?, - } - } - ErrorKind::Validation(msg) => msg.fmt(f)?, - ErrorKind::Duplicate => "duplicate".fmt(f)?, - ErrorKind::Missing => "missing".fmt(f)?, - ErrorKind::Unexpected => "unexpected".fmt(f)?, - ErrorKind::Unknown => "unknown internal error".fmt(f)?, - ErrorKind::Custom(e) => e.fmt(f)?, - ErrorKind::Multipart(e) => write!(f, "invalid multipart: {}", e)?, - ErrorKind::Utf8(e) => write!(f, "invalid UTF-8: {}", e)?, - ErrorKind::Int(e) => write!(f, "invalid integer: {}", e)?, - ErrorKind::Bool(e) => write!(f, "invalid boolean: {}", e)?, - ErrorKind::Float(e) => write!(f, "invalid float: {}", e)?, - ErrorKind::Addr(e) => write!(f, "invalid address: {}", e)?, - ErrorKind::Io(e) => write!(f, "i/o error: {}", e)?, - } - - Ok(()) - } -} - -impl fmt::Display for Error<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.kind.fmt(f) - } -} - -impl fmt::Display for Entity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let string = match self { - Entity::Form => "form", - Entity::Field => "field", - Entity::ValueField => "value field", - Entity::DataField => "data field", - Entity::Name => "name", - Entity::Value => "value", - Entity::Key => "key", - Entity::Indices => "indices", - Entity::Index(k) => return write!(f, "index {}", k), - }; - - string.fmt(f) - } -} - -impl fmt::Display for Errors<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} errors:", self.len())?; - for error in self.iter() { - write!(f, "\n{}", error)?; - } - - Ok(()) - } -} impl<'a, 'b> PartialEq> for ErrorKind<'a> { fn eq(&self, other: &ErrorKind<'b>) -> bool { @@ -407,61 +900,6 @@ impl<'a, 'b> PartialEq> for ErrorKind<'a> { } } -impl<'v> std::ops::Deref for Errors<'v> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'v> std::ops::DerefMut for Errors<'v> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl<'v, T: Into>> From for Errors<'v> { - #[inline(always)] - fn from(e: T) -> Self { - Errors(vec![e.into()]) - } -} - -impl<'v> From>> for Errors<'v> { - #[inline(always)] - fn from(v: Vec>) -> Self { - Errors(v) - } -} - -impl<'v, T: Into>> From for Error<'v> { - #[inline(always)] - fn from(k: T) -> Self { - let kind = k.into(); - let entity = kind.default_entity(); - Error { name: None, value: None, kind, entity } - } -} - -impl<'v> IntoIterator for Errors<'v> { - type Item = Error<'v>; - - type IntoIter = > as IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'v> std::ops::Deref for Error<'v> { - type Target = ErrorKind<'v>; - - fn deref(&self) -> &Self::Target { - &self.kind - } -} - impl From<(Option, Option)> for ErrorKind<'_> { fn from((min, max): (Option, Option)) -> Self { ErrorKind::InvalidLength { min, max } @@ -488,10 +926,7 @@ impl From<(Option, Option)> for ErrorKind<'_> { impl From<(Option, Option)> for ErrorKind<'_> { fn from((start, end): (Option, Option)) -> Self { - use std::convert::TryFrom; - - let as_isize = |b: ByteUnit| isize::try_from(b.as_u64()).ok(); - ErrorKind::from((start.and_then(as_isize), end.and_then(as_isize))) + ErrorKind::from((start.map(ByteUnit::as_u64), end.map(ByteUnit::as_u64))) } } @@ -518,33 +953,6 @@ macro_rules! impl_from_for { ) } -impl<'a> From for Error<'a> { - fn from(error: multer::Error) -> Self { - use multer::Error::*; - use self::ErrorKind::*; - - let incomplete = Error::from(InvalidLength { min: None, max: None }); - match error { - UnknownField { field_name: Some(name) } => Error::from(Unexpected).with_name(name), - UnknownField { field_name: None } => Error::from(Unexpected), - FieldSizeExceeded { limit, field_name } => { - let e = Error::from((None, Some(limit))); - match field_name { - Some(name) => e.with_name(name), - None => e - } - }, - StreamSizeExceeded { limit } => { - Error::from((None, Some(limit))).with_entity(Entity::Form) - } - IncompleteFieldData { field_name: Some(name) } => incomplete.with_name(name), - IncompleteFieldData { field_name: None } => incomplete, - IncompleteStream | IncompleteHeaders => incomplete.with_entity(Entity::Form), - e => Error::from(ErrorKind::Multipart(e)) - } - } -} - impl_from_for!(<'a> Utf8Error => ErrorKind<'a> as Utf8); impl_from_for!(<'a> ParseIntError => ErrorKind<'a> as Int); impl_from_for!(<'a> ParseFloatError => ErrorKind<'a> as Float); @@ -552,3 +960,54 @@ impl_from_for!(<'a> ParseBoolError => ErrorKind<'a> as Bool); impl_from_for!(<'a> AddrParseError => ErrorKind<'a> as Addr); impl_from_for!(<'a> io::Error => ErrorKind<'a> as Io); impl_from_for!(<'a> Box => ErrorKind<'a> as Custom); + +impl fmt::Display for Entity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let string = match self { + Entity::Form => "form", + Entity::Field => "field", + Entity::ValueField => "value field", + Entity::DataField => "data field", + Entity::Name => "name", + Entity::Value => "value", + Entity::Key => "key", + Entity::Index(k) => return write!(f, "index {}", k), + }; + + string.fmt(f) + } +} + +impl Entity { + /// The default entity for an [`Error`] created for `ErrorKind`. + /// + /// * **[`Field`]** if `Duplicate`, `Missing`, `Unexpected`, or `Unknown` + /// * **[`Form`]** if `Multipart` or `Io` + /// * **[`Value`]** otherwise + /// + /// [`Field`]: Entity::Field + /// [`Form`]: Entity::Form + /// [`Value`]: Entity::Value + pub const fn default_for(kind: &ErrorKind<'_>) -> Self { + match kind { + | ErrorKind::InvalidLength { .. } + | ErrorKind::InvalidChoice { .. } + | ErrorKind::OutOfRange { .. } + | ErrorKind::Validation { .. } + | ErrorKind::Utf8(_) + | ErrorKind::Int(_) + | ErrorKind::Float(_) + | ErrorKind::Bool(_) + | ErrorKind::Custom(_) + | ErrorKind::Addr(_) => Entity::Value, + + | ErrorKind::Duplicate + | ErrorKind::Missing + | ErrorKind::Unknown + | ErrorKind::Unexpected => Entity::Field, + + | ErrorKind::Multipart(_) + | ErrorKind::Io(_) => Entity::Form, + } + } +} diff --git a/core/lib/src/form/field.rs b/core/lib/src/form/field.rs index d08ea535..3a044390 100644 --- a/core/lib/src/form/field.rs +++ b/core/lib/src/form/field.rs @@ -3,52 +3,225 @@ use crate::form::error::{Error, ErrorKind, Entity}; use crate::http::{ContentType, RawStr}; use crate::{Request, Data}; +/// A form field with a string value. +/// +/// 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`. #[derive(Debug, Clone)] pub struct ValueField<'r> { + /// The (decoded) name of the form field. pub name: NameView<'r>, + /// The (decoded) value of the form field. pub value: &'r str, } +/// A multipart form field with an underlying data stream. +/// +/// Rocket preprocesses all form fields into either [`ValueField`]s or +/// [`DataField`]s. Multipart form fields with a `Content-Type` are preprocessed +/// as a `DataField`. The underlying data is _not_ read into memory, but +/// instead, streamable from the contained [`Data`] structure. pub struct DataField<'r, 'i> { + /// The (decoded) name of the form field. pub name: NameView<'r>, + /// The form field's sanitized file name. + /// + /// ## Santization + /// + /// A "sanitized" file name is a non-empty string, stripped of its file + /// exntesion, which does not contain any path delimiters (`/` or `\`), is + /// not a hidden (`.`) or `*`-prefixed name, and does not contain special + /// characters (`:`, `>`, or `<`). If the submitted file name matches any of + /// these properties or none was submitted, `file_name` will be `None`. pub file_name: Option<&'r str>, + /// The form field's Content-Type, as submitted, which may or may not + /// reflect on `data`. pub content_type: ContentType, + /// The request in which the form field was submitted. pub request: &'r Request<'i>, + /// The raw data stream. pub data: Data, } impl<'v> ValueField<'v> { - /// `raw` must already be URL-decoded. This is weird. + /// Parse a field string, where both the key and value are assumed to be + /// URL-decoded while preserving the `=` delimiter, into a `ValueField`. + /// + /// This implements 3.2, 3.3 of [section 5.1 of the WHATWG living standard]. + /// + /// [section 5.1 of the WHATWG living standard]: https://url.spec.whatwg.org/#urlencoded-parsing + /// + /// # Example + /// + /// ```rust + /// use rocket::form::ValueField; + /// + /// let parsed = ValueField::parse("a cat=an A+ pet"); + /// assert_eq!(parsed.name, "a cat"); + /// assert_eq!(parsed.value, "an A+ pet"); + /// + /// let parsed = ValueField::parse("a cat is an A+ pet"); + /// assert_eq!(parsed.name, "a cat is an A+ pet"); + /// assert_eq!(parsed.value, ""); + /// + /// let parsed = ValueField::parse("cat.food=yum?"); + /// assert_eq!(parsed.name, "cat"); + /// assert_eq!(parsed.name.source(), "cat.food"); + /// assert_eq!(parsed.value, "yum?"); + /// ``` pub fn parse(field: &'v str) -> Self { // WHATWG URL Living Standard 5.1 steps 3.2, 3.3. let (name, val) = RawStr::new(field).split_at_byte(b'='); ValueField::from((name.as_str(), val.as_str())) } + /// Create a `ValueField` from a value, which is assumed to be URL-decoded. + /// The field `name` will be empty. + /// + /// This is equivalent to `ValueField::from(("", value))`. To create a + /// `ValueField` from both a `name` and a `value`, use + /// `ValueField::from((name, value))`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::ValueField; + /// + /// let parsed = ValueField::from_value("A+=kitten"); + /// assert_eq!(parsed.name, ""); + /// assert_eq!(parsed.value, "A+=kitten"); + /// ``` pub fn from_value(value: &'v str) -> Self { ValueField::from(("", value)) } + /// Shift the `name` of `self` and return `self` with the shfited `name`. + /// + /// See [`NameView::shift()`] for the details on name "shifting". + /// + /// # Example + /// + /// ```rust + /// use rocket::form::ValueField; + /// + /// let parsed = ValueField::parse("cat.food=yum?"); + /// assert_eq!(parsed.name, "cat"); + /// assert_eq!(parsed.name.source(), "cat.food"); + /// assert_eq!(parsed.name.key_lossy(), "cat"); + /// + /// let shifted = parsed.shift(); + /// assert_eq!(shifted.name, "cat.food"); + /// assert_eq!(shifted.name.key_lossy(), "food"); + /// ``` pub fn shift(mut self) -> Self { self.name.shift(); self } + /// Creates a complete unexpected value field [`Error`] from `self`. + /// + /// The error will have the following properties: + /// * `kind`: [`ErrorKind::Unexpected`] + /// * `name`: [`self.name.source()`](NameView::source()) + /// * `value`: [`self.value`](ValueField::value) + /// * `entity`: [`Entity::ValueField`] + /// + /// # Example + /// + /// ```rust + /// use rocket::form::ValueField; + /// use rocket::form::error::{ErrorKind, Entity}; + /// + /// let field = ValueField::parse("cat.food=yum?"); + /// let error = field.unexpected(); + /// + /// assert_eq!(error.name.as_ref().unwrap(), "cat.food"); + /// assert_eq!(error.value.as_ref().unwrap(), "yum?"); + /// assert_eq!(error.kind, ErrorKind::Unexpected); + /// assert_eq!(error.entity, Entity::ValueField); + /// ``` pub fn unexpected(&self) -> Error<'v> { Error::from(ErrorKind::Unexpected) - .with_name(NameView::new(self.name.source())) + .with_name(self.name.source()) .with_value(self.value) .with_entity(Entity::ValueField) } + /// Creates a complete mising value field [`Error`] from `self`. + /// + /// The error will have the following properties: + /// * `kind`: [`ErrorKind::Missing`] + /// * `name`: [`self.name.source()`](NameView::source()) + /// * `value`: [`self.value`](ValueField::value) + /// * `entity`: [`Entity::ValueField`] + /// + /// # Example + /// + /// ```rust + /// use rocket::form::ValueField; + /// use rocket::form::error::{ErrorKind, Entity}; + /// + /// let field = ValueField::parse("cat.food=yum?"); + /// let error = field.missing(); + /// + /// assert_eq!(error.name.as_ref().unwrap(), "cat.food"); + /// assert_eq!(error.value.as_ref().unwrap(), "yum?"); + /// assert_eq!(error.kind, ErrorKind::Missing); + /// assert_eq!(error.entity, Entity::ValueField); + /// ``` pub fn missing(&self) -> Error<'v> { Error::from(ErrorKind::Missing) - .with_name(NameView::new(self.name.source())) + .with_name(self.name.source()) .with_value(self.value) .with_entity(Entity::ValueField) } } +impl<'v> DataField<'v, '_> { + /// Shift the `name` of `self` and return `self` with the shfited `name`. + /// + /// This is identical to [`ValueField::shift()`] but for `DataFields`s. See + /// [`NameView::shift()`] for the details on name "shifting". + /// + /// # Example + /// + /// ```rust + /// use rocket::form::DataField; + /// + /// fn push_data(field: DataField<'_, '_>) { + /// let shifted = field.shift(); + /// } + /// ``` + pub fn shift(mut self) -> Self { + self.name.shift(); + self + } + + /// Creates a complete unexpected data field [`Error`] from `self`. + /// + /// The error will have the following properties: + /// * `kind`: [`ErrorKind::Unexpected`] + /// * `name`: [`self.name.source()`](NameView::source()) + /// * `value`: `None` + /// * `entity`: [`Entity::DataField`] + /// + /// # Example + /// + /// ```rust + /// use rocket::form::DataField; + /// + /// fn push_data(field: DataField<'_, '_>) { + /// let error = field.unexpected(); + /// } + /// ``` + pub fn unexpected(&self) -> Error<'v> { + Error::from(ErrorKind::Unexpected) + .with_name(self.name.source()) + .with_entity(Entity::DataField) + } +} + impl<'a> From<(&'a str, &'a str)> for ValueField<'a> { fn from((name, value): (&'a str, &'a str)) -> Self { ValueField { name: NameView::new(name), value } @@ -60,16 +233,3 @@ impl<'a, 'b> PartialEq> for ValueField<'a> { self.name == other.name && self.value == other.value } } - -impl<'v> DataField<'v, '_> { - pub fn shift(mut self) -> Self { - self.name.shift(); - self - } - - pub fn unexpected(&self) -> Error<'v> { - Error::from(ErrorKind::Unexpected) - .with_name(self.name) - .with_entity(Entity::DataField) - } -} diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index 6d80ba8a..c3fd5f32 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -9,7 +9,7 @@ use crate::form::prelude::*; /// A data guard for [`FromForm`] types. /// /// This type implements the [`FromData`] trait. It provides a generic means to -/// parse arbitrary structures from incoming form data of any kind. +/// parse arbitrary structures from incoming form data. /// /// See the [forms guide](https://rocket.rs/master/guide/requests#forms) for /// general form support documentation. @@ -57,19 +57,44 @@ use crate::form::prelude::*; /// /// ## Data Limits /// -/// The default size limit for incoming form data is 32KiB. Setting a limit -/// protects your application from denial of service (DOS) attacks and from -/// resource exhaustion through high memory consumption. The limit can be -/// modified by setting the `limits.form` configuration parameter. For instance, -/// to increase the forms limit to 512KiB for all environments, you may add the -/// following to your `Rocket.toml`: +/// ### URL-Encoded Forms +/// +/// The `form` limit specifies the data limit for an entire url-encoded form +/// data. It defaults to 32KiB. URL-encoded form data is percent-decoded, stored +/// in-memory, and parsed into [`ValueField`]s. If the incoming data exceeds +/// this limit, the `Form` data guard fails without attempting to parse fields +/// with a `413: Payload Too Large` error. +/// +/// ### Multipart Forms +/// +/// The `data-form` limit specifies the data limit for an entire multipart form +/// data stream. It defaults to 2MiB. Multipart data is streamed, and form +/// fields are processed into [`DataField`]s or [`ValueField`]s as they arrive. +/// If the commulative data received while streaming exceeds the limit, parsing +/// is aborted, an error is created and pushed via [`FromForm::push_error()`], +/// and the form is finalized. +/// +/// ### Individual Fields +/// +/// Individual fields _may_ have data limits as well. The type of the field +/// determines whether there is a data limit. For instance, the `&str` type +/// imposes the `string` data limit. Consult the type's documentation or +/// [`FromFormField`] for details. +/// +/// ### Changing Limits +/// +/// To change data limits, set the `limits.form` and/or `limits.data-form` +/// configuration parameters. For instance, to increase the URL-encoded forms +/// limit to 128KiB for all environments, you might add the following to your +/// `Rocket.toml`: /// /// ```toml /// [global.limits] -/// form = 524288 +/// form = 128KiB /// ``` /// -/// See the [`Limits`](crate::data::Limits) docs for more. +/// See the [`Limits`](crate::data::Limits) and [`config`](crate::config) docs +/// for more. #[derive(Debug)] pub struct Form(T); diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index d68c797f..96b96ca1 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -532,7 +532,7 @@ impl<'v, K, V> MapContext<'v, K, V> } _ => { let error = Error::from(ErrorKind::Missing) - .with_entity(Entity::Indices) + .with_entity(Entity::Key) .with_name(name); self.errors.push(error); @@ -566,14 +566,14 @@ impl<'v, K, V> MapContext<'v, K, V> .zip(key_map.values().map(|(_, name)| name)) .filter_map(|(ctxt, name)| match K::finalize(ctxt) { Ok(value) => Some(value), - Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None } + Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None }, }); let values = values.into_iter() .zip(key_map.values().map(|(_, name)| name)) .filter_map(|(ctxt, name)| match V::finalize(ctxt) { Ok(value) => Some(value), - Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None } + Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None }, }); let map: T = keys.zip(values).collect(); diff --git a/core/lib/src/form/from_form_field.rs b/core/lib/src/form/from_form_field.rs index 4df742d0..a934f929 100644 --- a/core/lib/src/form/from_form_field.rs +++ b/core/lib/src/form/from_form_field.rs @@ -61,6 +61,7 @@ use crate::form::prelude::*; /// /// A value is validated successfully if the `from_str` method for the given /// type returns successfully. Only accepts form _values_, not binary data. +/// No data limit applies. /// /// * **`bool`** /// @@ -69,17 +70,21 @@ use crate::form::prelude::*; /// `"off"`, `"no"`, or `"false"`. Defaults to `false` otherwise. Only /// accepts form _values_, not binary data. /// +/// **No data limit applies.** +/// /// * **`&str`, `String`** /// /// The decoded form value or data is returned directly without /// modification. /// +/// **The data limit `string` applies.** +/// /// * **[`TempFile`]** /// /// Streams the form field value or data to a temporary file. See -/// [`TempFile`] for details. +/// [`TempFile`] for details and data limits. /// -/// * **[`Capped`], [`Capped`]** +/// * **[`Capped`], [`Capped`], [`Capped<&str>`]** /// /// Streams the form value or data to the inner value, succeeding even if /// the data exceeds the respective type limit by truncating the data. See @@ -91,6 +96,8 @@ use crate::form::prelude::*; /// This is the `"date"` HTML input type. Only accepts form _values_, not /// binary data. /// +/// **No data limit applies.** +/// /// * **[`time::PrimitiveDateTime`]** /// /// Parses a date in `%FT%R` or `%FT%T` format, that is, `YYYY-MM-DDTHH:MM` @@ -98,12 +105,16 @@ use crate::form::prelude::*; /// without support for the millisecond variant. Only accepts form _values_, /// not binary data. /// +/// **No data limit applies.** +/// /// * **[`time::Time`]** /// /// Parses a time in `%R` or `%T` format, that is, `HH:MM` or `HH:MM:SS`. /// This is the `"time"` HTML input type without support for the millisecond /// variant. Only accepts form _values_, not binary data. /// +/// **No data limit applies.** +/// /// [`TempFile`]: crate::data::TempFile /// /// # Implementing @@ -240,7 +251,7 @@ pub struct FromFieldContext<'v, T: FromFormField<'v>> { } impl<'v, T: FromFormField<'v>> FromFieldContext<'v, T> { - fn can_push(&mut self) -> bool { + fn should_push(&mut self) -> bool { self.pushes += 1; self.value.is_none() } @@ -273,14 +284,14 @@ impl<'v, T: FromFormField<'v>> FromForm<'v> for T { } fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { - if ctxt.can_push() { + if ctxt.should_push() { ctxt.field_value = Some(field.value); ctxt.push(field.name, Self::from_value(field)) } } async fn push_data(ctxt: &mut FromFieldContext<'v, T>, field: DataField<'v, '_>) { - if ctxt.can_push() { + if ctxt.should_push() { ctxt.push(field.name, Self::from_data(field).await); } } diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index 3a33090b..09acf0db 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -1,5 +1,8 @@ //! Parsing and validation of HTTP forms and fields. //! +//! See the [forms guide](https://rocket.rs/master/guide/requests#forms) for +//! general form support documentation. +//! //! # Field Wire Format //! //! Rocket's field wire format is a flexible, non-self-descriptive, text-based @@ -17,13 +20,13 @@ //! //! indices := index (':' index)* //! -//! index := STRING except ':' +//! index := STRING except ':', ']' //! //! value := STRING //! ``` //! //! Each field name consists of any number of `key`s and at most one `value`. -//! Keys are delimited by `[]` or `.`. A `key` consists of indices delimited by +//! Keys are delimited by `[` or `.`. A `key` consists of indices delimited by //! `:`. //! //! The meaning of a key or index is type-dependent, hence the format is @@ -51,7 +54,7 @@ //! multipart forms are supported. All url-encoded form fields are preprocessed //! as [`ValueField`]s. Multipart form fields with Content-Types are processed //! as [`DataField`]s while those without a set Content-Type are processed as -//! [`ValueField`]s. +//! [`ValueField`]s. `ValueField` field names and values are percent-decoded. //! //! # Data Limits //! @@ -68,372 +71,379 @@ //! imposes its own limits. For example, the `&str` form guard imposes a data //! limit of `string` when multipart data is streamed. //! -//! See the [`Limits`](crate::data::Limits) docs for more. +//! See the [`Limits`](crate::data::Limits) and [`Form#data-limits`] docs for +//! more. //! -/// # Examples -/// -/// The following examples use `f1=v1&f2=v2` to illustrate field/value pairs -/// `(f1, v2)` and `(f2, v2)`. This is the same encoding used to send HTML forms -/// over HTTP but Rocket's push-parsers are unaware of any specific encoding, -/// dealing only with logical `field`s, `index`es, and `value`s. -/// -/// ## A Single Value (`T: FormFormValue`) -/// -/// The simplest example parses a single value of type `T` from a string with an -/// optional default value: this is `impl FromForm for T`: -/// -/// 1. **Initialization.** The context stores parsing options and an `Option` -/// of `Result` for storing the `result` of parsing `T`, which is -/// initially set to `None`. -/// -/// ```rust,ignore -/// struct Context { -/// opts: FormOptions, -/// result: Option>, -/// } -/// ``` -/// -/// 2. **Push.** The field is treated as a string. If `context.result` is `None`, -/// `T` is parsed from `field`, and the result is stored in `context.result`. -/// -/// ```rust,ignore -/// fn push(this: &mut Self::Context, field: FormField<'v>) { -/// if this.result.is_none() { -/// this.result = Some(Self::from_value(field)); -/// } -/// } -/// ``` -/// -/// 3. **Finalization.** If `context.result` is `None` and `T` has a default, -/// the default is returned; otherwise a `Missing` error is returned. If -/// `context.result` is `Some(v)`, the result `v` is returned. -/// -/// ```rust,ignore -/// fn finalize(this: Self::Context) -> Result { -/// match this.result { -/// Some(value) => Ok(value), -/// None => match ::default() { -/// Some(default) => Ok(default), -/// None => Err(Error::Missing) -/// } -/// } -/// } -/// ``` -/// -/// This implementation is complete, barring checking for duplicate pushes when -/// paring is requested as `strict`. -/// -/// ## Maps w/named Fields (`struct`) -/// -/// A `struct` with named fields parses values of multiple types, indexed by the -/// name of its fields: -/// -/// ```rust,ignore -/// struct Dog { name: String, barks: bool, friends: Vec, } -/// struct Cat { name: String, meows: bool } -/// ``` -/// -/// Candidates for parsing into a `Dog` include: -/// -/// * `name=Fido&barks=0` -/// -/// `Dog { "Fido", false }` -/// -/// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0` -/// `name=Fido&barks=1&friends[0].name=Sally&friends[0].meows=0` -/// `name=Fido&barks=1&friends.0.name=Sally&friends.0.meows=0` -/// -/// `Dog { "Fido", true, vec![Cat { "Sally", false }] }` -/// -/// Parsers for structs are code-generated to proceed as follows: -/// -/// 1. **Initialization.** The context stores parsing options, a `T::Context` -/// for each field of type `T`, and a vector called `extra`. -/// -/// ```rust,ignore -/// struct Context<'v> { -/// opts: FormOptions, -/// field_a: A::Context, -/// field_b: B::Context, -/// /* ... */ -/// extra: Vec> -/// } -/// ``` -/// -/// 2. **Push.** The index of the first key is compared to known field names. -/// If none matches, the index is added to `extra`. Otherwise the key is -/// stripped from the field, and the remaining field is pushed to `T`. -/// -/// ```rust,ignore -/// fn push(this: &mut Self::Context, field: FormField<'v>) { -/// match field.key() { -/// "field_a" => A::push(&mut this.field_a, field.next()), -/// "field_b" => B::push(&mut this.field_b, field.next()), -/// /* ... */ -/// _ => this.extra.push(field) -/// } -/// } -/// ``` -/// -/// 3. **Finalization.** Every context is finalized; errors and `Ok` values -/// are collected. If parsing is strict and extras is non-empty, an error -/// added to the collection of errors. If there are no errors, all `Ok` -/// values are used to create the `struct`, and the created struct is -/// returned. Otherwise, `Err(errors)` is returned. -/// -/// ```rust,ignore -/// fn finalize(mut this: Self::Context) -> Result { -/// let mut errors = vec![]; -/// -/// let field_a = A::finalize(&mut this.field_a) -/// .map_err(|e| errors.push(e)) -/// .map(Some).unwrap_or(None); -/// -/// let field_b = B::finblize(&mut this.field_b) -/// .map_err(|e| errors.push(e)) -/// .map(Some).unwrap_or(None); -/// -/// /* .. */ -/// -/// if !errors.is_empty() { -/// return Err(Values(errors)); -/// } else if this.opts.is_strict() && !this.extra.is_empty() { -/// return Err(Extra(this.extra)); -/// } else { -/// // NOTE: All unwraps will succeed since `errors.is_empty()`. -/// Struct { -/// field_a: field_a.unwrap(), -/// field_b: field_b.unwrap(), -/// /* .. */ -/// } -/// } -/// } -/// ``` -/// -/// ## Sequences: (`Vec`) -/// -/// A `Vec` invokes `T`'s push-parser on every push, adding instances -/// of `T` to an internal vector. The instance of `T` whose parser is invoked -/// depends on the index of the first key: -/// -/// * if it is the first push, the index differs from the previous, or there is no -/// index, a new `T::Context` is `init`ialized and added to the internal vector -/// * if the index matches the previously seen index, the last initialized -/// `T::Context` is `push`ed to. -/// -/// For instance, the sequentially pushed values `=1`, `=2`, and `=3` for a -/// `Vec` (or any other integer) is expected to parse as `vec![1, 2, 3]`. The -/// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields -/// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the -/// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both -/// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T` -/// should be parsed using the field's value. -/// -/// If, instead, the index was non-empty and equal to the index of the field in the -/// _previous_ push, `Vec` pushes the value to the parser of the previously parsed -/// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results -/// in `vec![1, 3]` (see [`FromFormValue`]). -/// -/// This generalizes. Consider a `Vec>` named `x`, so `x` and an -/// optional `=` are stripped before being passed to `Vec`'s push-parser: -/// -/// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]` -/// -/// Every push (`1`, `2`, `3`) has no key, thus no index: a new `T` (here, -/// `Vec`) is thus initialized for every `push()` and passed the -/// value (here, `1`, `2`, and `3`). Each of these `push`es proceeds -/// recursively: every push again has no key, thus no index, so a new `T` is -/// initialized for every push (now a `usize`), which finally parse as -/// integers `1`, `2`, and `3`. -/// -/// Note: `x=1&x=2&x=3` _also_ can also parse as `vec![1, 2, 3]` when viewed -/// as a `Vec`; this is the non-self-descriptive part of the format. -/// -/// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![2], vec![3]]` -/// -/// This proceeds nearly identically to the previous example, with the exception -/// that the top-level `Vec` sees the values `[]=1`, `[]=2`, and `[]=3`. -/// -/// * `x[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]` -/// -/// The top-level `Vec` sees the values `[0]=1`, `[0]=2`, and `[]=3`. The first -/// value results in a new `Vec` being initialized, as before, which is -/// pushed a `1`. The second value has the same index as the first, `0`, and so -/// `2` is pushed to the previous `T`, the `Vec` which contains the `1`. -/// Finally, the third value has no index, so a new `Vec` is initialized -/// and pushed a `3`. -/// -/// * `x[0]=1&x[0]=2&x[]=3&x[]=4` parses as `vec![vec![1, 2], vec![3], vec![4]]` -/// * `x[0]=1&x[0]=2&x[1]=3&x[1]=4` parses as `vec![vec![1, 2], vec![3, 4]]` -/// -/// The indexing kind `[]` is purely by convention: the first two examples are -/// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`. -/// -/// The parser proceeds as follows: -/// -/// 1. **Initialization.** The context stores parsing options, the -/// `last_index` encountered in a `push`, an `Option` of a `T::Context` for -/// the `current` value being parsed, a `Vec` of `errors`, and -/// finally a `Vec` of already parsed `items`. -/// -/// ```rust,ignore -/// struct VecContext<'v, T: FromForm<'v>> { -/// opts: FormOptions, -/// last_index: Index<'v>, -/// current: Option, -/// errors: Vec, -/// items: Vec -/// } -/// ``` -/// -/// 2. **Push.** The index of the first key is compared against `last_index`. -/// If it differs, a new context for `T` is created and the previous is -/// finalized. The `Ok` result from finalization is stored in `items` and -/// the `Err` in `errors`. Otherwise the `index` is the same, the `current` -/// context is retrieved, and the field stripped of the current key is -/// pushed to `T`. `last_index` is updated. -/// -/// ```rust,ignore -/// fn push(this: &mut Self::Context, field: FormField<'v>) { -/// if this.last_index != field.index() { -/// this.shift(); // finalize `current`, add to `items`, `errors` -/// let mut context = T::init(this.opts); -/// T::push(&mut context, field.next()); -/// this.current = Some(context); -/// } else { -/// let context = this.current.as_mut(); -/// T::push(context, field.next()) -/// } -/// -/// this.last_index = field.index(); -/// } -/// ``` -/// -/// 3. **Finalization.** Any `current` context is finalized, storing the `Ok` -/// or `Err` as before. `Ok(items)` is returned if `errors` is empty, -/// otherwise `Err(errors)` is returned. -/// -/// ```rust,ignore -/// fn finalize(mut this: Self::Context) -> Result { -/// this.shift(); // finalizes `current`, as before. -/// match this.errors.is_empty() { -/// true => Ok(this.items), -/// false => Err(this.errors) -/// } -/// } -/// ``` -/// -/// ## Arbitrary Maps (`HashMap`) -/// -/// A `HashMap` can be parsed from keys with one index or, for composite -/// key values, such as structures or sequences, multiple indices. We begin with -/// a discussion of the simpler case: non-composite keys. -/// -/// ### Non-Composite Keys -/// -/// A non-composite value can be described by a single field with no indices. -/// Strings and integers are examples of non-composite values. The push-parser -/// for `HashMap` for a non-composite `K` uses the index of the first key -/// as the value of `K`; the remainder of the field is pushed to `V`'s parser: -/// -/// 1. **Initialization.** The context stores a column-based representation of -/// `keys` and `values`, a `key_map` from a string key to the column index, -/// an `errors` vector for storing errors as they arise, and the parsing -/// options. -/// -/// ```rust,ignore -/// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> { -/// opts: FormOptions, -/// key_map: HashMap<&'v str, usize>, -/// keys: Vec, -/// values: Vec, -/// errors: Vec>, -/// } -/// ``` -/// -/// 2. **Push.** The `key_map` index for the key associated with the index of -/// the first key in the field is retrieved. If such a key has not yet been -/// seen, a new key and value context are created, the key is pushed to -/// `K`'s parser, and the field minus the first key is pushed to `V`'s -/// parser. -/// -/// ```rust,ignore -/// fn push(this: &mut Self::Context, field: FormField<'v>) { -/// let key = field.index(); -/// let value_context = match this.key_map.get(Key) { -/// Some(i) => &mut this.values[i], -/// None => { -/// let i = this.keys.len(); -/// this.key_map.insert(key, i); -/// this.keys.push(K::init(this.opts)); -/// this.values.push(V::init(this.opts)); -/// K::push(&mut this.keys[i], key.into()); -/// &mut this.values[i] -/// } -/// }; -/// -/// V::push(value_context, field.next()); -/// } -/// ``` -/// -/// 3. **Finalization.** All key and value contexts are finalized; any errors -/// are collected in `errors`. If there are no errors, `keys` and `values` -/// are collected into a `HashMap` and returned. Otherwise, the errors are -/// returned. -/// -/// ```rust,ignore -/// fn finalize(mut this: Self::Context) -> Result { -/// this.finalize_keys(); -/// this.finalize_values(); -/// if this.errors.is_empty() { -/// Ok(this.keys.into_iter().zip(this.values.into_iter()).collect()) -/// } else { -/// Err(this.errors) -/// } -/// } -/// ``` -/// -/// Examples of forms parseable via this parser are: -/// -/// * `x[0].name=Bob&x[0].meows=true`as a `HashMap` parses with -/// `0` mapping to `Cat { name: "Bob", meows: true }` -/// * `x[0]name=Bob&x[0]meows=true`as a `HashMap` parses just as -/// above. -/// * `x[0]=Bob&x[0]=Sally&x[1]=Craig`as a `HashMap>` -/// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`. -/// -/// A `HashMap` can be thought of as a vector of key-value pairs: `Vec<(K, -/// V)` (row-based) or equivalently, as two vectors of keys and values: `Vec` -/// and `Vec` (column-based). The implication is that indexing into a -/// specific key or value requires _two_ indexes: the first to determine whether -/// a key or value is being indexed to, and the second to determine _which_ key -/// or value. The push-parser for maps thus optionally accepts two indexes for a -/// single key to allow piece-by-piece build-up of arbitrary keys and values. -/// -/// The parser proceeds as follows: -/// -/// 1. **Initialization.** The context stores parsing options, a vector of -/// `key_contexts: Vec`, a vector of `value_contexts: -/// Vec`, a `mapping` from a string index to an integer index -/// into the `contexts`, and a vector of `errors`. -/// 2. **Push.** An index is required; an error is emitted and `push` returns -/// if they field's first key does not contain an index. If the first key -/// contains _one_ index, a new `K::Context` and `V::Context` are created. -/// The key is pushed as the value to `K` and the remaining field as the -/// value to `V`. The key and value are finalized; if both succeed, the key -/// and value are stored in `keys` and `values`; otherwise the error(s) is -/// stored in `errors`. -/// -/// If the first keys contains _two_ indices, the first must starts with -/// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by -/// `second`; the integer is retrieved. If none exists, new contexts are -/// created an added to `{key,value}_contexts`, and their index is mapped -/// to `second` in `mapping`. If the first index is `k`, the field, -/// stripped of the first key, is pushed to the key's context; the same is -/// done for the value's context is the first index is `v`. -/// 3. **Finalization.** Every context is finalized; errors and `Ok` values -/// are collected. TODO: FINISH. Split this into two: one for single-index, -/// another for two-indices. +//! # A Simple Example +//! +//! The following example uses `f1=v1&f2=v2` to illustrate field/value pairs +//! `(f1, v2)` and `(f2, v2)`. This is the same encoding used to send HTML forms +//! over HTTP but Rocket's push-parsers are unaware of any specific encoding, +//! dealing only with logical `field`s, `index`es, and `value`s. +//! +//! ## A Single Field (`T: FormFormField`) +//! +//! The simplest example parses a single value of type `T` from a string with an +//! optional default value: this is `impl FromForm for T`: +//! +//! 1. **Initialization.** The context stores form options and an `Option` of +//! `Result` for storing the `result` of parsing `T`, which +//! is initially set to `None`. +//! +//! ```rust,ignore +//! struct Context { +//! opts: Options, +//! result: Option>, +//! } +//! ``` +//! +//! 2. **Push.** If `context.result` is `None`, `T` is parsed from `field`, +//! and the result is stored in `context.result`. Otherwise a field has +//! already been parsed and nothing is done. +//! +//! ```rust,ignore +//! fn push(ctxt: &mut Self::Context, field: Field<'v>) { +//! if ctxt.result.is_none() { +//! ctxt.result = Some(Self::from_field(field)); +//! } +//! } +//! ``` +//! +//! 3. **Finalization.** If `ctxt.result` is `None` and `T` has a default, +//! the default is returned. Otherwise a `Missing` error is returned. If +//! `ctxt.result` is `Some(v)`, the result `v` is returned. +//! +//! ```rust,ignore +//! fn finalize(ctxt: Self::Context) -> Result { +//! match ctxt.result { +//! Some(value) => Ok(value), +//! None => match ::default() { +//! Some(default) => Ok(default), +//! None => Err(ErrorKind::Missing)?, +//! } +//! } +//! } +//! ``` +//! +//! This implementation is complete except for the following details: +//! +//! * not being pseudocode, of course +//! * checking for duplicate pushes when paring is requested as `strict` +//! * tracking the field's name and value to generate a complete `Error` +//! +//! See [`FromForm`] for full details on push-parsing and a complete example. + +// ## Maps w/named Fields (`struct`) +// +// A `struct` with named fields parses values of multiple types, indexed by the +// name of its fields: +// +// ```rust,ignore +// struct Dog { name: String, barks: bool, friends: Vec, } +// struct Cat { name: String, meows: bool } +// ``` +// +// Candidates for parsing into a `Dog` include: +// +// * `name=Fido&barks=0` +// +// `Dog { "Fido", false }` +// +// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0` +// `name=Fido&barks=1&friends[0].name=Sally&friends[0].meows=0` +// `name=Fido&barks=1&friends.0.name=Sally&friends.0.meows=0` +// +// `Dog { "Fido", true, vec![Cat { "Sally", false }] }` +// +// Parsers for structs are code-generated to proceed as follows: +// +// 1. **Initialization.** The context stores parsing options, a `T::Context` +// for each field of type `T`, and a vector called `extra`. +// +// ```rust,ignore +// struct Context<'v> { +// opts: FormOptions, +// field_a: A::Context, +// field_b: B::Context, +// /* ... */ +// extra: Vec> +// } +// ``` +// +// 2. **Push.** The index of the first key is compared to known field names. +// If none matches, the index is added to `extra`. Otherwise the key is +// stripped from the field, and the remaining field is pushed to `T`. +// +// ```rust,ignore +// fn push(this: &mut Self::Context, field: FormField<'v>) { +// match field.key() { +// "field_a" => A::push(&mut this.field_a, field.next()), +// "field_b" => B::push(&mut this.field_b, field.next()), +// /* ... */ +// _ => this.extra.push(field) +// } +// } +// ``` +// +// 3. **Finalization.** Every context is finalized; errors and `Ok` values +// are collected. If parsing is strict and extras is non-empty, an error +// added to the collection of errors. If there are no errors, all `Ok` +// values are used to create the `struct`, and the created struct is +// returned. Otherwise, `Err(errors)` is returned. +// +// ```rust,ignore +// fn finalize(mut this: Self::Context) -> Result { +// let mut errors = vec![]; +// +// let field_a = A::finalize(&mut this.field_a) +// .map_err(|e| errors.push(e)) +// .map(Some).unwrap_or(None); +// +// let field_b = B::finblize(&mut this.field_b) +// .map_err(|e| errors.push(e)) +// .map(Some).unwrap_or(None); +// +// /* .. */ +// +// if !errors.is_empty() { +// return Err(Values(errors)); +// } else if this.opts.is_strict() && !this.extra.is_empty() { +// return Err(Extra(this.extra)); +// } else { +// // NOTE: All unwraps will succeed since `errors.is_empty()`. +// Struct { +// field_a: field_a.unwrap(), +// field_b: field_b.unwrap(), +// /* .. */ +// } +// } +// } +// ``` +// +// ## Sequences: (`Vec`) +// +// A `Vec` invokes `T`'s push-parser on every push, adding instances +// of `T` to an internal vector. The instance of `T` whose parser is invoked +// depends on the index of the first key: +// +// * if it is the first push, the index differs from the previous, or there is no +// index, a new `T::Context` is `init`ialized and added to the internal vector +// * if the index matches the previously seen index, the last initialized +// `T::Context` is `push`ed to. +// +// For instance, the sequentially pushed values `=1`, `=2`, and `=3` for a +// `Vec` (or any other integer) is expected to parse as `vec![1, 2, 3]`. The +// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields +// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the +// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both +// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T` +// should be parsed using the field's value. +// +// If, instead, the index was non-empty and equal to the index of the field in the +// _previous_ push, `Vec` pushes the value to the parser of the previously parsed +// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results +// in `vec![1, 3]` (see [`FromFormValue`]). +// +// This generalizes. Consider a `Vec>` named `x`, so `x` and an +// optional `=` are stripped before being passed to `Vec`'s push-parser: +// +// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]` +// +// Every push (`1`, `2`, `3`) has no key, thus no index: a new `T` (here, +// `Vec`) is thus initialized for every `push()` and passed the +// value (here, `1`, `2`, and `3`). Each of these `push`es proceeds +// recursively: every push again has no key, thus no index, so a new `T` is +// initialized for every push (now a `usize`), which finally parse as +// integers `1`, `2`, and `3`. +// +// Note: `x=1&x=2&x=3` _also_ can also parse as `vec![1, 2, 3]` when viewed +// as a `Vec`; this is the non-self-descriptive part of the format. +// +// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![2], vec![3]]` +// +// This proceeds nearly identically to the previous example, with the exception +// that the top-level `Vec` sees the values `[]=1`, `[]=2`, and `[]=3`. +// +// * `x[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]` +// +// The top-level `Vec` sees the values `[0]=1`, `[0]=2`, and `[]=3`. The first +// value results in a new `Vec` being initialized, as before, which is +// pushed a `1`. The second value has the same index as the first, `0`, and so +// `2` is pushed to the previous `T`, the `Vec` which contains the `1`. +// Finally, the third value has no index, so a new `Vec` is initialized +// and pushed a `3`. +// +// * `x[0]=1&x[0]=2&x[]=3&x[]=4` parses as `vec![vec![1, 2], vec![3], vec![4]]` +// * `x[0]=1&x[0]=2&x[1]=3&x[1]=4` parses as `vec![vec![1, 2], vec![3, 4]]` +// +// The indexing kind `[]` is purely by convention: the first two examples are +// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`. +// +// The parser proceeds as follows: +// +// 1. **Initialization.** The context stores parsing options, the +// `last_index` encountered in a `push`, an `Option` of a `T::Context` for +// the `current` value being parsed, a `Vec` of `errors`, and +// finally a `Vec` of already parsed `items`. +// +// ```rust,ignore +// struct VecContext<'v, T: FromForm<'v>> { +// opts: FormOptions, +// last_index: Index<'v>, +// current: Option, +// errors: Vec, +// items: Vec +// } +// ``` +// +// 2. **Push.** The index of the first key is compared against `last_index`. +// If it differs, a new context for `T` is created and the previous is +// finalized. The `Ok` result from finalization is stored in `items` and +// the `Err` in `errors`. Otherwise the `index` is the same, the `current` +// context is retrieved, and the field stripped of the current key is +// pushed to `T`. `last_index` is updated. +// +// ```rust,ignore +// fn push(this: &mut Self::Context, field: FormField<'v>) { +// if this.last_index != field.index() { +// this.shift(); // finalize `current`, add to `items`, `errors` +// let mut context = T::init(this.opts); +// T::push(&mut context, field.next()); +// this.current = Some(context); +// } else { +// let context = this.current.as_mut(); +// T::push(context, field.next()) +// } +// +// this.last_index = field.index(); +// } +// ``` +// +// 3. **Finalization.** Any `current` context is finalized, storing the `Ok` +// or `Err` as before. `Ok(items)` is returned if `errors` is empty, +// otherwise `Err(errors)` is returned. +// +// ```rust,ignore +// fn finalize(mut this: Self::Context) -> Result { +// this.shift(); // finalizes `current`, as before. +// match this.errors.is_empty() { +// true => Ok(this.items), +// false => Err(this.errors) +// } +// } +// ``` +// +// ## Arbitrary Maps (`HashMap`) +// +// A `HashMap` can be parsed from keys with one index or, for composite +// key values, such as structures or sequences, multiple indices. We begin with +// a discussion of the simpler case: non-composite keys. +// +// ### Non-Composite Keys +// +// A non-composite value can be described by a single field with no indices. +// Strings and integers are examples of non-composite values. The push-parser +// for `HashMap` for a non-composite `K` uses the index of the first key +// as the value of `K`; the remainder of the field is pushed to `V`'s parser: +// +// 1. **Initialization.** The context stores a column-based representation of +// `keys` and `values`, a `key_map` from a string key to the column index, +// an `errors` vector for storing errors as they arise, and the parsing +// options. +// +// ```rust,ignore +// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> { +// opts: FormOptions, +// key_map: HashMap<&'v str, usize>, +// keys: Vec, +// values: Vec, +// errors: Vec>, +// } +// ``` +// +// 2. **Push.** The `key_map` index for the key associated with the index of +// the first key in the field is retrieved. If such a key has not yet been +// seen, a new key and value context are created, the key is pushed to +// `K`'s parser, and the field minus the first key is pushed to `V`'s +// parser. +// +// ```rust,ignore +// fn push(this: &mut Self::Context, field: FormField<'v>) { +// let key = field.index(); +// let value_context = match this.key_map.get(Key) { +// Some(i) => &mut this.values[i], +// None => { +// let i = this.keys.len(); +// this.key_map.insert(key, i); +// this.keys.push(K::init(this.opts)); +// this.values.push(V::init(this.opts)); +// K::push(&mut this.keys[i], key.into()); +// &mut this.values[i] +// } +// }; +// +// V::push(value_context, field.next()); +// } +// ``` +// +// 3. **Finalization.** All key and value contexts are finalized; any errors +// are collected in `errors`. If there are no errors, `keys` and `values` +// are collected into a `HashMap` and returned. Otherwise, the errors are +// returned. +// +// ```rust,ignore +// fn finalize(mut this: Self::Context) -> Result { +// this.finalize_keys(); +// this.finalize_values(); +// if this.errors.is_empty() { +// Ok(this.keys.into_iter().zip(this.values.into_iter()).collect()) +// } else { +// Err(this.errors) +// } +// } +// ``` +// +// Examples of forms parseable via this parser are: +// +// * `x[0].name=Bob&x[0].meows=true`as a `HashMap` parses with +// `0` mapping to `Cat { name: "Bob", meows: true }` +// * `x[0]name=Bob&x[0]meows=true`as a `HashMap` parses just as +// above. +// * `x[0]=Bob&x[0]=Sally&x[1]=Craig`as a `HashMap>` +// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`. +// +// A `HashMap` can be thought of as a vector of key-value pairs: `Vec<(K, +// V)` (row-based) or equivalently, as two vectors of keys and values: `Vec` +// and `Vec` (column-based). The implication is that indexing into a +// specific key or value requires _two_ indexes: the first to determine whether +// a key or value is being indexed to, and the second to determine _which_ key +// or value. The push-parser for maps thus optionally accepts two indexes for a +// single key to allow piece-by-piece build-up of arbitrary keys and values. +// +// The parser proceeds as follows: +// +// 1. **Initialization.** The context stores parsing options, a vector of +// `key_contexts: Vec`, a vector of `value_contexts: +// Vec`, a `mapping` from a string index to an integer index +// into the `contexts`, and a vector of `errors`. +// 2. **Push.** An index is required; an error is emitted and `push` returns +// if they field's first key does not contain an index. If the first key +// contains _one_ index, a new `K::Context` and `V::Context` are created. +// The key is pushed as the value to `K` and the remaining field as the +// value to `V`. The key and value are finalized; if both succeed, the key +// and value are stored in `keys` and `values`; otherwise the error(s) is +// stored in `errors`. +// +// If the first keys contains _two_ indices, the first must starts with +// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by +// `second`; the integer is retrieved. If none exists, new contexts are +// created an added to `{key,value}_contexts`, and their index is mapped +// to `second` in `mapping`. If the first index is `k`, the field, +// stripped of the first key, is pushed to the key's context; the same is +// done for the value's context is the first index is `v`. +// 3. **Finalization.** Every context is finalized; errors and `Ok` values +// are collected. TODO: FINISH. Split this into two: one for single-index, +// another for two-indices. mod field; mod options; @@ -450,6 +460,7 @@ pub mod error; #[cfg(test)] mod tests; +/// Type alias for `Result` with an error type of [`Errors`]. pub type Result<'v, T> = std::result::Result>; #[doc(hidden)] diff --git a/core/lib/src/form/name.rs b/core/lib/src/form/name.rs index 83f0c962..54635415 100644 --- a/core/lib/src/form/name.rs +++ b/core/lib/src/form/name.rs @@ -66,7 +66,7 @@ impl Name { type Item = &'v Key; fn next(&mut self) -> Option { - if self.0.is_terminal() { + if self.0.exhausted() { return None; } @@ -102,7 +102,7 @@ impl Name { type Item = &'v Name; fn next(&mut self) -> Option { - if self.0.is_terminal() { + if self.0.exhausted() { return None; } @@ -476,16 +476,55 @@ impl<'v> NameView<'v> { /// Shifts the current key once to the right. /// - /// # Example + /// # Examples /// /// ```rust /// use rocket::form::name::NameView; /// - /// let mut view = NameView::new("a.b[c:d]"); + /// let mut view = NameView::new("a.b[c:d][d.e]"); /// assert_eq!(view.key().unwrap(), "a"); /// /// view.shift(); /// assert_eq!(view.key().unwrap(), "b"); + /// + /// view.shift(); + /// assert_eq!(view.key().unwrap(), "c:d"); + /// + /// view.shift(); + /// assert_eq!(view.key().unwrap(), "d.e"); + /// ``` + /// + /// Malformed strings can have interesting results: + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a[c.d"); + /// assert_eq!(view.key_lossy(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.key_lossy(), "c.d"); + /// + /// let mut view = NameView::new("a[c[.d]"); + /// assert_eq!(view.key_lossy(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.key_lossy(), "c[.d"); + /// + /// view.shift(); + /// assert_eq!(view.key(), None); + /// + /// let mut view = NameView::new("foo[c[.d]]"); + /// assert_eq!(view.key_lossy(), "foo"); + /// + /// view.shift(); + /// assert_eq!(view.key_lossy(), "c[.d"); + /// + /// view.shift(); + /// assert_eq!(view.key_lossy(), "]"); + /// + /// view.shift(); + /// assert_eq!(view.key(), None); /// ``` pub fn shift(&mut self) { const START_DELIMS: &'static [char] = &['.', '[']; @@ -494,13 +533,10 @@ impl<'v> NameView<'v> { let bytes = string.as_bytes(); let shift = match bytes.get(0) { None | Some(b'=') => 0, - Some(b'[') => match string[1..].find(&[']', '.'][..]) { - Some(j) => match string[1..].as_bytes()[j] { - b']' => j + 2, - _ => j + 1, - } + Some(b'[') => match memchr::memchr(b']', bytes) { + Some(j) => j + 1, None => bytes.len(), - } + }, Some(b'.') => match string[1..].find(START_DELIMS) { Some(j) => j + 1, None => bytes.len(), @@ -569,6 +605,7 @@ impl<'v> NameView<'v> { let key = match view.as_bytes().get(0) { Some(b'.') => &view[1..], Some(b'[') if view.ends_with(']') => &view[1..view.len() - 1], + Some(b'[') if self.is_at_last() => &view[1..], _ => view }; @@ -643,7 +680,13 @@ impl<'v> NameView<'v> { self.name } - fn is_terminal(&self) -> bool { + // This is the last key. The next `shift()` will exhaust `self`. + fn is_at_last(&self) -> bool { + self.end == self.name.len() + } + + // There are no more keys. A `shift` will do nothing. + fn exhausted(&self) -> bool { self.start == self.name.len() } } diff --git a/core/lib/src/form/options.rs b/core/lib/src/form/options.rs index 9df4d335..c6043dc7 100644 --- a/core/lib/src/form/options.rs +++ b/core/lib/src/form/options.rs @@ -1,11 +1,17 @@ +/// Form guard options. +/// +/// See [`Form#leniency`](crate::form::Form#leniency) for details. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Options { + /// Whether parsing should be strict (no extra parameters) or not. pub strict: bool, } #[allow(non_upper_case_globals, dead_code)] impl Options { + /// `Options` with `strict` set to `false`. pub const Lenient: Self = Options { strict: false }; + /// `Options` with `strict` set to `true`. pub const Strict: Self = Options { strict: true }; } diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs index 895e7757..9f0a97a4 100644 --- a/core/lib/src/form/parser.rs +++ b/core/lib/src/form/parser.rs @@ -148,6 +148,8 @@ mod raw_str_parse_tests { } impl<'r, 'i> MultipartParser<'r, 'i> { + /// Returns `None` when there are no further fields. Otherwise tries to + /// parse the next multipart form field and returns the result. async fn next(&mut self) -> Option>> { if self.done { return None; diff --git a/core/lib/src/form/validate.rs b/core/lib/src/form/validate.rs index ffd53f7d..c06afd74 100644 --- a/core/lib/src/form/validate.rs +++ b/core/lib/src/form/validate.rs @@ -1,68 +1,332 @@ -use std::{borrow::Cow, convert::TryInto, fmt::Display, ops::{RangeBounds, Bound}}; +//! Form field validation routines. +//! +//! Each function in this module can be used as the target of the +//! `field(validate)` field attribute of the `FromForm` derive. +//! +//! ```rust +//! use rocket::form::FromForm; +//! +//! #[derive(FromForm)] +//! struct MyForm<'r> { +//! #[field(validate = range(2..10))] +//! id: usize, +//! #[field(validate = omits("password"))] +//! password: &'r str, +//! } +//! ``` +//! +//! The `validate` parameter takes any expression that returns a +//! [`form::Result<()>`](crate::form::Result). If the expression is a function +//! call, a reference to the field is inserted as the first parameter. Thus, +//! functions calls to `validate` must take a reference to _some_ type, +//! typically a generic with some bounds, as their first argument. +//! +//! ## Custom Error Messages +//! +//! To set a custom error messages, it is useful to chain results: +//! +//! ```rust +//! use rocket::form::{Errors, Error, FromForm}; +//! +//! #[derive(FromForm)] +//! struct MyForm<'r> { +//! // By defining another function... +//! #[field(validate = omits("password").map_err(pass_help))] +//! password: &'r str, +//! // or inline using the `msg` helper. +//! #[field(validate = omits("password").or_else(msg!("please omit `password`")))] +//! password2: &'r str, +//! // You can even refer to the field in the message... +//! #[field(validate = range(1..).or_else(msg!("`{}` < 1", self.n)))] +//! n: isize, +//! // ..or other fields! +//! #[field(validate = range(..self.n).or_else(msg!("`{}` > `{}`", self.z, self.n)))] +//! z: isize, +//! } +//! +//! fn pass_help<'a>(errors: Errors<'_>) -> Errors<'a> { +//! Error::validation("passwords can't contain the text \"password\"").into() +//! } +//! ``` +//! +//! ## Custom Validation +//! +//! Custom validation routines can be defined as regular functions. Consider a +//! routine that tries to validate a credit card number: +//! +//! ```rust +//! extern crate time; +//! +//! use rocket::form::{self, FromForm, Error}; +//! +//! #[derive(FromForm)] +//! struct CreditCard { +//! #[field(validate = luhn(self.cvv, &self.expiration))] +//! number: u64, +//! cvv: u16, +//! expiration: time::Date, +//! } +//! +//! // Implementation of Luhn validator. +//! fn luhn<'v>(number: &u64, cvv: u16, exp: &time::Date) -> form::Result<'v, ()> { +//! # let valid = false; +//! if !valid { +//! Err(Error::validation("invalid credit card number"))?; +//! } +//! +//! Ok(()) +//! } +//! ``` +use std::borrow::Cow; +use std::convert::TryInto; +use std::ops::{RangeBounds, Bound}; +use std::fmt::Debug; + +use crate::data::ByteUnit; use rocket_http::ContentType; -use crate::{data::TempFile, form::error::{Error, Errors}}; +use crate::{data::TempFile, form::{Result, Error}}; -pub fn eq<'v, A, B>(a: &A, b: B) -> Result<(), Errors<'v>> +/// A helper macro for custom validation error messages. +/// +/// The macro works identically to [`std::format!`] except it does not allocate +/// when the expression is a string literal. It returns a function (a closure) +/// that takes one parameter and evaluates to an `Err` of validation [`Error`] +/// with the formatted message. While useful in other contexts, it is designed +/// to be chained to validation results via `.or_else()` and `.and_then()`. +/// +/// Note that the macro never needs to be imported when used with a `FromForm` +/// derive; all items in [`form::validate`](crate::form::validate) are already +/// in scope. +/// +/// # Example +/// +/// ```rust +/// use rocket::form::FromForm; +/// +/// #[derive(FromForm)] +/// struct Person<'r> { +/// #[field(validate = len(3..).or_else(msg!("that's a short name...")))] +/// name: &'r str, +/// #[field(validate = contains('f').and_then(msg!("please, no `f`!")))] +/// non_f_name: &'r str, +/// } +/// ``` +/// +/// See the [top-level docs](crate::form::validate) for more examples. +#[macro_export] +macro_rules! msg { + ($e:expr) => ( + |_| { + Err($crate::form::Errors::from($crate::form::Error::validation($e))) + as $crate::form::Result<()> + } + ); + ($($arg:tt)*) => ( + |_| { + Err($crate::form::Errors::from($crate::form::Error::validation(format!($($arg)*)))) + as $crate::form::Result<()> + } + ); +} + +#[doc(inline)] +pub use msg; + +/// Equality validator: succeeds exactly when `a` == `b`, using [`PartialEq`]. +/// +/// On failure, returns a validation error with the following message: +/// +/// ```text +/// value does not match expected value +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::{FromForm, FromFormField}; +/// +/// #[derive(FromFormField, PartialEq)] +/// enum Kind { +/// Car, +/// Truck +/// } +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = eq("Bob Marley"))] +/// name: &'r str, +/// #[field(validate = eq(Kind::Car))] +/// vehicle: Kind, +/// #[field(validate = eq(&[5, 7, 8]))] +/// numbers: Vec, +/// } +/// ``` +pub fn eq<'v, A, B>(a: &A, b: B) -> Result<'v, ()> where A: PartialEq { if a != &b { - Err(Error::validation("value does not match"))? + Err(Error::validation("value does not match expected value"))? } Ok(()) } -pub trait Len { - fn len(&self) -> usize; - - fn len_u64(&self) -> u64 { - self.len() as u64 - } -} - -impl Len for str { - fn len(&self) -> usize { self.len() } -} - -impl Len for String { - fn len(&self) -> usize { self.len() } -} - -impl Len for Vec { - fn len(&self) -> usize { >::len(self) } -} - -impl Len for TempFile<'_> { - fn len(&self) -> usize { TempFile::len(self) as usize } - - fn len_u64(&self) -> u64 { TempFile::len(self) } -} - -impl Len for std::collections::HashMap { - fn len(&self) -> usize { >::len(self) } -} - -impl Len for &T { - fn len(&self) -> usize { - ::len(self) - } -} - -pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>> - where V: Len, R: RangeBounds +/// Negative equality validator: succeeds exactly when `a` != `b`, using +/// [`PartialEq`]. +/// +/// On failure, returns a validation error with the following message: +/// +/// ```text +/// value is equal to an invalid value +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::{FromForm, FromFormField}; +/// +/// #[derive(FromFormField, PartialEq)] +/// enum Kind { +/// Car, +/// Truck +/// } +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = neq("Bob Marley"))] +/// name: &'r str, +/// #[field(validate = neq(Kind::Car))] +/// vehicle: Kind, +/// #[field(validate = neq(&[5, 7, 8]))] +/// numbers: Vec, +/// } +/// ``` +pub fn neq<'v, A, B>(a: &A, b: B) -> Result<'v, ()> + where A: PartialEq { - if !range.contains(&value.len_u64()) { + if a == &b { + Err(Error::validation("value is equal to an invalid value"))? + } + + Ok(()) +} + +/// Types for values that have a length. +/// +/// At present, these are: +/// +/// | type | length description | +/// |-----------------------------------|--------------------------------------| +/// | `&str`, `String` | length in bytes | +/// | `Vec` | number of elements in the vector | +/// | `HashMap`, `BTreeMap` | number of key/value pairs in the map | +/// | [`TempFile`] | length of the file in bytes | +/// | `Option` where `T: Len` | length of `T` or 0 if `None` | +/// | [`form::Result<'_, T>`] | length of `T` or 0 if `Err` | +/// +/// [`form::Result<'_, T>`]: crate::form::Result +pub trait Len { + /// The length of the value. + fn len(&self) -> L; + + /// Convert `len` into `u64`. + fn len_into_u64(len: L) -> u64; + + /// The zero value for `L`. + fn zero_len() -> L; +} + +macro_rules! impl_len { + (<$($gen:ident),*> $T:ty => $L:ty) => ( + impl <$($gen),*> Len<$L> for $T { + fn len(&self) -> $L { self.len() } + fn len_into_u64(len: $L) -> u64 { len as u64 } + fn zero_len() -> $L { 0 } + } + ) +} + +impl_len!(<> str => usize); +impl_len!(<> String => usize); +impl_len!( Vec => usize); +impl_len!(<> TempFile<'_> => u64); +impl_len!( std::collections::HashMap => usize); +impl_len!( std::collections::BTreeMap => usize); + +impl Len for TempFile<'_> { + fn len(&self) -> ByteUnit { self.len().into() } + fn len_into_u64(len: ByteUnit) -> u64 { len.into() } + fn zero_len() -> ByteUnit { ByteUnit::from(0) } +} + +impl + ?Sized> Len for &T { + fn len(&self) -> L { >::len(self) } + fn len_into_u64(len: L) -> u64 { T::len_into_u64(len) } + fn zero_len() -> L { T::zero_len() } +} + +impl> Len for Option { + fn len(&self) -> L { self.as_ref().map(|v| v.len()).unwrap_or(T::zero_len()) } + fn len_into_u64(len: L) -> u64 { T::len_into_u64(len) } + fn zero_len() -> L { T::zero_len() } +} + +impl> Len for Result<'_, T> { + fn len(&self) -> L { self.as_ref().ok().len() } + fn len_into_u64(len: L) -> u64 { T::len_into_u64(len) } + fn zero_len() -> L { T::zero_len() } +} + +/// Length validator: succeeds when the length of a value is within a `range`. +/// +/// The value must implement [`Len`]. On failure, returns an [`InvalidLength`] +/// error. See [`Len`] for supported types and how their length is computed. +/// +/// [`InvalidLength`]: rocket::form::error::ErrorKind::InvalidLength +/// +/// # Data Limits +/// +/// All form types are constrained by a data limit. As such, the `len()` +/// validator should be used only when a data limit is insufficiently specific. +/// For example, prefer to use data [`Limits`](crate::data::Limits) to validate +/// the length of files as not doing so will result in writing more data to disk +/// than necessary. +/// +/// # Example +/// +/// ```rust +/// use rocket::http::ContentType; +/// use rocket::form::{FromForm, FromFormField}; +/// use rocket::data::{TempFile, ToByteUnit}; +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = len(5..20))] +/// name: &'r str, +/// #[field(validate = len(..=200))] +/// maybe_name: Option<&'r str>, +/// #[field(validate = len(..=2.mebibytes()))] +/// #[field(validate = ext(ContentType::Plain))] +/// file: TempFile<'r>, +/// } +/// ``` +pub fn len<'v, V, L, R>(value: V, range: R) -> Result<'v, ()> + where V: Len, + L: Copy + PartialOrd, + R: RangeBounds +{ + if !range.contains(&value.len()) { let start = match range.start_bound() { - Bound::Included(v) => Some(*v), - Bound::Excluded(v) => Some(v.saturating_add(1)), + Bound::Included(v) => Some(V::len_into_u64(*v)), + Bound::Excluded(v) => Some(V::len_into_u64(*v).saturating_add(1)), Bound::Unbounded => None }; let end = match range.end_bound() { - Bound::Included(v) => Some(*v), - Bound::Excluded(v) => Some(v.saturating_sub(1)), + Bound::Included(v) => Some(V::len_into_u64(*v)), + Bound::Excluded(v) => Some(V::len_into_u64(*v).saturating_sub(1)), Bound::Unbounded => None, }; @@ -72,87 +336,42 @@ pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>> Ok(()) } +/// Types for values that contain items. +/// +/// At present, these are: +/// +/// | type | contains | +/// |-------------------------|----------------------------| +/// | `&str`, `String` | `&str`, `char` | +/// | `Vec` | `T`, `&T` | +/// | `Option` | `I` where `T: Contains` | +/// | [`form::Result<'_, T>`] | `I` where `T: Contains` | +/// +/// [`form::Result<'_, T>`]: crate::form::Result pub trait Contains { + /// Returns `true` if `self` contains `item`. fn contains(&self, item: I) -> bool; } -impl> Contains for &T { - fn contains(&self, item: I) -> bool { - >::contains(self, item) - } +macro_rules! impl_contains { + ([$($gen:tt)*] $T:ty [contains] $I:ty [via] $P:ty) => { + impl_contains!([$($gen)*] $T [contains] $I [via] $P [with] |v| v); + }; + + ([$($gen:tt)*] $T:ty [contains] $I:ty [via] $P:ty [with] $f:expr) => { + impl<$($gen)*> Contains<$I> for $T { + fn contains(&self, item: $I) -> bool { + <$P>::contains(self, $f(item)) + } + } + }; } -impl Contains<&str> for str { - fn contains(&self, string: &str) -> bool { - ::contains(self, string) - } -} - -impl Contains<&&str> for str { - fn contains(&self, string: &&str) -> bool { - ::contains(self, string) - } -} - -impl Contains for str { - fn contains(&self, c: char) -> bool { - ::contains(self, c) - } -} - -impl Contains<&char> for str { - fn contains(&self, c: &char) -> bool { - ::contains(self, *c) - } -} - -impl Contains<&str> for &str { - fn contains(&self, string: &str) -> bool { - ::contains(self, string) - } -} - -impl Contains<&&str> for &str { - fn contains(&self, string: &&str) -> bool { - ::contains(self, string) - } -} - -impl Contains for &str { - fn contains(&self, c: char) -> bool { - ::contains(self, c) - } -} - -impl Contains<&char> for &str { - fn contains(&self, c: &char) -> bool { - ::contains(self, *c) - } -} - -impl Contains<&str> for String { - fn contains(&self, string: &str) -> bool { - ::contains(self, string) - } -} - -impl Contains<&&str> for String { - fn contains(&self, string: &&str) -> bool { - ::contains(self, string) - } -} - -impl Contains for String { - fn contains(&self, c: char) -> bool { - ::contains(self, c) - } -} - -impl Contains<&char> for String { - fn contains(&self, c: &char) -> bool { - ::contains(self, *c) - } -} +impl_contains!([] str [contains] &str [via] str); +impl_contains!([] str [contains] char [via] str); +impl_contains!([] String [contains] &str [via] str); +impl_contains!([] String [contains] char [via] str); +impl_contains!([T: PartialEq] Vec [contains] &T [via] [T]); impl Contains for Vec { fn contains(&self, item: T) -> bool { @@ -160,33 +379,202 @@ impl Contains for Vec { } } -impl Contains<&T> for Vec { - fn contains(&self, item: &T) -> bool { - <[T]>::contains(self, item) +impl> Contains for Option { + fn contains(&self, item: I) -> bool { + self.as_ref().map(|v| v.contains(item)).unwrap_or(false) } } -pub fn contains<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>> - where V: for<'a> Contains<&'a I>, I: std::fmt::Debug +impl> Contains for Result<'_, T> { + fn contains(&self, item: I) -> bool { + self.as_ref().map(|v| v.contains(item)).unwrap_or(false) + } +} + +impl + ?Sized> Contains for &T { + fn contains(&self, item: I) -> bool { + >::contains(self, item) + } +} + +/// Contains validator: succeeds when a value contains `item`. +/// +/// The value must implement [`Contains`](Contains) where `I` is the type of +/// the `item`. See [`Contains`] for supported types and items. +/// +/// On failure, returns a validation error with the following message: +/// +/// ```text +/// value is equal to an invalid value +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::{FromForm, FromFormField}; +/// +/// #[derive(PartialEq, FromFormField)] +/// enum Pet { Cat, Dog } +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// best_pet: Pet, +/// #[field(validate = contains(Pet::Cat))] +/// #[field(validate = contains(&self.best_pet))] +/// pets: Vec, +/// #[field(validate = contains('/'))] +/// license: &'r str, +/// #[field(validate = contains("@rust-lang.org"))] +/// rust_lang_email: &'r str, +/// } +/// ``` +pub fn contains<'v, V, I>(value: V, item: I) -> Result<'v, ()> + where V: Contains { - if !value.contains(&item) { - Err(Error::validation(format!("must contain {:?}", item)))? + if !value.contains(item) { + Err(Error::validation("value does not contain expected item"))? } Ok(()) } -pub fn omits<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>> - where V: for<'a> Contains<&'a I>, I: std::fmt::Debug +/// Verbose contains validator: like `contains` but mentions `item` in the +/// error message. +/// +/// The is identical to [`contains()`] except that `item` must be `Debug + Copy` +/// and the error message is as follows, where `$item` is the [`Debug`] +/// representation of `item`: +/// +/// ```text +/// values must contains $item +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::{FromForm, FromFormField}; +/// +/// #[derive(PartialEq, Debug, Clone, Copy, FromFormField)] +/// enum Pet { Cat, Dog } +/// +/// #[derive(FromForm)] +/// struct Foo { +/// best_pet: Pet, +/// #[field(validate = verbose_contains(Pet::Dog))] +/// #[field(validate = verbose_contains(&self.best_pet))] +/// pets: Vec, +/// } +/// ``` +pub fn verbose_contains<'v, V, I>(value: V, item: I) -> Result<'v, ()> + where V: Contains, I: Debug + Copy { - if value.contains(&item) { - Err(Error::validation(format!("cannot contain {:?}", item)))? + if !value.contains(item) { + Err(Error::validation(format!("value must contain {:?}", item)))? } Ok(()) } -pub fn range<'v, V, R>(value: &V, range: R) -> Result<(), Errors<'v>> +/// Omits validator: succeeds when a value _does not_ contains `item`. +/// error message. +/// +/// The value must implement [`Contains`](Contains) where `I` is the type of +/// the `item`. See [`Contains`] for supported types and items. +/// +/// On failure, returns a validation error with the following message: +/// +/// ```text +/// value contains a disallowed item +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::{FromForm, FromFormField}; +/// +/// #[derive(PartialEq, FromFormField)] +/// enum Pet { Cat, Dog } +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = omits(Pet::Cat))] +/// pets: Vec, +/// #[field(validate = omits('@'))] +/// not_email: &'r str, +/// #[field(validate = omits("@gmail.com"))] +/// non_gmail_email: &'r str, +/// } +/// ``` +pub fn omits<'v, V, I>(value: V, item: I) -> Result<'v, ()> + where V: Contains +{ + if value.contains(item) { + Err(Error::validation("value contains a disallowed item"))? + } + + Ok(()) +} + +/// Verbose omits validator: like `omits` but mentions `item` in the error +/// message. +/// +/// The is identical to [`omits()`] except that `item` must be `Debug + Copy` +/// and the error message is as follows, where `$item` is the [`Debug`] +/// representation of `item`: +/// +/// ```text +/// value cannot contain $item +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::{FromForm, FromFormField}; +/// +/// #[derive(PartialEq, Debug, Clone, Copy, FromFormField)] +/// enum Pet { Cat, Dog } +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = verbose_omits(Pet::Cat))] +/// pets: Vec, +/// #[field(validate = verbose_omits('@'))] +/// not_email: &'r str, +/// #[field(validate = verbose_omits("@gmail.com"))] +/// non_gmail_email: &'r str, +/// } +/// ``` +pub fn verbose_omits<'v, V, I>(value: V, item: I) -> Result<'v, ()> + where V: Contains, I: Copy + Debug +{ + if value.contains(item) { + Err(Error::validation(format!("value cannot contain {:?}", item)))? + } + + Ok(()) +} + +/// Integer range validator: succeeds when an integer value is within a range. +/// +/// The value must be an integer type that implement `TryInto + Copy`. On +/// failure, returns an [`OutOfRange`] error. +/// +/// [`OutOfRange`]: rocket::form::error::ErrorKind::OutOfRange +/// +/// # Example +/// +/// ```rust +/// use rocket::form::FromForm; +/// +/// #[derive(FromForm)] +/// struct Foo { +/// #[field(validate = range(0..))] +/// non_negative: isize, +/// #[field(validate = range(18..=130))] +/// maybe_adult: u8, +/// } +/// ``` +pub fn range<'v, V, R>(value: &V, range: R) -> Result<'v, ()> where V: TryInto + Copy, R: RangeBounds { if let Ok(v) = (*value).try_into() { @@ -211,36 +599,93 @@ pub fn range<'v, V, R>(value: &V, range: R) -> Result<(), Errors<'v>> Err((start, end))? } -pub fn one_of<'v, V, I>(value: V, items: &[I]) -> Result<(), Errors<'v>> - where V: for<'a> Contains<&'a I>, I: Display +/// Contains one of validator: succeeds when a value contains at least one item +/// in an `items` iterator. +/// +/// The value must implement [`Contains`](Contains) where `I` is the type of +/// the `item`. The iterator must be [`Clone`]. See [`Contains`] for supported +/// types and items. The item must be [`Debug`]. +/// +/// On failure, returns a [`InvalidChoice`] error with the debug representation +/// of each item in `items`. +/// +/// [`InvalidChoice`]: rocket::form::error::ErrorKind::InvalidChoice +/// +/// # Example +/// +/// ```rust +/// use rocket::form::FromForm; +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = one_of(&[3, 5, 7]))] +/// single_digit_primes: Vec, +/// #[field(validate = one_of(" \t\n".chars()))] +/// has_space_char: &'r str, +/// #[field(validate = one_of(" \t\n".chars()).and_then(msg!("no spaces")))] +/// no_space: &'r str, +/// } +/// ``` +pub fn one_of<'v, V, I, R>(value: V, items: R) -> Result<'v, ()> + where V: Contains, + I: Debug, + R: IntoIterator, + ::IntoIter: Clone { - for item in items { + let items = items.into_iter(); + for item in items.clone() { if value.contains(item) { return Ok(()); } } - let choices = items.iter() - .map(|item| item.to_string().into()) - .collect::>>(); + let choices: Vec> = items + .map(|item| format!("{:?}", item).into()) + .collect(); Err(choices)? } -pub fn ext<'v>(file: &TempFile<'_>, ext: &str) -> Result<(), Errors<'v>> { +/// File type validator: succeeds when a [`TempFile`] has the Content-Type +/// `content_type`. +/// +/// On failure, returns a validation error with one of the following messages: +/// +/// ```text +/// // the file has an incorrect extension +/// file type was .$file_ext but must be $type +/// +/// // the file does not have an extension +/// file type must be $type +/// ``` +/// +/// # Example +/// +/// ```rust +/// use rocket::form::FromForm; +/// use rocket::data::{ToByteUnit, TempFile}; +/// use rocket::http::ContentType; +/// +/// #[derive(FromForm)] +/// struct Foo<'r> { +/// #[field(validate = ext(ContentType::PDF))] +/// #[field(validate = len(..1.mebibytes()))] +/// document: TempFile<'r>, +/// } +/// ``` +pub fn ext<'v>(file: &TempFile<'_>, r#type: ContentType) -> Result<'v, ()> { if let Some(file_ct) = file.content_type() { - if let Some(ext_ct) = ContentType::from_extension(ext) { - if file_ct == &ext_ct { - return Ok(()); - } - - let m = file_ct.extension() - .map(|fext| format!("file type was .{} but must be .{}", fext, ext)) - .unwrap_or_else(|| format!("file type must be .{}", ext)); - - Err(Error::validation(m))? + if file_ct == &r#type { + return Ok(()); } } - Err(Error::validation(format!("invalid extension: expected {}", ext)))? + let msg = match (file.content_type().and_then(|c| c.extension()), r#type.extension()) { + (Some(a), Some(b)) => format!("invalid file type: .{}, must be .{}", a, b), + (Some(a), None) => format!("invalid file type: .{}, must be {}", a, r#type), + (None, Some(b)) => format!("file type must be .{}", b), + (None, None) => format!("file type must be {}", r#type), + }; + + Err(Error::validation(msg))? } diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 79c8b680..26d592b4 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -832,30 +832,35 @@ means that validation was successful while an `Err` of [`Errors<'_>`] indicates the error(s) that occured. For instance, if you wanted to implement an ad-hoc Luhn validator for credit-card-like numbers, you might write: - ```rust # #[macro_use] extern crate rocket; extern crate time; +use rocket::form::{self, Error}; + #[derive(FromForm)] -struct CreditCard<'v> { - #[field(validate = luhn())] - number: &'v str, - # #[field(validate = luhn())] - # other: String, +struct CreditCard { + #[field(validate = luhn(self.cvv, &self.expiration))] + number: u64, #[field(validate = range(..9999))] cvv: u16, expiration: time::Date, } -fn luhn<'v, S: AsRef>(field: S) -> rocket::form::Result<'v, ()> { - let num = field.as_ref().parse::()?; +fn luhn<'v>(number: &u64, cvv: u16, exp: &time::Date) -> form::Result<'v, ()> { + # let valid = false; + if !valid { + Err(Error::validation("invalid credit card number"))?; + } - /* implementation of Luhn validator... */ - # Ok(()) + Ok(()) } ``` +If a field's validation doesn't depend on other fields (validation is _local_), +it is validated prior to those fields that do. For `CreditCard`, `cvv` and +`expiration` will be validated prior to `number`. + ### Defaults The [`FromForm`] trait allows types to specify a default value if one isn't