Use the `RawStr` type for all form raw strings.

This is a breaking change.

This commit introduces `RawStr` to forms. In particular, after this
commit, the `&str` type no longer implements `FromFormValue`, and so it
cannot be used as a field in forms. Instad, the `&RawStr` can be used.

The `FormItems` iterator now returns an `(&RawStr, &RawStr)` pair.
This commit is contained in:
Sergio Benitez 2017-03-30 23:06:53 -07:00
parent f57d984e2e
commit 0c44e44641
15 changed files with 145 additions and 110 deletions

View File

@ -68,7 +68,11 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
is_unsafe: false,
supports_unions: false,
span: span,
attributes: Vec::new(),
// We add this attribute because some `FromFormValue` implementations
// can't fail. This is indicated via the `!` type. Rust checks if a
// match is made with something of that type, and since we always emit
// an `Err` match, we'll get this lint warning.
attributes: vec![quote_attr!(ecx, #[allow(unreachable_code)])],
path: ty::Path {
path: vec!["rocket", "request", "FromForm"],
lifetime: lifetime_var,
@ -178,7 +182,8 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
let id_str = ident_string.as_str();
arms.push(quote_tokens!(cx,
$id_str => {
$ident = match ::rocket::request::FromFormValue::from_form_value(v) {
let r = ::rocket::http::RawStr::from_str(v);
$ident = match ::rocket::request::FromFormValue::from_form_value(r) {
Ok(v) => Some(v),
Err(e) => {
println!(" => Error parsing form val '{}': {:?}",
@ -194,9 +199,9 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// and use the $arms generated above.
stmts.push(quote_stmt!(cx,
for (k, v) in $arg {
match k {
match k.as_str() {
$arms
field if field == "_method" => {
"_method" => {
/* This is a Rocket-specific field. If the user hasn't asked
* for it, just let it go by without error. This should stay
* in sync with Rocket::preprocess. */
@ -214,19 +219,13 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// that each parameter actually is Some() or has a default value.
let mut failure_conditions = vec![];
// Start with `false` in case there are no fields.
failure_conditions.push(quote_tokens!(cx, false));
for &(ref ident, ref ty) in (&fields_and_types).iter() {
// Pushing an "||" (or) between every condition.
failure_conditions.push(quote_tokens!(cx, ||));
failure_conditions.push(quote_tokens!(cx,
if $ident.is_none() &&
<$ty as ::rocket::request::FromFormValue>::default().is_none() {
println!(" => '{}' did not parse.", stringify!($ident));
true
} else { false }
$return_err_stmt;
}
));
}
@ -245,9 +244,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// the structure.
let self_ident = substr.type_ident;
let final_block = quote_block!(cx, {
if $failure_conditions {
$return_err_stmt;
}
$failure_conditions
Ok($self_ident { $result_fields })
});

View File

@ -81,7 +81,7 @@ impl RouteGenerateExt for RouteParams {
Err(_) => return ::rocket::Outcome::Forward(_data)
};
if !items.exhausted() {
if !items.exhaust() {
println!(" => The query string {:?} is malformed.", $form_string);
return ::rocket::Outcome::Failure(::rocket::http::Status::BadRequest);
}

View File

@ -3,12 +3,12 @@
extern crate rocket;
use rocket::http::Cookies;
use rocket::http::{Cookies, RawStr};
use rocket::request::Form;
#[derive(FromForm)]
struct User<'a> {
name: &'a str,
name: &'a RawStr,
nickname: String,
}

View File

@ -4,6 +4,7 @@
extern crate rocket;
use rocket::request::{FromForm, FromFormValue, FormItems};
use rocket::http::RawStr;
#[derive(Debug, PartialEq, FromForm)]
struct TodoTask {
@ -20,8 +21,8 @@ enum FormOption {
impl<'v> FromFormValue<'v> for FormOption {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let variant = match v {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
let variant = match v.as_str() {
"a" => FormOption::A,
"b" => FormOption::B,
"c" => FormOption::C,
@ -37,19 +38,19 @@ struct FormInput<'r> {
checkbox: bool,
number: usize,
radio: FormOption,
password: &'r str,
password: &'r RawStr,
textarea: String,
select: FormOption,
}
#[derive(Debug, PartialEq, FromForm)]
struct DefaultInput<'r> {
arg: Option<&'r str>,
arg: Option<&'r RawStr>,
}
#[derive(Debug, PartialEq, FromForm)]
struct ManualMethod<'r> {
_method: Option<&'r str>,
_method: Option<&'r RawStr>,
done: bool
}
@ -61,13 +62,13 @@ struct UnpresentCheckbox {
#[derive(Debug, PartialEq, FromForm)]
struct UnpresentCheckboxTwo<'r> {
checkbox: bool,
something: &'r str
something: &'r RawStr
}
fn parse<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> {
let mut items = FormItems::from(string);
let result = T::from_form_items(items.by_ref());
if !items.exhausted() {
if !items.exhaust() {
panic!("Invalid form input.");
}
@ -103,7 +104,7 @@ fn main() {
checkbox: false,
number: 10,
radio: FormOption::C,
password: "testing",
password: "testing".into(),
textarea: "".to_string(),
select: FormOption::A,
}));
@ -117,7 +118,7 @@ fn main() {
// Ensure _method can be captured if desired.
let manual: Option<ManualMethod> = parse("_method=put&done=true");
assert_eq!(manual, Some(ManualMethod {
_method: Some("put"),
_method: Some("put".into()),
done: true
}));
@ -138,6 +139,6 @@ fn main() {
let manual: Option<UnpresentCheckboxTwo> = parse("something=hello");
assert_eq!(manual, Some(UnpresentCheckboxTwo {
checkbox: false,
something: "hello"
something: "hello".into()
}));
}

View File

@ -5,6 +5,7 @@ use std::str::FromStr;
use std::ops::Deref;
use rocket::request::{FromParam, FromFormValue};
use rocket::http::RawStr;
pub use self::uuid_ext::ParseError as UuidParseError;
@ -89,11 +90,12 @@ impl<'a> FromParam<'a> for UUID {
}
impl<'v> FromFormValue<'v> for UUID {
type Error = &'v str;
type Error = &'v RawStr;
/// A value is successfully parsed if `form_value` is a properly formatted
/// UUID. Otherwise, the raw form value is returned.
fn from_form_value(form_value: &'v str) -> Result<UUID, &'v str> {
#[inline(always)]
fn from_form_value(form_value: &'v RawStr) -> Result<UUID, &'v RawStr> {
form_value.parse().map_err(|_| form_value)
}
}

View File

@ -4,11 +4,11 @@
extern crate rocket;
mod files;
#[cfg(test)]
mod tests;
#[cfg(test)] mod tests;
use rocket::response::Redirect;
use rocket::request::{Form, FromFormValue};
use rocket::http::RawStr;
#[derive(Debug)]
struct StrongPassword<'r>(&'r str);
@ -18,7 +18,7 @@ struct AdultAge(isize);
#[derive(FromForm)]
struct UserLogin<'r> {
username: &'r str,
username: &'r RawStr,
password: Result<StrongPassword<'r>, &'static str>,
age: Result<AdultAge, &'static str>,
}
@ -26,11 +26,11 @@ struct UserLogin<'r> {
impl<'v> FromFormValue<'v> for StrongPassword<'v> {
type Error = &'static str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
if v.len() < 8 {
Err("Too short!")
Err("too short!")
} else {
Ok(StrongPassword(v))
Ok(StrongPassword(v.as_str()))
}
}
}
@ -38,15 +38,15 @@ impl<'v> FromFormValue<'v> for StrongPassword<'v> {
impl<'v> FromFormValue<'v> for AdultAge {
type Error = &'static str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
let age = match isize::from_form_value(v) {
Ok(v) => v,
Err(_) => return Err("Age value is not a number."),
Err(_) => return Err("value is not a number."),
};
match age > 20 {
true => Ok(AdultAge(age)),
false => Err("Must be at least 21."),
false => Err("must be at least 21."),
}
}
}

View File

@ -37,14 +37,14 @@ fn test_invalid_user() {
#[test]
fn test_invalid_password() {
test_login("Sergio", "password1", "30", Status::Ok, "Wrong password!");
test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: Too short!");
test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: too short!");
}
#[test]
fn test_invalid_age() {
test_login("Sergio", "password", "20", Status::Ok, "Must be at least 21.");
test_login("Sergio", "password", "-100", Status::Ok, "Must be at least 21.");
test_login("Sergio", "password", "hi", Status::Ok, "Age value is not a number");
test_login("Sergio", "password", "20", Status::Ok, "must be at least 21.");
test_login("Sergio", "password", "-100", Status::Ok, "must be at least 21.");
test_login("Sergio", "password", "hi", Status::Ok, "value is not a number");
}
fn check_bad_form(form_str: &str, status: Status) {

View File

@ -3,9 +3,10 @@
extern crate rocket;
use std::io;
use rocket::request::{Form, FromFormValue};
use rocket::response::NamedFile;
use std::io;
use rocket::http::RawStr;
// TODO: Make deriving `FromForm` for this enum possible.
#[derive(Debug)]
@ -14,10 +15,10 @@ enum FormOption {
}
impl<'v> FromFormValue<'v> for FormOption {
type Error = &'v str;
type Error = &'v RawStr;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let variant = match v {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
let variant = match v.as_str() {
"a" => FormOption::A,
"b" => FormOption::B,
"c" => FormOption::C,

View File

@ -8,12 +8,13 @@ mod files;
use rocket::request::Form;
use rocket::response::Redirect;
use rocket::http::RawStr;
#[derive(FromForm)]
struct UserLogin<'r> {
username: &'r str,
username: &'r RawStr,
password: String,
age: Result<usize, &'r str>,
age: Result<usize, &'r RawStr>,
}
#[post("/login", data = "<user_form>")]
@ -36,9 +37,8 @@ fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
}
}
#[get("/user/<username>")]
fn user_page(username: &str) -> String {
fn user_page(username: String) -> String {
format!("This is {}'s page.", username)
}

View File

@ -6,8 +6,8 @@ extern crate rocket;
#[cfg(test)] mod tests;
#[derive(FromForm)]
struct Person<'r> {
name: &'r str,
struct Person {
name: String,
age: Option<u8>
}

View File

@ -6,6 +6,7 @@
#![feature(type_ascription)]
#![feature(lookup_host)]
#![feature(plugin)]
#![feature(never_type)]
#![plugin(pear_codegen)]

View File

@ -1,17 +1,20 @@
use memchr::memchr2;
use http::RawStr;
/// Iterator over the key/value pairs of a given HTTP form string.
///
/// **Note:** The returned key/value pairs are _not_ URL decoded. To URL decode
/// the raw strings, use `String::from_form_value`:
/// the raw strings, use the
/// [`url_decode`](/rocket/http/struct.RawStr.html#method.url_decode) method:
///
/// ```rust
/// use rocket::request::{FormItems, FromFormValue};
///
/// let form_string = "greeting=Hello%2C+Mark%21&username=jake%2Fother";
/// for (key, value) in FormItems::from(form_string) {
/// let decoded_value = String::from_form_value(value);
/// match key {
/// let decoded_value = value.url_decode();
/// match key.as_str() {
/// "greeting" => assert_eq!(decoded_value, Ok("Hello, Mark!".into())),
/// "username" => assert_eq!(decoded_value, Ok("jake/other".into())),
/// _ => unreachable!()
@ -26,7 +29,7 @@ use memchr::memchr2;
/// for completion via the [completed](#method.completed) method, which returns
/// `true` if the iterator parsed the entire string that was passed to it. The
/// iterator can also attempt to parse any remaining contents via
/// [exhausted](#method.exhausted); this method returns `true` if exhaustion
/// [exhaust](#method.exhaust); this method returns `true` if exhaustion
/// succeeded.
///
/// This iterator guarantees that all valid form strings are parsed to
@ -57,13 +60,20 @@ use memchr::memchr2;
///
/// let form_string = "greeting=hello&username=jake";
/// let mut items = FormItems::from(form_string);
/// assert_eq!(items.next(), Some(("greeting", "hello")));
/// assert_eq!(items.next(), Some(("username", "jake")));
///
/// let next = items.next().unwrap();
/// assert_eq!(next.0, "greeting");
/// assert_eq!(next.1, "hello");
///
/// let next = items.next().unwrap();
/// assert_eq!(next.0, "username");
/// assert_eq!(next.1, "jake");
///
/// assert_eq!(items.next(), None);
/// assert!(items.completed());
/// ```
pub struct FormItems<'f> {
string: &'f str,
string: &'f RawStr,
next_index: usize
}
@ -117,7 +127,7 @@ impl<'f> FormItems<'f> {
///
/// assert!(items.next().is_some());
/// assert_eq!(items.completed(), false);
/// assert_eq!(items.exhausted(), true);
/// assert_eq!(items.exhaust(), true);
/// assert_eq!(items.completed(), true);
/// ```
///
@ -130,10 +140,11 @@ impl<'f> FormItems<'f> {
///
/// assert!(items.next().is_some());
/// assert_eq!(items.completed(), false);
/// assert_eq!(items.exhausted(), false);
/// assert_eq!(items.exhaust(), false);
/// assert_eq!(items.completed(), false);
/// ```
pub fn exhausted(&mut self) -> bool {
#[inline]
pub fn exhaust(&mut self) -> bool {
while let Some(_) = self.next() { }
self.completed()
}
@ -167,15 +178,16 @@ impl<'f> FormItems<'f> {
/// assert_eq!(items.inner_str(), form_string);
/// ```
#[inline]
pub fn inner_str(&self) -> &'f str {
pub fn inner_str(&self) -> &'f RawStr {
self.string
}
}
impl<'f> From<&'f str> for FormItems<'f> {
impl<'f> From<&'f RawStr> for FormItems<'f> {
/// Returns an iterator over the key/value pairs in the
/// `x-www-form-urlencoded` form `string`.
fn from(string: &'f str) -> FormItems<'f> {
#[inline(always)]
fn from(string: &'f RawStr) -> FormItems<'f> {
FormItems {
string: string,
next_index: 0
@ -183,8 +195,20 @@ impl<'f> From<&'f str> for FormItems<'f> {
}
}
impl<'f> From<&'f str> for FormItems<'f> {
/// Returns an iterator over the key/value pairs in the
/// `x-www-form-urlencoded` form `string`.
#[inline(always)]
fn from(string: &'f str) -> FormItems<'f> {
FormItems {
string: string.into(),
next_index: 0
}
}
}
impl<'f> Iterator for FormItems<'f> {
type Item = (&'f str, &'f str);
type Item = (&'f RawStr, &'f RawStr);
fn next(&mut self) -> Option<Self::Item> {
let s = &self.string[self.next_index..];
@ -204,7 +228,7 @@ impl<'f> Iterator for FormItems<'f> {
};
self.next_index += key.len() + 1 + consumed;
Some((key, value))
Some((key.into(), value.into()))
}
}
@ -224,18 +248,19 @@ mod test {
let (expected_key, actual_key) = (expected[i].0, results[i].0);
let (expected_val, actual_val) = (expected[i].1, results[i].1);
assert!(expected_key == actual_key,
assert!(actual_key == expected_key,
"key [{}] mismatch: expected {}, got {}",
i, expected_key, actual_key);
assert!(expected_val == actual_val,
assert!(actual_val == expected_val,
"val [{}] mismatch: expected {}, got {}",
i, expected_val, actual_val);
}
} else {
assert!(!items.exhausted());
assert!(!items.exhaust());
}
});
(@bad $string:expr) => (check_form!(@opt $string, None : Option<&[(&str, &str)]>));
($string:expr, $expected:expr) => (check_form!(@opt $string, Some($expected)));
}

View File

@ -1,8 +1,7 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr};
use std::str::FromStr;
use error::Error;
use http::uri::URI;
use http::RawStr;
/// Trait to create instance of some type from a form value; expected from field
/// types in structs deriving `FromForm`.
@ -38,15 +37,16 @@ use http::uri::URI;
/// following structure:
///
/// ```rust
/// # use rocket::http::RawStr;
/// # #[allow(dead_code)]
/// struct Person<'r> {
/// name: String,
/// age: Result<u16, &'r str>
/// age: Result<u16, &'r RawStr>
/// }
/// ```
///
/// The `Err` value in this case is `&str` since `u16::from_form_value` returns
/// a `Result<u16, &str>`.
/// The `Err` value in this case is `&RawStr` since `u16::from_form_value`
/// returns a `Result<u16, &RawStr>`.
///
/// # Provided Implementations
///
@ -67,7 +67,7 @@ use http::uri::URI;
/// `"false"`, `"off"`, or not present. In any other case, the raw form
/// value is returned in the `Err` value.
///
/// * **str**
/// * **&RawStr**
///
/// _This implementation always returns successfully._
///
@ -106,14 +106,15 @@ use http::uri::URI;
///
/// ```rust
/// use rocket::request::FromFormValue;
/// use rocket::http::RawStr;
///
/// struct AdultAge(usize);
///
/// impl<'v> FromFormValue<'v> for AdultAge {
/// type Error = &'v str;
/// type Error = &'v RawStr;
///
/// fn from_form_value(form_value: &'v str) -> Result<AdultAge, &'v str> {
/// match usize::from_form_value(form_value) {
/// fn from_form_value(form_value: &'v RawStr) -> Result<AdultAge, &'v RawStr> {
/// match form_value.parse::<usize>() {
/// Ok(age) if age >= 21 => Ok(AdultAge(age)),
/// _ => Err(form_value),
/// }
@ -141,50 +142,50 @@ pub trait FromFormValue<'v>: Sized {
/// Parses an instance of `Self` from an HTTP form field value or returns an
/// `Error` if one cannot be parsed.
fn from_form_value(form_value: &'v str) -> Result<Self, Self::Error>;
fn from_form_value(form_value: &'v RawStr) -> Result<Self, Self::Error>;
/// Returns a default value to be used when the form field does not exist.
/// If this returns `None`, then the field is required. Otherwise, this
/// should return `Some(default_value)`. The default implementation simply
/// returns `None`.
#[inline(always)]
fn default() -> Option<Self> {
None
}
}
impl<'v> FromFormValue<'v> for &'v str {
type Error = Error;
impl<'v> FromFormValue<'v> for &'v RawStr {
type Error = !;
// This just gives the raw string.
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
#[inline(always)]
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
Ok(v)
}
}
impl<'v> FromFormValue<'v> for String {
type Error = &'v str;
type Error = &'v RawStr;
// This actually parses the value according to the standard.
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let replaced = v.replace("+", " ");
match URI::percent_decode(replaced.as_bytes()) {
Err(_) => Err(v),
Ok(string) => Ok(string.into_owned())
}
#[inline(always)]
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
v.url_decode().map_err(|_| v)
}
}
impl<'v> FromFormValue<'v> for bool {
type Error = &'v str;
type Error = &'v RawStr;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match v {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
match v.as_str() {
"on" | "true" => Ok(true),
"off" | "false" => Ok(false),
_ => Err(v),
}
}
#[inline(always)]
fn default() -> Option<bool> {
Some(false)
}
@ -193,9 +194,11 @@ impl<'v> FromFormValue<'v> for bool {
macro_rules! impl_with_fromstr {
($($T:ident),+) => ($(
impl<'v> FromFormValue<'v> for $T {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
$T::from_str(v).map_err(|_| v)
type Error = &'v RawStr;
#[inline(always)]
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
$T::from_str(v.as_str()).map_err(|_| v)
}
}
)+)
@ -205,29 +208,31 @@ impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64,
IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr);
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option<T> {
type Error = Error;
type Error = !;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
#[inline(always)]
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
match T::from_form_value(v) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
}
#[inline(always)]
fn default() -> Option<Option<T>> {
Some(None)
}
}
// TODO: Add more useful implementations (range, regex, etc.).
// // TODO: Add more useful implementations (range, regex, etc.).
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result<T, T::Error> {
type Error = Error;
type Error = !;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
#[inline(always)]
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
match T::from_form_value(v) {
ok@Ok(_) => Ok(ok),
e@Err(_) => Ok(e),
}
}
}

View File

@ -72,9 +72,10 @@ use outcome::Outcome::*;
/// # #![allow(deprecated, dead_code, unused_attributes)]
/// # #![plugin(rocket_codegen)]
/// # extern crate rocket;
/// # use rocket::http::RawStr;
/// #[derive(FromForm)]
/// struct UserInput<'f> {
/// value: &'f str
/// value: &'f RawStr
/// }
/// # fn main() { }
/// ```
@ -88,9 +89,10 @@ use outcome::Outcome::*;
/// # #![plugin(rocket_codegen)]
/// # extern crate rocket;
/// # use rocket::request::Form;
/// # use rocket::http::RawStr;
/// # #[derive(FromForm)]
/// # struct UserInput<'f> {
/// # value: &'f str
/// # value: &'f RawStr
/// # }
/// #[post("/submit", data = "<user_input>")]
/// fn submit_task<'r>(user_input: Form<'r, UserInput<'r>>) -> String {
@ -221,7 +223,7 @@ impl<'f, T: FromForm<'f> + 'f> Form<'f, T> {
let mut items = FormItems::from(long_lived_string);
let result = T::from_form_items(items.by_ref());
if !items.exhausted() {
if !items.exhaust() {
return FormResult::Invalid(form_string);
}

View File

@ -168,10 +168,11 @@ impl Rocket {
from_utf8_unchecked(&data.peek()[..min(data_len, max_len)])
};
let mut form_items = FormItems::from(form);
if let Some(("_method", value)) = form_items.next() {
if let Ok(method) = value.parse() {
req.set_method(method);
if let Some((key, value)) = FormItems::from(form).next() {
if key == "_method" {
if let Ok(method) = value.parse() {
req.set_method(method);
}
}
}
}