diff --git a/src/header/common/content_range.rs b/src/header/common/content_range.rs new file mode 100644 index 0000000000..25353cf4d9 --- /dev/null +++ b/src/header/common/content_range.rs @@ -0,0 +1,189 @@ +use std::fmt::{self, Display}; +use std::str::FromStr; + +header! { + #[doc="`Content-Range` header, defined in"] + #[doc="[RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2)"] + (ContentRange, "Content-Range") => [ContentRangeSpec] + + test_content_range { + test_header!(test_bytes, + vec![b"bytes 0-499/500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + instance_length: Some(500) + }))); + + test_header!(test_bytes_unknown_len, + vec![b"bytes 0-499/*"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + instance_length: None + }))); + + test_header!(test_bytes_unknown_range, + vec![b"bytes */500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: None, + instance_length: Some(500) + }))); + + test_header!(test_unregistered, + vec![b"seconds 1-2"], + Some(ContentRange(ContentRangeSpec::Unregistered { + unit: "seconds".to_string(), + resp: "1-2".to_string() + }))); + + test_header!(test_no_len, + vec![b"bytes 0-499"], + None::); + + test_header!(test_only_unit, + vec![b"bytes"], + None::); + + test_header!(test_end_less_than_start, + vec![b"bytes 499-0/500"], + None::); + + test_header!(test_blank, + vec![b""], + None::); + + test_header!(test_bytes_many_spaces, + vec![b"bytes 1-2/500 3"], + None::); + + test_header!(test_bytes_many_slashes, + vec![b"bytes 1-2/500/600"], + None::); + + test_header!(test_bytes_many_dashes, + vec![b"bytes 1-2-3/500"], + None::); + + } +} + + +/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) +/// +/// # ABNF +/// ```plain +/// Content-Range = byte-content-range +/// / other-content-range +/// +/// byte-content-range = bytes-unit SP +/// ( byte-range-resp / unsatisfied-range ) +/// +/// byte-range-resp = byte-range "/" ( complete-length / "*" ) +/// byte-range = first-byte-pos "-" last-byte-pos +/// unsatisfied-range = "*/" complete-length +/// +/// complete-length = 1*DIGIT +/// +/// other-content-range = other-range-unit SP other-range-resp +/// other-range-resp = *CHAR +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub enum ContentRangeSpec { + /// Byte range + Bytes { + /// First and last bytes of the range, omitted if request could not be + /// satisfied + range: Option<(u64, u64)>, + + /// Total length of the instance, can be omitted if unknown + instance_length: Option + }, + + /// Custom range, with unit not registered at IANA + Unregistered { + /// other-range-unit + unit: String, + + /// other-range-resp + resp: String + } +} + +fn split_in_two<'a>(s: &'a str, separator: char) -> Option<(&'a str, &'a str)> { + let mut iter = s.splitn(2, separator); + match (iter.next(), iter.next()) { + (Some(a), Some(b)) => Some((a, b)), + _ => None + } +} + +impl FromStr for ContentRangeSpec { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + let res = match split_in_two(s, ' ') { + Some(("bytes", resp)) => { + let (range, instance_length) = try!(split_in_two(resp, '/').ok_or(::Error::Header)); + + let instance_length = if instance_length == "*" { + None + } else { + Some(try!(instance_length.parse().map_err(|_| ::Error::Header))) + }; + + let range = if range == "*" { + None + } else { + let (first_byte, last_byte) = try!(split_in_two(range, '-').ok_or(::Error::Header)); + let first_byte = try!(first_byte.parse().map_err(|_| ::Error::Header)); + let last_byte = try!(last_byte.parse().map_err(|_| ::Error::Header)); + if last_byte < first_byte { + return Err(::Error::Header); + } + Some((first_byte, last_byte)) + }; + + ContentRangeSpec::Bytes { + range: range, + instance_length: instance_length + } + } + Some((unit, resp)) => { + ContentRangeSpec::Unregistered { + unit: unit.to_string(), + resp: resp.to_string() + } + } + _ => return Err(::Error::Header) + }; + Ok(res) + } +} + +impl Display for ContentRangeSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ContentRangeSpec::Bytes { range, instance_length } => { + try!(f.write_str("bytes ")); + match range { + Some((first_byte, last_byte)) => { + try!(write!(f, "{}-{}", first_byte, last_byte)); + }, + None => { + try!(f.write_str("*")); + } + }; + try!(f.write_str("/")); + if let Some(v) = instance_length { + write!(f, "{}", v) + } else { + f.write_str("*") + } + } + ContentRangeSpec::Unregistered { ref unit, ref resp } => { + try!(f.write_str(&unit)); + try!(f.write_str(" ")); + f.write_str(&resp) + } + } + } +} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index 3ce8dbbac8..f1be3a05cc 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -24,6 +24,7 @@ pub use self::connection::{Connection, ConnectionOption}; pub use self::content_length::ContentLength; pub use self::content_encoding::ContentEncoding; pub use self::content_language::ContentLanguage; +pub use self::content_range::{ContentRange, ContentRangeSpec}; pub use self::content_type::ContentType; pub use self::cookie::Cookie; pub use self::date::Date; @@ -368,6 +369,7 @@ mod connection; mod content_encoding; mod content_language; mod content_length; +mod content_range; mod content_type; mod date; mod etag;