diff --git a/src/header/common/link.rs b/src/header/common/link.rs new file mode 100644 index 0000000000..16fc2bf173 --- /dev/null +++ b/src/header/common/link.rs @@ -0,0 +1,1142 @@ +use std::fmt; +use std::borrow::Cow; +use std::str::FromStr; +use std::ascii::AsciiExt; + +use mime::Mime; +use language_tags::LanguageTag; + +use header::parsing; +use header::{Header, HeaderFormat}; + +/// The `Link` header, defined in +/// [RFC5988](http://tools.ietf.org/html/rfc5988#section-5) +/// +/// # ABNF +/// ```plain +/// Link = "Link" ":" #link-value +/// link-value = "<" URI-Reference ">" *( ";" link-param ) +/// link-param = ( ( "rel" "=" relation-types ) +/// | ( "anchor" "=" <"> URI-Reference <"> ) +/// | ( "rev" "=" relation-types ) +/// | ( "hreflang" "=" Language-Tag ) +/// | ( "media" "=" ( MediaDesc | ( <"> MediaDesc <"> ) ) ) +/// | ( "title" "=" quoted-string ) +/// | ( "title*" "=" ext-value ) +/// | ( "type" "=" ( media-type | quoted-mt ) ) +/// | ( link-extension ) ) +/// link-extension = ( parmname [ "=" ( ptoken | quoted-string ) ] ) +/// | ( ext-name-star "=" ext-value ) +/// ext-name-star = parmname "*" ; reserved for RFC2231-profiled +/// ; extensions. Whitespace NOT +/// ; allowed in between. +/// ptoken = 1*ptokenchar +/// ptokenchar = "!" | "#" | "$" | "%" | "&" | "'" | "(" +/// | ")" | "*" | "+" | "-" | "." | "/" | DIGIT +/// | ":" | "<" | "=" | ">" | "?" | "@" | ALPHA +/// | "[" | "]" | "^" | "_" | "`" | "{" | "|" +/// | "}" | "~" +/// media-type = type-name "/" subtype-name +/// quoted-mt = <"> media-type <"> +/// relation-types = relation-type +/// | <"> relation-type *( 1*SP relation-type ) <"> +/// relation-type = reg-rel-type | ext-rel-type +/// reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" ) +/// ext-rel-type = URI +/// ``` +/// +/// # Example values +/// +/// `Link: ; rel="previous"; +/// title="previous chapter"` +/// +/// `Link: ; rel="previous"; title*=UTF-8'de'letztes%20Kapitel, +/// ; rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel` +/// +/// # Examples +/// ``` +/// use hyper::header::{Headers, Link, LinkValue, RelationType}; +/// +/// let link_value = LinkValue::new("http://example.com/TheBook/chapter2") +/// .push_rel(RelationType::Previous) +/// .set_title("previous chapter"); +/// +/// let mut headers = Headers::new(); +/// headers.set( +/// Link::new(vec![link_value]) +/// ); +/// ``` +#[derive(Clone, PartialEq, Debug)] +pub struct Link { + /// A list of the `link-value`s of the Link entity-header. + values: Vec +} + +/// A single `link-value` of a `Link` header, based on: +/// [RFC5988](http://tools.ietf.org/html/rfc5988#section-5) +#[derive(Clone, PartialEq, Debug)] +pub struct LinkValue { + /// Target IRI: `link-value`. + link: Cow<'static, str>, + + /// Forward Relation Types: `rel`. + rel: Option>, + + /// Context IRI: `anchor`. + anchor: Option, + + /// Reverse Relation Types: `rev`. + rev: Option>, + + /// Hint on the language of the result of dereferencing + /// the link: `hreflang`. + href_lang: Option>, + + /// Destination medium or media: `media`. + media_desc: Option>, + + /// Label of the destination of a Link: `title`. + title: Option, + + /// The `title` encoded in a different charset: `title*`. + title_star: Option, + + /// Hint on the media type of the result of dereferencing + /// the link: `type`. + media_type: Option, + + /// Link Extension: `link-extension`. + link_extension: Option +} + +/// A Media Descriptors Enum based on: +/// https://www.w3.org/TR/html401/types.html#h-6.13 +#[derive(Clone, PartialEq, Debug)] +pub enum MediaDesc { + /// screen. + Screen, + /// tty. + Tty, + /// tv. + Tv, + /// projection. + Projection, + /// handheld. + Handheld, + /// print. + Print, + /// braille. + Braille, + /// aural. + Aural, + /// all. + All, + /// Unrecognized media descriptor extension. + Extension(String) +} + +/// A Link Relation Type Enum based on: +/// [RFC5988](https://tools.ietf.org/html/rfc5988#section-6.2.2) +#[derive(Clone, PartialEq, Debug)] +pub enum RelationType { + /// alternate. + Alternate, + /// appendix. + Appendix, + /// bookmark. + Bookmark, + /// chapter. + Chapter, + /// contents. + Contents, + /// copyright. + Copyright, + /// current. + Current, + /// describedby. + DescribedBy, + /// edit. + Edit, + /// edit-media. + EditMedia, + /// enclosure. + Enclosure, + /// first. + First, + /// glossary. + Glossary, + /// help. + Help, + /// hub. + Hub, + /// index. + Index, + /// last. + Last, + /// latest-version. + LatestVersion, + /// license. + License, + /// next. + Next, + /// next-archive. + NextArchive, + /// payment. + Payment, + /// prev. + Prev, + /// predecessor-version. + PredecessorVersion, + /// previous. + Previous, + /// prev-archive. + PrevArchive, + /// related. + Related, + /// replies. + Replies, + /// section. + Section, + /// self. + RelationTypeSelf, + /// service. + Service, + /// start. + Start, + /// stylesheet. + Stylesheet, + /// subsection. + Subsection, + /// successor-version. + SuccessorVersion, + /// up. + Up, + /// versionHistory. + VersionHistory, + /// via. + Via, + /// working-copy. + WorkingCopy, + /// working-copy-of. + WorkingCopyOf, + /// ext-rel-type. + ExtRelType(String) +} + +//////////////////////////////////////////////////////////////////////////////// +// Struct methods +//////////////////////////////////////////////////////////////////////////////// + +impl Link { + /// Create `Link` from a `Vec`. + pub fn new(link_values: Vec) -> Link { + Link { values: link_values } + } + + /// Get the `Link` header's `LinkValue`s. + pub fn values(&self) -> &[LinkValue] { + self.values.as_ref() + } + + /// Add a `LinkValue` instance to the `Link` header's values. + pub fn push_value(&mut self, link_value: LinkValue) { + self.values.push(link_value); + } +} + +impl LinkValue { + /// Create `LinkValue` from URI-Reference. + pub fn new(uri: T) -> LinkValue + where T: Into> { + LinkValue { + link: uri.into(), + rel: None, + anchor: None, + rev: None, + href_lang: None, + media_desc: None, + title: None, + title_star: None, + media_type: None, + link_extension: None, + } + } + + /// Get the `LinkValue`'s value. + pub fn link(&self) -> &str { + self.link.as_ref() + } + + /// Get the `LinkValue`'s `rel` parameter(s). + pub fn rel(&self) -> Option<&[RelationType]> { + self.rel.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `anchor` parameter. + pub fn anchor(&self) -> Option<&str> { + self.anchor.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `rev` parameter(s). + pub fn rev(&self) -> Option<&[RelationType]> { + self.rev.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `hreflang` parameter(s). + pub fn href_lang(&self) -> Option<&[LanguageTag]> { + self.href_lang.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `media` parameter(s). + pub fn media_desc(&self) -> Option<&[MediaDesc]> { + self.media_desc.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `title` parameter. + pub fn title(&self) -> Option<&str> { + self.title.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `title*` parameter. + pub fn title_star(&self) -> Option<&str> { + self.title_star.as_ref().map(AsRef::as_ref) + } + + /// Get the `LinkValue`'s `type` parameter. + pub fn media_type(&self) -> Option<&Mime> { + self.media_type.as_ref() + } + + /// Get the `LinkValue`'s `link-extension` parameter. + pub fn link_extension(&self) -> Option<&str> { + self.link_extension.as_ref().map(AsRef::as_ref) + } + + /// Add a `RelationType` to the `LinkValue`'s `rel` parameter. + pub fn push_rel(mut self, rel: RelationType) -> LinkValue { + let mut v = self.rel.take().unwrap_or(Vec::new()); + + v.push(rel); + + self.rel = Some(v); + + self + } + + /// Set `LinkValue`'s `anchor` parameter. + pub fn set_anchor>(mut self, anchor: T) -> LinkValue { + self.anchor = Some(anchor.into()); + + self + } + + /// Add a `RelationType` to the `LinkValue`'s `rev` parameter. + pub fn push_rev(mut self, rev: RelationType) -> LinkValue { + let mut v = self.rev.take().unwrap_or(Vec::new()); + + v.push(rev); + + self.rev = Some(v); + + self + } + + /// Add a `LanguageTag` to the `LinkValue`'s `hreflang` parameter. + pub fn push_href_lang(mut self, language_tag: LanguageTag) -> LinkValue { + let mut v = self.href_lang.take().unwrap_or(Vec::new()); + + v.push(language_tag); + + self.href_lang = Some(v); + + self + } + + /// Add a `MediaDesc` to the `LinkValue`'s `media_desc` parameter. + pub fn push_media_desc(mut self, media_desc: MediaDesc) -> LinkValue { + let mut v = self.media_desc.take().unwrap_or(Vec::new()); + + v.push(media_desc); + + self.media_desc = Some(v); + + self + } + + /// Set `LinkValue`'s `title` parameter. + pub fn set_title>(mut self, title: T) -> LinkValue { + self.title = Some(title.into()); + + self + } + + /// Set `LinkValue`'s `title*` parameter. + pub fn set_title_star>(mut self, title_star: T) -> LinkValue { + self.title_star = Some(title_star.into()); + + self + } + + /// Set `LinkValue`'s `type` parameter. + pub fn set_media_type(mut self, media_type: Mime) -> LinkValue { + self.media_type = Some(media_type); + + self + } + + /// Set `LinkValue`'s `link-extension` parameter. + pub fn set_link_extension>(mut self, link_extension: T) -> LinkValue { + self.link_extension = Some(link_extension.into()); + + self + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Trait implementations +//////////////////////////////////////////////////////////////////////////////// + +impl Header for Link { + fn header_name() -> &'static str { + static NAME: &'static str = "Link"; + NAME + } + + fn parse_header(raw: &[Vec]) -> ::Result { + // If more that one `Link` headers are present in a request's + // headers they are combined in a single `Link` header containing + // all the `link-value`s present in each of those `Link` headers. + raw.iter() + .map(|v| parsing::from_raw_str::(&v)) + .fold(None, |p, c| match (p, c) { + (None, c) => Some(c), + (e @ Some(Err(_)), _) => e, + (Some(Ok(mut p)), Ok(c)) => { + p.values.extend(c.values); + + Some(Ok(p)) + } + _ => Some(Err(::Error::Header)), + }) + .unwrap_or(Err(::Error::Header)) + } +} +impl HeaderFormat for Link { + fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_delimited(f, self.values.as_slice(), ", ", ("", "")) + } +} + +impl fmt::Display for Link { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_header(f) + } +} + +impl fmt::Display for LinkValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + try!(write!(f, "<{}>", self.link)); + + if let Some(ref rel) = self.rel { + try!(fmt_delimited(f, rel.as_slice(), " ", ("; rel=\"", "\""))); + } + if let Some(ref anchor) = self.anchor { + try!(write!(f, "; anchor=\"{}\"", anchor)); + } + if let Some(ref rev) = self.rev { + try!(fmt_delimited(f, rev.as_slice(), " ", ("; rev=\"", "\""))); + } + if let Some(ref href_lang) = self.href_lang { + for tag in href_lang { + try!(write!(f, "; hreflang={}", tag)); + } + } + if let Some(ref media_desc) = self.media_desc { + try!(fmt_delimited(f, media_desc.as_slice(), ", ", ("; media=\"", "\""))); + } + if let Some(ref title) = self.title { + try!(write!(f, "; title=\"{}\"", title)); + } + if let Some(ref title_star) = self.title_star { + try!(write!(f, "; title*={}", title_star)); + } + if let Some(ref media_type) = self.media_type { + try!(write!(f, "; type=\"{}\"", media_type)); + } + if let Some(ref link_extension) = self.link_extension { + try!(write!(f, "; link-extension={}", link_extension)); + } + + Ok(()) + } +} + +impl FromStr for Link { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + // Create a split iterator with delimiters: `;`, `,` + let link_split = SplitAsciiUnquoted::new(s, ";,"); + + let mut link_values: Vec = Vec::new(); + + // Loop over the splits parsing the Link header into + // a `Vec` + for segment in link_split { + // Parse the `Target IRI` + // https://tools.ietf.org/html/rfc5988#section-5.1 + if segment.trim().starts_with('<') { + link_values.push( + match verify_and_trim(segment.trim(), (b'<', b'>')) { + Err(_) => return Err(::Error::Header), + Ok(s) => { + LinkValue { + link: s.to_owned().into(), + rel: None, + anchor: None, + rev: None, + href_lang: None, + media_desc: None, + title: None, + title_star: None, + media_type: None, + link_extension: None, + } + }, + } + ); + } else { + // Parse the current link-value's parameters + let mut link_param_split = segment.splitn(2, '='); + + let link_param_name = match link_param_split.next() { + None => return Err(::Error::Header), + Some(p) => p.trim(), + }; + + let mut link_header = match link_values.last_mut() { + None => return Err(::Error::Header), + Some(l) => l, + }; + + if "rel".eq_ignore_ascii_case(link_param_name) { + // Parse relation type: `rel`. + // https://tools.ietf.org/html/rfc5988#section-5.3 + if link_header.rel.is_none() { + link_header.rel = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => { + s.trim_matches(|c: char| c == '"' || c.is_whitespace()) + .split(' ') + .map(|t| t.trim().parse()) + .collect::, _>>() + .or_else(|_| return Err(::Error::Header)) + .ok() + }, + }; + } + } else if "anchor".eq_ignore_ascii_case(link_param_name) { + // Parse the `Context IRI`. + // https://tools.ietf.org/html/rfc5988#section-5.2 + link_header.anchor = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => match verify_and_trim(s.trim(), (b'"', b'"')) { + Err(_) => return Err(::Error::Header), + Ok(a) => Some(String::from(a)), + }, + }; + } else if "rev".eq_ignore_ascii_case(link_param_name) { + // Parse relation type: `rev`. + // https://tools.ietf.org/html/rfc5988#section-5.3 + if link_header.rev.is_none() { + link_header.rev = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => { + s.trim_matches(|c: char| c == '"' || c.is_whitespace()) + .split(' ') + .map(|t| t.trim().parse()) + .collect::, _>>() + .or_else(|_| return Err(::Error::Header)) + .ok() + }, + } + } + } else if "hreflang".eq_ignore_ascii_case(link_param_name) { + // Parse target attribute: `hreflang`. + // https://tools.ietf.org/html/rfc5988#section-5.4 + let mut v = link_header.href_lang.take().unwrap_or(Vec::new()); + + v.push( + match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => match s.trim().parse() { + Err(_) => return Err(::Error::Header), + Ok(t) => t, + }, + } + ); + + link_header.href_lang = Some(v); + } else if "media".eq_ignore_ascii_case(link_param_name) { + // Parse target attribute: `media`. + // https://tools.ietf.org/html/rfc5988#section-5.4 + if link_header.media_desc.is_none() { + link_header.media_desc = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => { + s.trim_matches(|c: char| c == '"' || c.is_whitespace()) + .split(',') + .map(|t| t.trim().parse()) + .collect::, _>>() + .or_else(|_| return Err(::Error::Header)) + .ok() + }, + }; + } + } else if "title".eq_ignore_ascii_case(link_param_name) { + // Parse target attribute: `title`. + // https://tools.ietf.org/html/rfc5988#section-5.4 + if link_header.title.is_none() { + link_header.title = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => match verify_and_trim(s.trim(), (b'"', b'"')) { + Err(_) => return Err(::Error::Header), + Ok(t) => Some(String::from(t)), + }, + }; + } + } else if "title*".eq_ignore_ascii_case(link_param_name) { + // Parse target attribute: `title*`. + // https://tools.ietf.org/html/rfc5988#section-5.4 + // + // Definition of `ext-value`: + // https://tools.ietf.org/html/rfc5987#section-3.2.1 + if link_header.title_star.is_none() { + link_header.title_star = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => Some(String::from(s.trim())), + }; + } + } else if "type".eq_ignore_ascii_case(link_param_name) { + // Parse target attribute: `type`. + // https://tools.ietf.org/html/rfc5988#section-5.4 + if link_header.media_type.is_none() { + link_header.media_type = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => match verify_and_trim(s.trim(), (b'"', b'"')) { + Err(_) => return Err(::Error::Header), + Ok(t) => match t.parse() { + Err(_) => return Err(::Error::Header), + Ok(m) => Some(m), + }, + }, + + }; + } + } else if "link-extension".eq_ignore_ascii_case(link_param_name) { + // Parse target attribute: `link-extension`. + // https://tools.ietf.org/html/rfc5988#section-5.4 + if link_header.link_extension.is_none() { + link_header.link_extension = match link_param_split.next() { + None => return Err(::Error::Header), + Some("") => return Err(::Error::Header), + Some(s) => Some(String::from(s.trim())), + }; + } + } else { + return Err(::Error::Header); + } + } + } + + Ok(Link::new(link_values)) + } +} + +impl fmt::Display for MediaDesc { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + MediaDesc::Screen => write!(f, "screen"), + MediaDesc::Tty => write!(f, "tty"), + MediaDesc::Tv => write!(f, "tv"), + MediaDesc::Projection => write!(f, "projection"), + MediaDesc::Handheld => write!(f, "handheld"), + MediaDesc::Print => write!(f, "print"), + MediaDesc::Braille => write!(f, "braille"), + MediaDesc::Aural => write!(f, "aural"), + MediaDesc::All => write!(f, "all"), + MediaDesc::Extension(ref other) => write!(f, "{}", other), + } + } +} + +impl FromStr for MediaDesc { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + match s { + "screen" => Ok(MediaDesc::Screen), + "tty" => Ok(MediaDesc::Tty), + "tv" => Ok(MediaDesc::Tv), + "projection" => Ok(MediaDesc::Projection), + "handheld" => Ok(MediaDesc::Handheld), + "print" => Ok(MediaDesc::Print), + "braille" => Ok(MediaDesc::Braille), + "aural" => Ok(MediaDesc::Aural), + "all" => Ok(MediaDesc::All), + _ => Ok(MediaDesc::Extension(String::from(s))), + } + } +} + +impl fmt::Display for RelationType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + RelationType::Alternate => write!(f, "alternate"), + RelationType::Appendix => write!(f, "appendix"), + RelationType::Bookmark => write!(f, "bookmark"), + RelationType::Chapter => write!(f, "chapter"), + RelationType::Contents => write!(f, "contents"), + RelationType::Copyright => write!(f, "copyright"), + RelationType::Current => write!(f, "current"), + RelationType::DescribedBy => write!(f, "describedby"), + RelationType::Edit => write!(f, "edit"), + RelationType::EditMedia => write!(f, "edit-media"), + RelationType::Enclosure => write!(f, "enclosure"), + RelationType::First => write!(f, "first"), + RelationType::Glossary => write!(f, "glossary"), + RelationType::Help => write!(f, "help"), + RelationType::Hub => write!(f, "hub"), + RelationType::Index => write!(f, "index"), + RelationType::Last => write!(f, "last"), + RelationType::LatestVersion => write!(f, "latest-version"), + RelationType::License => write!(f, "license"), + RelationType::Next => write!(f, "next"), + RelationType::NextArchive => write!(f, "next-archive"), + RelationType::Payment => write!(f, "payment"), + RelationType::Prev => write!(f, "prev"), + RelationType::PredecessorVersion => write!(f, "predecessor-version"), + RelationType::Previous => write!(f, "previous"), + RelationType::PrevArchive => write!(f, "prev-archive"), + RelationType::Related => write!(f, "related"), + RelationType::Replies => write!(f, "replies"), + RelationType::Section => write!(f, "section"), + RelationType::RelationTypeSelf => write!(f, "self"), + RelationType::Service => write!(f, "service"), + RelationType::Start => write!(f, "start"), + RelationType::Stylesheet => write!(f, "stylesheet"), + RelationType::Subsection => write!(f, "subsection"), + RelationType::SuccessorVersion => write!(f, "successor-version"), + RelationType::Up => write!(f, "up"), + RelationType::VersionHistory => write!(f, "version-history"), + RelationType::Via => write!(f, "via"), + RelationType::WorkingCopy => write!(f, "working-copy"), + RelationType::WorkingCopyOf => write!(f, "working-copy-of"), + RelationType::ExtRelType(ref uri) => write!(f, "{}", uri), + } + } +} + +impl FromStr for RelationType { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + if "alternate".eq_ignore_ascii_case(s) { + Ok(RelationType::Alternate) + } else if "appendix".eq_ignore_ascii_case(s) { + Ok(RelationType::Appendix) + } else if "bookmark".eq_ignore_ascii_case(s) { + Ok(RelationType::Bookmark) + } else if "chapter".eq_ignore_ascii_case(s) { + Ok(RelationType::Chapter) + } else if "contents".eq_ignore_ascii_case(s) { + Ok(RelationType::Contents) + } else if "copyright".eq_ignore_ascii_case(s) { + Ok(RelationType::Copyright) + } else if "current".eq_ignore_ascii_case(s) { + Ok(RelationType::Current) + } else if "describedby".eq_ignore_ascii_case(s) { + Ok(RelationType::DescribedBy) + } else if "edit".eq_ignore_ascii_case(s) { + Ok(RelationType::Edit) + } else if "edit-media".eq_ignore_ascii_case(s) { + Ok(RelationType::EditMedia) + } else if "enclosure".eq_ignore_ascii_case(s) { + Ok(RelationType::Enclosure) + } else if "first".eq_ignore_ascii_case(s) { + Ok(RelationType::First) + } else if "glossary".eq_ignore_ascii_case(s) { + Ok(RelationType::Glossary) + } else if "help".eq_ignore_ascii_case(s) { + Ok(RelationType::Help) + } else if "hub".eq_ignore_ascii_case(s) { + Ok(RelationType::Hub) + } else if "index".eq_ignore_ascii_case(s) { + Ok(RelationType::Index) + } else if "last".eq_ignore_ascii_case(s) { + Ok(RelationType::Last) + } else if "latest-version".eq_ignore_ascii_case(s) { + Ok(RelationType::LatestVersion) + } else if "license".eq_ignore_ascii_case(s) { + Ok(RelationType::License) + } else if "next".eq_ignore_ascii_case(s) { + Ok(RelationType::Next) + } else if "next-archive".eq_ignore_ascii_case(s) { + Ok(RelationType::NextArchive) + } else if "payment".eq_ignore_ascii_case(s) { + Ok(RelationType::Payment) + } else if "prev".eq_ignore_ascii_case(s) { + Ok(RelationType::Prev) + } else if "predecessor-version".eq_ignore_ascii_case(s) { + Ok(RelationType::PredecessorVersion) + } else if "previous".eq_ignore_ascii_case(s) { + Ok(RelationType::Previous) + } else if "prev-archive".eq_ignore_ascii_case(s) { + Ok(RelationType::PrevArchive) + } else if "related".eq_ignore_ascii_case(s) { + Ok(RelationType::Related) + } else if "replies".eq_ignore_ascii_case(s) { + Ok(RelationType::Replies) + } else if "section".eq_ignore_ascii_case(s) { + Ok(RelationType::Section) + } else if "self".eq_ignore_ascii_case(s) { + Ok(RelationType::RelationTypeSelf) + } else if "service".eq_ignore_ascii_case(s) { + Ok(RelationType::Service) + } else if "start".eq_ignore_ascii_case(s) { + Ok(RelationType::Start) + } else if "stylesheet".eq_ignore_ascii_case(s) { + Ok(RelationType::Stylesheet) + } else if "subsection".eq_ignore_ascii_case(s) { + Ok(RelationType::Subsection) + } else if "successor-version".eq_ignore_ascii_case(s) { + Ok(RelationType::SuccessorVersion) + } else if "up".eq_ignore_ascii_case(s) { + Ok(RelationType::Up) + } else if "version-history".eq_ignore_ascii_case(s) { + Ok(RelationType::VersionHistory) + } else if "via".eq_ignore_ascii_case(s) { + Ok(RelationType::Via) + } else if "working-copy".eq_ignore_ascii_case(s) { + Ok(RelationType::WorkingCopy) + } else if "working-copy-of".eq_ignore_ascii_case(s) { + Ok(RelationType::WorkingCopyOf) + } else { + Ok(RelationType::ExtRelType(String::from(s))) + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Utilities +//////////////////////////////////////////////////////////////////////////////// + +struct SplitAsciiUnquoted<'a> { + src: &'a str, + pos: usize, + del: &'a str +} + +impl<'a> SplitAsciiUnquoted<'a> { + fn new(s: &'a str, d: &'a str) -> SplitAsciiUnquoted<'a> { + SplitAsciiUnquoted{ + src: s, + pos: 0, + del: d, + } + } +} + +impl<'a> Iterator for SplitAsciiUnquoted<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + if self.pos < self.src.len() { + let prev_pos = self.pos; + let mut pos = self.pos; + + let mut in_quotes = false; + + for c in self.src[prev_pos..].as_bytes().iter() { + in_quotes ^= *c == b'"'; + + // Ignore `c` if we're `in_quotes`. + if !in_quotes && self.del.as_bytes().contains(c) { + break; + } + + pos += 1; + } + + self.pos = pos + 1; + + Some(&self.src[prev_pos..pos]) + } else { + None + } + } +} + +fn fmt_delimited(f: &mut fmt::Formatter, p: &[T], d: &str, b: (&str, &str)) -> fmt::Result { + if p.len() != 0 { + // Write a starting string `b.0` before the first element + try!(write!(f, "{}{}", b.0, p[0])); + + for i in &p[1..] { + // Write the next element preceded by the delimiter `d` + try!(write!(f, "{}{}", d, i)); + } + + // Write a ending string `b.1` before the first element + try!(write!(f, "{}", b.1)); + } + + Ok(()) +} + +fn verify_and_trim(s: &str, b: (u8, u8)) -> ::Result<&str> { + let length = s.len(); + let byte_array = s.as_bytes(); + + // Verify that `s` starts with `b.0` and ends with `b.1` and return + // the contained substring after triming whitespace. + if length > 1 && b.0 == byte_array[0] && b.1 == byte_array[length - 1] { + Ok(s.trim_matches( + |c: char| c == b.0 as char || c == b.1 as char || c.is_whitespace()) + ) + } else { + Err(::Error::Header) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use std::fmt; + use std::fmt::Write; + + use super::{Link, LinkValue, MediaDesc, RelationType, SplitAsciiUnquoted}; + use super::{fmt_delimited, verify_and_trim}; + + use header::Header; + + use buffer::BufReader; + use mock::MockStream; + use http::h1::parse_request; + + use mime::Mime; + use mime::TopLevel::Text; + use mime::SubLevel::Plain; + + #[test] + fn test_link() { + let link_value = LinkValue::new("http://example.com/TheBook/chapter2") + .push_rel(RelationType::Previous) + .push_rev(RelationType::Next) + .set_title("previous chapter"); + + let link_header = b"; \ + rel=\"previous\"; rev=next; title=\"previous chapter\""; + + let expected_link = Link::new(vec![link_value]); + + let link = Header::parse_header(&vec![link_header.to_vec()]); + assert_eq!(link.ok(), Some(expected_link)); + } + + #[test] + fn test_link_multiple_values() { + let first_link = LinkValue::new("/TheBook/chapter2") + .push_rel(RelationType::Previous) + .set_title_star("UTF-8'de'letztes%20Kapitel"); + + let second_link = LinkValue::new("/TheBook/chapter4") + .push_rel(RelationType::Next) + .set_title_star("UTF-8'de'n%c3%a4chstes%20Kapitel"); + + let link_header = b"; \ + rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, \ + ; \ + rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel"; + + let expected_link = Link::new(vec![first_link, second_link]); + + let link = Header::parse_header(&vec![link_header.to_vec()]); + assert_eq!(link.ok(), Some(expected_link)); + } + + #[test] + fn test_link_all_attributes() { + let link_value = LinkValue::new("http://example.com/TheBook/chapter2") + .push_rel(RelationType::Previous) + .set_anchor("../anchor/example/") + .push_rev(RelationType::Next) + .push_href_lang(langtag!(de)) + .push_media_desc(MediaDesc::Screen) + .set_title("previous chapter") + .set_title_star("title* unparsed") + .set_media_type(Mime(Text, Plain, vec![])) + .set_link_extension("link-extension unparsed"); + + let link_header = b"; \ + rel=\"previous\"; anchor=\"../anchor/example/\"; \ + rev=\"next\"; hreflang=de; media=\"screen\"; \ + title=\"previous chapter\"; title*=title* unparsed; \ + type=\"text/plain\"; link-extension=link-extension unparsed"; + + let expected_link = Link::new(vec![link_value]); + + let link = Header::parse_header(&vec![link_header.to_vec()]); + assert_eq!(link.ok(), Some(expected_link)); + } + + #[test] + fn test_link_multiple_link_headers() { + let first_link = LinkValue::new("/TheBook/chapter2") + .push_rel(RelationType::Previous) + .set_title_star("UTF-8'de'letztes%20Kapitel"); + + let second_link = LinkValue::new("/TheBook/chapter4") + .push_rel(RelationType::Next) + .set_title_star("UTF-8'de'n%c3%a4chstes%20Kapitel"); + + let third_link = LinkValue::new("http://example.com/TheBook/chapter2") + .push_rel(RelationType::Previous) + .push_rev(RelationType::Next) + .set_title("previous chapter"); + + let expected_link = Link::new(vec![first_link, second_link, third_link]); + + let mut raw = MockStream::with_input(b"GET /super_short_uri/and_whatever HTTP/1.1\r\nHost: \ + hyper.rs\r\nAccept: a lot of things\r\nAccept-Charset: \ + utf8\r\nAccept-Encoding: *\r\nLink: ; \ + rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, \ + ; rel=\"next\"; title*=\ + UTF-8'de'n%c3%a4chstes%20Kapitel\r\n\ + Access-Control-Allow-Credentials: None\r\nLink: \ + ; \ + rel=\"previous\"; rev=next; title=\"previous chapter\"\ + \r\n\r\n"); + + let mut buf = BufReader::new(&mut raw); + let res = parse_request(&mut buf).unwrap(); + + let link = res.headers.get::().unwrap(); + + assert_eq!(*link, expected_link); + } + + #[test] + fn test_link_display() { + let link_value = LinkValue::new("http://example.com/TheBook/chapter2") + .push_rel(RelationType::Previous) + .set_anchor("/anchor/example/") + .push_rev(RelationType::Next) + .push_href_lang(langtag!(de)) + .push_media_desc(MediaDesc::Screen) + .set_title("previous chapter") + .set_title_star("title* unparsed") + .set_media_type(Mime(Text, Plain, vec![])) + .set_link_extension("link-extension unparsed"); + + let link = Link::new(vec![link_value]); + + let mut link_header = String::new(); + write!(&mut link_header, "{}", link).unwrap(); + + let expected_link_header = "; \ + rel=\"previous\"; anchor=\"/anchor/example/\"; \ + rev=\"next\"; hreflang=de; media=\"screen\"; \ + title=\"previous chapter\"; title*=title* unparsed; \ + type=\"text/plain\"; link-extension=link-extension unparsed"; + + assert_eq!(link_header, expected_link_header); + } + + #[test] + fn test_link_parsing_errors() { + let link_a = b"http://example.com/TheBook/chapter2; \ + rel=\"previous\"; rev=next; title=\"previous chapter\""; + + let mut err: Result = Header::parse_header(&vec![link_a.to_vec()]); + assert_eq!(err.is_err(), true); + + let link_b = b"; \ + =\"previous\"; rev=next; title=\"previous chapter\""; + + err = Header::parse_header(&vec![link_b.to_vec()]); + assert_eq!(err.is_err(), true); + + let link_c = b"; \ + rel=; rev=next; title=\"previous chapter\""; + + err = Header::parse_header(&vec![link_c.to_vec()]); + assert_eq!(err.is_err(), true); + + let link_d = b"; \ + rel=\"previous\"; rev=next; title="; + + err = Header::parse_header(&vec![link_d.to_vec()]); + assert_eq!(err.is_err(), true); + + let link_e = b"; \ + rel=\"previous\"; rev=next; attr=unknown"; + + err = Header::parse_header(&vec![link_e.to_vec()]); + assert_eq!(err.is_err(), true); + } + + #[test] + fn test_link_split_ascii_unquoted_iterator() { + let string = "some, text; \"and, more; in quotes\", or not"; + let mut string_split = SplitAsciiUnquoted::new(string, ";,"); + + assert_eq!(Some("some"), string_split.next()); + assert_eq!(Some(" text"), string_split.next()); + assert_eq!(Some(" \"and, more; in quotes\""), string_split.next()); + assert_eq!(Some(" or not"), string_split.next()); + assert_eq!(None, string_split.next()); + } + + #[test] + fn test_link_fmt_delimited() { + struct TestFormatterStruct<'a> { v: Vec<&'a str> }; + + impl<'a> fmt::Display for TestFormatterStruct<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_delimited(f, self.v.as_slice(), ", ", (">>", "<<")) + } + } + + let test_formatter = TestFormatterStruct { v: vec!["first", "second"] }; + + let mut string = String::new(); + write!(&mut string, "{}", test_formatter).unwrap(); + + let expected_string = ">>first, second<<"; + + assert_eq!(string, expected_string); + } + + #[test] + fn test_link_verify_and_trim() { + let string = verify_and_trim("> some string <", (b'>', b'<')); + assert_eq!(string.ok(), Some("some string")); + + let err = verify_and_trim(" > some string <", (b'>', b'<')); + assert_eq!(err.is_err(), true); + } +} + +bench_header!(bench_link, Link, { vec![b"; rel=\"previous\"; rev=next; title=\"previous chapter\"; type=\"text/html\"; media=\"screen, tty\"".to_vec()] }); diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index a91ca302bd..3b96f5157b 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -57,6 +57,7 @@ pub use self::transfer_encoding::TransferEncoding; pub use self::upgrade::{Upgrade, Protocol, ProtocolName}; pub use self::user_agent::UserAgent; pub use self::vary::Vary; +pub use self::link::{Link, LinkValue, RelationType, MediaDesc}; #[doc(hidden)] #[macro_export] @@ -391,3 +392,4 @@ mod transfer_encoding; mod upgrade; mod user_agent; mod vary; +mod link;