diff --git a/lib/src/ext.rs b/lib/src/ext.rs index 394d0997..4c974786 100644 --- a/lib/src/ext.rs +++ b/lib/src/ext.rs @@ -52,14 +52,30 @@ impl IntoCollection for Vec { } } -impl<'a, T: Clone> IntoCollection for &'a [T] { - #[inline(always)] - fn into_collection>(self) -> SmallVec { - self.iter().cloned().collect() - } +macro_rules! impl_for_slice { + ($($size:tt)*) => ( + impl<'a, T: Clone> IntoCollection for &'a [T $($size)*] { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + self.iter().cloned().collect() + } - #[inline] - fn mapped U, A: Array>(self, mut f: F) -> SmallVec { - self.iter().cloned().map(|item| f(item)).collect() - } + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.iter().cloned().map(|item| f(item)).collect() + } + } + ) } + +impl_for_slice!(); +impl_for_slice!(; 1); +impl_for_slice!(; 2); +impl_for_slice!(; 3); +impl_for_slice!(; 4); +impl_for_slice!(; 5); +impl_for_slice!(; 6); +impl_for_slice!(; 7); +impl_for_slice!(; 8); +impl_for_slice!(; 9); +impl_for_slice!(; 10); diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs index dce9be96..9b48271c 100644 --- a/lib/src/http/accept.rs +++ b/lib/src/http/accept.rs @@ -8,34 +8,68 @@ use ext::IntoCollection; use http::{Header, MediaType}; use http::parse::parse_accept; +/// A `MediaType` with an associated quality value. #[derive(Debug, Clone, PartialEq)] -pub struct WeightedMediaType(pub MediaType, pub Option); +pub struct QMediaType(pub MediaType, pub Option); -impl WeightedMediaType { +impl QMediaType { + /// Retrieve the weight of the media type, if there is any. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{MediaType, QMediaType}; + /// + /// let q_type = QMediaType(MediaType::HTML, Some(0.3)); + /// assert_eq!(q_type.weight(), Some(0.3)); + /// ``` #[inline(always)] pub fn weight(&self) -> Option { self.1 } + /// Retrieve the weight of the media type or a given default value. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{MediaType, QMediaType}; + /// + /// let q_type = QMediaType(MediaType::HTML, Some(0.3)); + /// assert_eq!(q_type.weight_or(0.9), 0.3); + /// + /// let q_type = QMediaType(MediaType::HTML, None); + /// assert_eq!(q_type.weight_or(0.9), 0.9); + /// ``` #[inline(always)] pub fn weight_or(&self, default: f32) -> f32 { self.1.unwrap_or(default) } + /// Borrow the internal `MediaType`. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{MediaType, QMediaType}; + /// + /// let q_type = QMediaType(MediaType::HTML, Some(0.3)); + /// assert_eq!(q_type.media_type(), &MediaType::HTML); + /// ``` #[inline(always)] pub fn media_type(&self) -> &MediaType { &self.0 } } -impl From for WeightedMediaType { +impl From for QMediaType { #[inline(always)] - fn from(media_type: MediaType) -> WeightedMediaType { - WeightedMediaType(media_type, None) + fn from(media_type: MediaType) -> QMediaType { + QMediaType(media_type, None) } } -impl Deref for WeightedMediaType { +impl Deref for QMediaType { type Target = MediaType; #[inline(always)] @@ -47,11 +81,53 @@ impl Deref for WeightedMediaType { // FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`. #[derive(Debug, PartialEq, Clone)] pub enum AcceptParams { - Static(&'static [WeightedMediaType]), - Dynamic(SmallVec<[WeightedMediaType; 1]>) + Static(&'static [QMediaType]), + Dynamic(SmallVec<[QMediaType; 1]>) } /// The HTTP Accept header. +/// +/// An `Accept` header is composed of zero or more media types, each of which +/// may have an optional quality value (a [`QMediaType`]). The header is sent by an HTTP client to +/// describe the formats it accepts as well as the order in which it prefers +/// different formats. +/// +/// # Usage +/// +/// The Accept header of an incoming request can be retrieved via the +/// [`Request::accept`] method. The [`preferred`] method can be used to retrieve +/// the client's preferred media type. +/// +/// [`Request::accept`]: /rocket/struct.Request.html#method.accept +/// [`preferred`]: /rocket/http/struct.Accept.html#method.preferred +/// [`QMediaType`]: /rocket/http/struct.QMediaType.html +/// +/// An `Accept` type with a single, common media type can be easily constructed +/// via provided associated constants. +/// +/// ## Example +/// +/// Construct an `Accept` header with a single `application/json` media type: +/// +/// ```rust +/// use rocket::http::Accept; +/// +/// # #[allow(unused_variables)] +/// let accept_json = Accept::JSON; +/// ``` +/// +/// # Header +/// +/// `Accept` implements `Into
`. As such, it can be used in any context +/// where an `Into
` is expected: +/// +/// ```rust +/// use rocket::http::Accept; +/// use rocket::response::Response; +/// +/// # #[allow(unused_variables)] +/// let response = Response::build().header(Accept::JSON).finalize(); +/// ``` #[derive(Debug, Clone, PartialEq)] pub struct Accept(AcceptParams); @@ -65,7 +141,7 @@ macro_rules! accept_constructor { #[doc=""] #[allow(non_upper_case_globals)] pub const $name: Accept = Accept( - AcceptParams::Static(&[WeightedMediaType(MediaType::$name, None)]) + AcceptParams::Static(&[QMediaType(MediaType::$name, None)]) ); )+ }; @@ -79,20 +155,62 @@ impl> From for Accept { } impl Accept { + /// Constructs a new `Accept` header from one or more media types. + /// + /// The `items` parameter may be of type `QMediaType`, `&[QMediaType]`, or + /// `Vec`. To prevent additional allocations, prefer to provide + /// inputs of type `QMediaType` and `Vec`. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{QMediaType, MediaType, Accept}; + /// + /// // Construct an `Accept` via a `Vec`. + /// let json_then_html = vec![MediaType::JSON.into(), MediaType::HTML.into()]; + /// let accept = Accept::new(json_then_html); + /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON); + /// + /// // Construct an `Accept` via an `&[QMediaType]`. + /// let accept = Accept::new(&[MediaType::JSON.into(), MediaType::HTML.into()]); + /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON); + /// + /// // Construct an `Accept` via a `QMediaType`. + /// let accept = Accept::new(QMediaType(MediaType::JSON, None)); + /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON); + /// ``` #[inline(always)] - pub fn new>(items: T) -> Accept { + pub fn new>(items: T) -> Accept { Accept(AcceptParams::Dynamic(items.into_collection())) } - // FIXME: IMPLEMENT THIS. + // TODO: Implement this. // #[inline(always)] - // pub fn add>(&mut self, media_type: M) { + // pub fn add>(&mut self, media_type: M) { // self.0.push(media_type.into()); // } - /// TODO: Cache this? - pub fn preferred(&self) -> &WeightedMediaType { - static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None); + /// Retrieve the client's preferred media type. This method follows [RFC + /// 7231 5.3.2]. If the list of media types is empty, this method returns a + /// media type of any with no quality value: (`*/*`). + /// + /// [RFC 7231 5.3.2]: https://tools.ietf.org/html/rfc7231#section-5.3.2 + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{QMediaType, MediaType, Accept}; + /// + /// let media_types = vec![ + /// QMediaType(MediaType::JSON, Some(0.3)), + /// QMediaType(MediaType::HTML, Some(0.9)) + /// ]; + /// + /// let accept = Accept::new(media_types); + /// assert_eq!(accept.preferred().media_type(), &MediaType::HTML); + /// ``` + pub fn preferred(&self) -> &QMediaType { + static ANY: QMediaType = QMediaType(MediaType::Any, None); // See https://tools.ietf.org/html/rfc7231#section-5.3.2. let mut all = self.iter(); @@ -122,13 +240,44 @@ impl Accept { preferred } + /// Retrieve the first media type in `self`, if any. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{QMediaType, MediaType, Accept}; + /// + /// let accept = Accept::new(QMediaType(MediaType::XML, None)); + /// assert_eq!(accept.first(), Some(&MediaType::XML.into())); + /// ``` #[inline(always)] - pub fn first(&self) -> Option<&WeightedMediaType> { + pub fn first(&self) -> Option<&QMediaType> { self.iter().next() } + /// Returns an iterator over all of the (quality) media types in `self`. + /// Media types are returned in the order in which they appear in the + /// header. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{QMediaType, MediaType, Accept}; + /// + /// let qmedia_types = vec![ + /// QMediaType(MediaType::JSON, Some(0.3)), + /// QMediaType(MediaType::HTML, Some(0.9)) + /// ]; + /// + /// let accept = Accept::new(qmedia_types.clone()); + /// + /// let mut iter = accept.iter(); + /// assert_eq!(iter.next(), Some(&qmedia_types[0])); + /// assert_eq!(iter.next(), Some(&qmedia_types[1])); + /// assert_eq!(iter.next(), None); + /// ``` #[inline(always)] - pub fn iter<'a>(&'a self) -> impl Iterator + 'a { + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { let slice = match self.0 { AcceptParams::Static(slice) => slice, AcceptParams::Dynamic(ref vec) => &vec[..], @@ -137,6 +286,26 @@ impl Accept { slice.iter() } + /// Returns an iterator over all of the (bare) media types in `self`. Media + /// types are returned in the order in which they appear in the header. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{QMediaType, MediaType, Accept}; + /// + /// let qmedia_types = vec![ + /// QMediaType(MediaType::JSON, Some(0.3)), + /// QMediaType(MediaType::HTML, Some(0.9)) + /// ]; + /// + /// let accept = Accept::new(qmedia_types.clone()); + /// + /// let mut iter = accept.media_types(); + /// assert_eq!(iter.next(), Some(qmedia_types[0].media_type())); + /// assert_eq!(iter.next(), Some(qmedia_types[1].media_type())); + /// assert_eq!(iter.next(), None); + /// ``` #[inline(always)] pub fn media_types<'a>(&'a self) -> impl Iterator + 'a { self.iter().map(|weighted_mt| weighted_mt.media_type()) @@ -148,8 +317,11 @@ impl Accept { impl fmt::Display for Accept { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for (i, media_type) in self.iter().enumerate() { - if i >= 1 { write!(f, ", ")?; } - write!(f, "{}", media_type.0)?; + if i >= 1 { + write!(f, ", {}", media_type.0)?; + } else { + write!(f, "{}", media_type.0)?; + } } Ok(()) diff --git a/lib/src/http/header.rs b/lib/src/http/header.rs index 49b30a68..571d6386 100644 --- a/lib/src/http/header.rs +++ b/lib/src/http/header.rs @@ -108,6 +108,13 @@ impl<'h> fmt::Display for Header<'h> { } /// A collection of headers, mapping a header name to its many ordered values. +/// +/// # Case-Insensitivity +/// +/// All header names, including those passed in to `HeaderMap` methods and those +/// stored in an existing `HeaderMap`, are treated case-insensitively. This +/// means that, for instance, a look for a header by the name of "aBC" will +/// returns values for headers of names "AbC", "ABC", "abc", and so on. #[derive(Clone, Debug, PartialEq, Default)] pub struct HeaderMap<'h> { headers: OrderMap, Vec>> @@ -115,6 +122,14 @@ pub struct HeaderMap<'h> { impl<'h> HeaderMap<'h> { /// Returns an empty collection. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::HeaderMap; + /// + /// let map = HeaderMap::new(); + /// ``` #[inline(always)] pub fn new() -> HeaderMap<'h> { HeaderMap { headers: OrderMap::new() } @@ -277,6 +292,21 @@ impl<'h> HeaderMap<'h> { /// assert_eq!(map.get_one("Content-Type"), Some("image/gif")); /// assert_eq!(map.len(), 1); /// ``` + /// + /// An example of case-insensitivity. + /// + /// ```rust + /// use rocket::http::{HeaderMap, Header, ContentType}; + /// + /// let mut map = HeaderMap::new(); + /// + /// map.replace(ContentType::JSON); + /// assert_eq!(map.get_one("Content-Type"), Some("application/json")); + /// + /// map.replace(Header::new("CONTENT-type", "image/gif")); + /// assert_eq!(map.get_one("Content-Type"), Some("image/gif")); + /// assert_eq!(map.len(), 1); + /// ``` #[inline(always)] pub fn replace<'p: 'h, H: Into>>(&mut self, header: H) -> bool { let header = header.into(); @@ -310,6 +340,7 @@ impl<'h> HeaderMap<'h> { /// Replaces all of the values for a header with name `name` with `values`. /// This a low-level method and should rarely be used. /// + /// /// # Example /// /// ```rust @@ -475,6 +506,38 @@ impl<'h> HeaderMap<'h> { /// Returns an iterator over all of the `Header`s stored in the map. Header /// names are returned in no specific order, but all values for a given /// header name are grouped together, and values are in FIFO order. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{HeaderMap, Header}; + /// + /// // The headers we'll be storing. + /// let all_headers = vec![ + /// Header::new("X-Custom", "value_1"), + /// Header::new("X-Other", "other"), + /// Header::new("X-Third", "third"), + /// ]; + /// + /// // Create a map, store all of the headers. + /// let mut map = HeaderMap::new(); + /// for header in all_headers { + /// map.add(header) + /// } + /// + /// // Ensure there are three headers via the iterator. + /// assert_eq!(map.iter().count(), 3); + /// + /// // Actually iterate through them. + /// for header in map.iter() { + /// match header.name() { + /// "X-Custom" => assert_eq!(header.value(), "value_1"), + /// "X-Other" => assert_eq!(header.value(), "other"), + /// "X-Third" => assert_eq!(header.value(), "third"), + /// _ => unreachable!("there are only three headers") + /// } + /// } + /// ``` pub fn iter<'s>(&'s self) -> impl Iterator> { self.headers.iter().flat_map(|(key, values)| { values.iter().map(move |val| { @@ -487,7 +550,39 @@ impl<'h> HeaderMap<'h> { /// in the map. Header names are returned in no specific order, but all /// values for a given header name are grouped together, and values are in /// FIFO order. - // TODO: Figure out what the return type is to implement IntoIterator. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{HeaderMap, Header}; + /// + /// // The headers we'll be storing. + /// let all_headers = vec![ + /// Header::new("X-Custom", "value_1"), + /// Header::new("X-Other", "other"), + /// Header::new("X-Third", "third"), + /// ]; + /// + /// // Create a map, store all of the headers. + /// let mut map = HeaderMap::new(); + /// for header in all_headers { + /// map.add(header) + /// } + /// + /// // Ensure there are three headers via the iterator. + /// assert_eq!(map.iter().count(), 3); + /// + /// // Actually iterate through them. + /// for header in map.into_iter() { + /// match header.name() { + /// "X-Custom" => assert_eq!(header.value(), "value_1"), + /// "X-Other" => assert_eq!(header.value(), "other"), + /// "X-Third" => assert_eq!(header.value(), "third"), + /// _ => unreachable!("there are only three headers") + /// } + /// } + /// ``` + // TODO: Implement IntoIterator. #[inline(always)] pub fn into_iter(self) -> impl Iterator> { self.headers.into_iter().flat_map(|(name, value)| { diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 24cdb161..2dd865a2 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -40,8 +40,49 @@ impl Source { } } -// Describe a media type. In particular, describe its comparison and hashing -// semantics. +/// An HTTP media type. +/// +/// # Usage +/// +/// A `MediaType` should rarely be used directly. Instead, one is typically used +/// indirectly via types like [`Accept`] and [`ContentType`], which internally +/// contain `MediaType`s. Nonetheless, a `MediaType` can be created via the +/// [`new`], [`with_params`], and [`from_extension`] methods. The preferred +/// method, however, is to create a `MediaType` via an associated constant. +/// +/// [`Accept`]: /rocket/http/struct.Accept.html +/// [`ContentType`]: /rocket/http/struct.ContentType.html +/// [`new`]: /rocket/http/struct.MediaType.html#method.new +/// [`with_params`]: /rocket/http/struct.MediaType.html#method.with_params +/// [`from_extension`]: /rocket/http/struct.MediaType.html#method.from_extension +/// +/// ## Example +/// +/// A media type of `application/json` can be insantiated via the `JSON` +/// constant: +/// +/// ```rust +/// use rocket::http::MediaType; +/// +/// let json = MediaType::JSON; +/// assert_eq!(json.top(), "application"); +/// assert_eq!(json.sub(), "json"); +/// +/// let json = MediaType::new("application", "json"); +/// assert_eq!(MediaType::JSON, json); +/// ``` +/// +/// # Comparison and Hashing +/// +/// The `PartialEq` and `Hash` implementations for `MediaType` _do not_ take +/// into account parameters. This means that a media type of `text/html` is +/// equal to a media type of `text/html; charset=utf-8`, for instance. This is +/// typically the comparison that is desired. +/// +/// If an exact comparison is desired that takes into account parameters, the +/// [`exact_eq`] method can be used. +/// +/// [`exact_eq`]: /rocket/http/struct.MediaType.html#method.exact_eq #[derive(Debug, Clone)] pub struct MediaType { /// Storage for the entire media type string. diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index c70e6c1f..89bc9ebc 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -29,7 +29,7 @@ pub mod uncased; pub use self::method::Method; pub use self::content_type::ContentType; -pub use self::accept::{Accept, WeightedMediaType}; +pub use self::accept::{Accept, QMediaType}; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; pub use self::raw_str::RawStr; diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs index 13ff931f..0519e828 100644 --- a/lib/src/http/parse/accept.rs +++ b/lib/src/http/parse/accept.rs @@ -3,7 +3,7 @@ use pear::parsers::*; use http::parse::checkers::is_whitespace; use http::parse::media_type::media_type; -use http::{MediaType, Accept, WeightedMediaType}; +use http::{MediaType, Accept, QMediaType}; fn q<'a>(_: &'a str, media_type: &MediaType) -> ParseResult<&'a str, Option> { match media_type.params().next() { @@ -24,7 +24,7 @@ fn accept<'a>(input: &mut &'a str) -> ParseResult<&'a str, Accept> { skip_while(is_whitespace); let media_type = media_type(); let weight = q(&media_type); - media_types.push(WeightedMediaType(media_type, weight)); + media_types.push(QMediaType(media_type, weight)); }); Accept::new(media_types) diff --git a/lib/src/http/raw_str.rs b/lib/src/http/raw_str.rs index 5c81e0a1..76b7de41 100644 --- a/lib/src/http/raw_str.rs +++ b/lib/src/http/raw_str.rs @@ -10,31 +10,139 @@ use url; use http::uncased::UncasedStr; -/// A reference to a raw HTTP string. +/// A reference to a string inside of a raw HTTP message. +/// +/// A `RawStr` is an unsanitzed, unvalidated, and undecoded raw string from an +/// HTTP message. It exists to separate validated string inputs, represented by +/// the `String`, `&str`, and `Cow` types, from unvalidated inputs, +/// represented by `&RawStr`. +/// +/// # Validation +/// +/// An `&RawStr` should be converted into one of the validated string input +/// types through methods on `RawStr`. These methods are summarized below: +/// +/// * **[`url_decode`]** - used to decode a raw string in a form value context +/// * **[`percent_decode`], [`percent_decode_lossy`]** - used to +/// percent-decode a raw string, typically in a URL context +/// * **[`html_escape`]** - used to decode a string for use in HTML templates +/// * **[`as_str`]** - used when the `RawStr` is known to be safe in the +/// context of its intended use. Use sparingly and with care! +/// * **[`as_uncased_str`]** - used when the `RawStr` is known to be safe in +/// the context of its intended, uncased use +/// +/// [`as_str`]: /rocket/http/struct.RawStr.html#method.as_str +/// [`as_uncased_str`]: /rocket/http/struct.RawStr.html#method.as_uncased_str +/// [`url_decode`]: /rocket/http/struct.RawStr.html#method.url_decode +/// [`html_escape`]: /rocket/http/struct.RawStr.html#method.html_escape +/// [`percent_decode`]: /rocket/http/struct.RawStr.html#method.percent_decode +/// [`percent_decode_lossy`]: /rocket/http/struct.RawStr.html#method.percent_decode_lossy +/// +/// # Usage +/// +/// A `RawStr` is a dynamically sized type (just like `str`). It is always used +/// through a reference an as `&RawStr` (just like &str). You'll likely +/// encounted an `&RawStr` as a parameter via [`FromParam`] or as a form value +/// via [`FromFormValue`]. +/// +/// [`FromParam`]: /rocket/request/trait.FromParam.html +/// [`FromFormValue`]: /rocket/request/trait.FromFormValue.html #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RawStr(str); impl RawStr { + /// Constructs an `&RawStr` from an `&str` at no cost. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::from_str("Hello, world!"); + /// + /// // `into` can also be used; note that the type must be specified + /// let raw_str: &RawStr = "Hello, world!".into(); + /// ``` #[inline(always)] pub fn from_str<'a>(string: &'a str) -> &'a RawStr { string.into() } + /// Returns a percent-decoded version of the string. + /// + /// # Errors + /// + /// Returns an `Err` if the percent encoded values are not valid UTF-8. + /// + /// # Example + /// + /// With a valid string: + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::from_str("Hello%21"); + /// let decoded = raw_str.percent_decode(); + /// assert_eq!(decoded, Ok("Hello!".into())); + /// ``` + /// + /// With an invalid string: + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// // Note: Rocket should never hand you a bad `&RawStr`. + /// let bad_str = unsafe { ::std::str::from_utf8_unchecked(b"a=\xff") }; + /// let bad_raw_str = RawStr::from_str(bad_str); + /// assert!(bad_raw_str.percent_decode().is_err()); + /// ``` #[inline(always)] - pub fn as_str(&self) -> &str { - self + pub fn percent_decode(&self) -> Result, Utf8Error> { + url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8() } + /// Returns a percent-decoded version of the string. Any invalid UTF-8 + /// percent-encoded byte sequences will be replaced � U+FFFD, the + /// replacement character. + /// + /// # Errors + /// + /// Returns an `Err` if the percent encoded values are not valid UTF-8. + /// + /// # Example + /// + /// With a valid string: + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::from_str("Hello%21"); + /// let decoded = raw_str.percent_decode_lossy(); + /// assert_eq!(decoded, "Hello!"); + /// ``` + /// + /// With an invalid string: + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// // Note: Rocket should never hand you a bad `&RawStr`. + /// let bad_str = unsafe { ::std::str::from_utf8_unchecked(b"a=\xff") }; + /// let bad_raw_str = RawStr::from_str(bad_str); + /// assert_eq!(bad_raw_str.percent_decode_lossy(), "a=�"); + /// ``` #[inline(always)] - pub fn as_uncased_str(&self) -> &UncasedStr { - self.as_str().into() + pub fn percent_decode_lossy(&self) -> Cow { + url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8_lossy() } /// Returns a URL-decoded version of the string. This is identical to - /// percent decoding except that '+' characters are converted into spaces. + /// percent decoding except that `+` characters are converted into spaces. /// This is the encoding used by form values. /// - /// If the percent encoded values are not valid UTF-8, an `Err` is returned. + /// # Errors + /// + /// Returns an `Err` if the percent encoded values are not valid UTF-8. /// /// # Example /// @@ -45,7 +153,6 @@ impl RawStr { /// let decoded = raw_str.url_decode(); /// assert_eq!(decoded, Ok("Hello, world!".to_string())); /// ``` - #[inline] pub fn url_decode(&self) -> Result { let replaced = self.replace("+", " "); RawStr::from_str(replaced.as_str()) @@ -53,22 +160,15 @@ impl RawStr { .map(|cow| cow.into_owned()) } - /// Returns a percent-decoded version of the string. If the percent encoded - /// values are not valid UTF-8, an `Err` is returned. - #[inline(always)] - pub fn percent_decode(&self) -> Result, Utf8Error> { - url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8() - } - - /// Returns a percent-decoded version of the string. Any invalid UTF-8 - /// percent-encoded byte sequences will be replaced � U+FFFD, the - /// replacement character. - #[inline(always)] - pub fn percent_decode_lossy(&self) -> Cow { - url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8_lossy() - } - - /// Do some HTML escaping. + /// Returns an HTML escaped version of `self`. Allocates only when + /// characters need to be escaped. + /// + /// The following characters are escaped: `&`, `<`, `>`, `"`, `'`, `/`, + /// `. **This suffices as long as the escaped string is not + /// used in an execution context such as inside of <script> or <style> + /// tags!** See the [OWASP XSS Prevention Rules] for more information. + /// + /// [OWASP XSS Prevention Rules]: https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#XSS_Prevention_Rules /// /// # Example /// @@ -136,6 +236,42 @@ impl RawStr { Cow::Borrowed(self.as_str()) } } + + /// Converts `self` into an `&str`. + /// + /// This method should be used sparingly. **Only use this method when you + /// are absolutely certain that doing so is safe.** + /// + /// # Example + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::from_str("Hello, world!"); + /// assert_eq!(raw_str.as_str(), "Hello, world!"); + /// ``` + #[inline(always)] + pub fn as_str(&self) -> &str { + self + } + + /// Converts `self` into an `&UncasedStr`. + /// + /// This method should be used sparingly. **Only use this method when you + /// are absolutely certain that doing so is safe.** + /// + /// # Example + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::from_str("Content-Type"); + /// assert!(raw_str.as_uncased_str() == "content-TYPE"); + /// ``` + #[inline(always)] + pub fn as_uncased_str(&self) -> &UncasedStr { + self.as_str().into() + } } impl<'a> From<&'a str> for &'a RawStr { diff --git a/lib/src/http/status.rs b/lib/src/http/status.rs index 933c84cc..4c3bad14 100644 --- a/lib/src/http/status.rs +++ b/lib/src/http/status.rs @@ -19,7 +19,7 @@ pub enum StatusClass { Unknown } -/// Structure representing HTTP statuses: an integer code and a reason phrase. +/// Structure representing an HTTP status: an integer code and a reason phrase. /// /// # Usage /// diff --git a/lib/src/request/form/from_form.rs b/lib/src/request/form/from_form.rs index 68fbc35e..7b57b788 100644 --- a/lib/src/request/form/from_form.rs +++ b/lib/src/request/form/from_form.rs @@ -3,8 +3,15 @@ use request::FormItems; /// Trait to create an instance of some type from an HTTP form. /// [Form](struct.Form.html) requires its generic type to implement this trait. /// +/// # Deriving +/// /// This trait can be automatically derived via the -/// [rocket_codegen](/rocket_codegen) plugin: +/// [rocket_codegen](/rocket_codegen) plugin. When deriving `FromForm`, every +/// field in the structure must implement +/// [FromFormValue](trait.FromFormValue.html). Rocket validates each field in +/// the structure by calling its `FromFormValue` implemention. You may wish to +/// implement `FromFormValue` for your own types for custom, automatic +/// validation. /// /// ```rust /// #![feature(plugin, custom_derive)] @@ -21,8 +28,10 @@ use request::FormItems; /// # fn main() { } /// ``` /// -/// The type can then be parsed from incoming form data via the `data` -/// parameter and `Form` type. +/// # Data Guard +/// +/// Types that implement `FromForm` can be parsed directly from incoming form +/// data via the `data` parameter and `Form` type. /// /// ```rust /// # #![feature(plugin, custom_derive)] @@ -39,21 +48,70 @@ use request::FormItems; /// # fn main() { } /// ``` /// -/// When deriving `FromForm`, every field in the structure must implement -/// [FromFormValue](trait.FromFormValue.html). -/// /// # Implementing /// -/// An implementation of `FromForm` uses the [FormItems](struct.FormItems.html) +/// Implementing `FromForm` should be a rare occurence. Prefer instead to use +/// Rocket's built-in derivation. +/// +/// When implementing `FromForm`, use use the [FormItems](struct.FormItems.html) /// iterator to iterate through the raw form key/value pairs. Be aware that form /// fields that are typically hidden from your application, such as `_method`, -/// will be present while iterating. +/// will be present while iterating. Ensure that you adhere to the properties of +/// the `strict` parameter, as detailed in the documentation below. +/// +/// ## Example +/// +/// Consider the following scenario: we have a struct `Item` with field name +/// `field`. We'd like to parse any form that has a field named either `balloon` +/// _or_ `space`, and we'd like that field's value to be the value for our +/// structure's `field`. The following snippet shows how this would be +/// implemented: +/// +/// ```rust +/// use rocket::request::{FromForm, FormItems}; +/// +/// struct Item { +/// field: String +/// } +/// +/// impl<'f> FromForm<'f> for Item { +/// // In practice, we'd use a more descriptive error type. +/// type Error = (); +/// +/// fn from_form(items: &mut FormItems<'f>, strict: bool) -> Result { +/// let mut field = None; +/// +/// for (key, value) in items { +/// match key.as_str() { +/// "balloon" | "space" if field.is_none() => { +/// let decoded = value.url_decode().map_err(|_| ())?; +/// field = Some(decoded); +/// } +/// _ if strict => return Err(()), +/// _ => { /* allow extra value when not strict */ } +/// } +/// } +/// +/// field.map(|field| Item { field }).ok_or(()) +/// } +/// } +/// ``` pub trait FromForm<'f>: Sized { /// The associated error to be returned when parsing fails. type Error; - /// Parses an instance of `Self` from the iterator of form items `it` or - /// returns an instance of `Self::Error` if one cannot be parsed. + /// Parses an instance of `Self` from the iterator of form items `it`. + /// + /// Extra form field are allowed when `strict` is `false` and disallowed + /// when `strict` is `true`. + /// + /// # Errors + /// + /// If `Self` cannot be parsed from the given form items, an instance of + /// `Self::Error` will be returned. + /// + /// When `strict` is `true` and unexpected, extra fields are present in + /// `it`, an instance of `Self::Error` will be returned. fn from_form(it: &mut FormItems<'f>, strict: bool) -> Result; } diff --git a/lib/src/request/form/from_form_value.rs b/lib/src/request/form/from_form_value.rs index 5e765b37..a1d2e752 100644 --- a/lib/src/request/form/from_form_value.rs +++ b/lib/src/request/form/from_form_value.rs @@ -67,7 +67,7 @@ use http::RawStr; /// `"false"`, `"off"`, or not present. In any other case, the raw form /// value is returned in the `Err` value. /// -/// * **&RawStr** +/// * **&[RawStr](/rocket/http/struct.RawStr.html)** /// /// _This implementation always returns successfully._ /// diff --git a/lib/src/request/param.rs b/lib/src/request/param.rs index 7602e277..3c2cbf68 100644 --- a/lib/src/request/param.rs +++ b/lib/src/request/param.rs @@ -82,7 +82,7 @@ use http::RawStr; /// type returns successfully. Otherwise, the raw path segment is returned /// in the `Err` value. /// -/// * **&RawStr** +/// * **&[`RawStr`](/rocket/http/struct.RawStr.html)** /// /// _This implementation always returns successfully._ /// diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 5939c2e8..f13bf926 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -327,11 +327,6 @@ impl<'r> Request<'r> { }).as_ref() } - #[inline(always)] - pub fn accept_first(&self) -> Option<&MediaType> { - self.accept().and_then(|accept| accept.first()).map(|wmt| wmt.media_type()) - } - pub fn format(&self) -> Option<&MediaType> { static ANY: MediaType = MediaType::Any; if self.method.supports_payload() {