Skip to content

Commit

Permalink
refactor(headers): factor out EntityTag from Etag
Browse files Browse the repository at this point in the history
Allow use of EntityTag in other headers that use entity tags.

BREAKING CHANGE: for any consumers of the Etag header, since the entity
tag is now in a tuple.
  • Loading branch information
hugoduncan committed Feb 7, 2015
1 parent c2784bc commit 28fd5c8
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 71 deletions.
88 changes: 18 additions & 70 deletions src/header/common/etag.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use header::{Header, HeaderFormat};
use header::{EntityTag, Header, HeaderFormat};
use std::fmt::{self};
use header::parsing::from_one_raw_str;

Expand All @@ -9,123 +9,71 @@ use header::parsing::from_one_raw_str;
/// which always looks like this: W/
/// See also: https://tools.ietf.org/html/rfc7232#section-2.3
#[derive(Clone, PartialEq, Debug)]
pub struct Etag {
/// Weakness indicator for the tag
pub weak: bool,
/// The opaque string in between the DQUOTEs
pub tag: String
}
pub struct Etag(pub EntityTag);

deref!(Etag => EntityTag);

impl Header for Etag {
fn header_name() -> &'static str {
"Etag"
}

fn parse_header(raw: &[Vec<u8>]) -> Option<Etag> {
// check that each char in the slice is either:
// 1. %x21, or
// 2. in the range %x23 to %x7E, or
// 3. in the range %x80 to %xFF
fn check_slice_validity(slice: &str) -> bool {
for c in slice.bytes() {
match c {
b'\x21' | b'\x23' ... b'\x7e' | b'\x80' ... b'\xff' => (),
_ => { return false; }
}
}
true
}


from_one_raw_str(raw).and_then(|s: String| {
let length: usize = s.len();
let slice = &s[];

// Early exits:
// 1. The string is empty, or,
// 2. it doesn't terminate in a DQUOTE.
if slice.is_empty() || !slice.ends_with("\"") {
return None;
}

// The etag is weak if its first char is not a DQUOTE.
if slice.char_at(0) == '"' {
// No need to check if the last char is a DQUOTE,
// we already did that above.
if check_slice_validity(slice.slice_chars(1, length-1)) {
return Some(Etag {
weak: false,
tag: slice.slice_chars(1, length-1).to_string()
});
} else {
return None;
}
}

if slice.slice_chars(0, 3) == "W/\"" {
if check_slice_validity(slice.slice_chars(3, length-1)) {
return Some(Etag {
weak: true,
tag: slice.slice_chars(3, length-1).to_string()
});
} else {
return None;
}
}

None
s.parse::<EntityTag>().and_then(|x| Ok(Etag(x))).ok()
})
}
}

impl HeaderFormat for Etag {
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.weak {
if self.0.weak {
try!(fmt.write_str("W/"));
}
write!(fmt, "\"{}\"", self.tag)
write!(fmt, "\"{}\"", self.0.tag)
}
}

#[cfg(test)]
mod tests {
use super::Etag;
use header::Header;
use header::{Header,EntityTag};

#[test]
fn test_etag_successes() {
// Expected successes
let mut etag: Option<Etag>;

etag = Header::parse_header([b"\"foobar\"".to_vec()].as_slice());
assert_eq!(etag, Some(Etag {
assert_eq!(etag, Some(Etag(EntityTag{
weak: false,
tag: "foobar".to_string()
}));
})));

etag = Header::parse_header([b"\"\"".to_vec()].as_slice());
assert_eq!(etag, Some(Etag {
assert_eq!(etag, Some(Etag(EntityTag{
weak: false,
tag: "".to_string()
}));
})));

etag = Header::parse_header([b"W/\"weak-etag\"".to_vec()].as_slice());
assert_eq!(etag, Some(Etag {
assert_eq!(etag, Some(Etag(EntityTag{
weak: true,
tag: "weak-etag".to_string()
}));
})));

etag = Header::parse_header([b"W/\"\x65\x62\"".to_vec()].as_slice());
assert_eq!(etag, Some(Etag {
assert_eq!(etag, Some(Etag(EntityTag{
weak: true,
tag: "\u{0065}\u{0062}".to_string()
}));
})));

etag = Header::parse_header([b"W/\"\"".to_vec()].as_slice());
assert_eq!(etag, Some(Etag {
assert_eq!(etag, Some(Etag(EntityTag{
weak: true,
tag: "".to_string()
}));
})));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use unicase::UniCase;

use {http, HttpResult, HttpError};

pub use self::shared::{Encoding, QualityItem, qitem};
pub use self::shared::{Encoding, EntityTag, QualityItem, qitem};
pub use self::common::*;

mod common;
Expand Down
145 changes: 145 additions & 0 deletions src/header/shared/entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use std::str::FromStr;
use std::fmt::{self, Display};

/// An entity tag
///
/// An Etag consists of a string enclosed by two literal double quotes.
/// Preceding the first double quote is an optional weakness indicator,
/// which always looks like this: W/
/// See also: https://tools.ietf.org/html/rfc7232#section-2.3
#[derive(Clone, PartialEq, Debug)]
pub struct EntityTag {
/// Weakness indicator for the tag
pub weak: bool,
/// The opaque string in between the DQUOTEs
pub tag: String
}

impl Display for EntityTag {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.weak {
try!(write!(fmt, "{}", "W/"));
}
write!(fmt, "{}", self.tag)
}
}

// check that each char in the slice is either:
// 1. %x21, or
// 2. in the range %x23 to %x7E, or
// 3. in the range %x80 to %xFF
fn check_slice_validity(slice: &str) -> bool {
for c in slice.bytes() {
match c {
b'\x21' | b'\x23' ... b'\x7e' | b'\x80' ... b'\xff' => (),
_ => { return false; }
}
}
true
}

impl FromStr for EntityTag {
type Err = ();
fn from_str(s: &str) -> Result<EntityTag, ()> {
let length: usize = s.len();
let slice = &s[];

// Early exits:
// 1. The string is empty, or,
// 2. it doesn't terminate in a DQUOTE.
if slice.is_empty() || !slice.ends_with("\"") {
return Err(());
}

// The etag is weak if its first char is not a DQUOTE.
if slice.char_at(0) == '"' /* '"' */ {
// No need to check if the last char is a DQUOTE,
// we already did that above.
if check_slice_validity(slice.slice_chars(1, length-1)) {
return Ok(EntityTag {
weak: false,
tag: slice.slice_chars(1, length-1).to_string()
});
} else {
return Err(());
}
}

if slice.slice_chars(0, 3) == "W/\"" {
if check_slice_validity(slice.slice_chars(3, length-1)) {
return Ok(EntityTag {
weak: true,
tag: slice.slice_chars(3, length-1).to_string()
});
} else {
return Err(());
}
}

Err(())
}
}


#[cfg(test)]
mod tests {
use super::EntityTag;

#[test]
fn test_etag_successes() {
// Expected successes
let mut etag : EntityTag = "\"foobar\"".parse().unwrap();
assert_eq!(etag, (EntityTag {
weak: false,
tag: "foobar".to_string()
}));

etag = "\"\"".parse().unwrap();
assert_eq!(etag, EntityTag {
weak: false,
tag: "".to_string()
});

etag = "W/\"weak-etag\"".parse().unwrap();
assert_eq!(etag, EntityTag {
weak: true,
tag: "weak-etag".to_string()
});

etag = "W/\"\x65\x62\"".parse().unwrap();
assert_eq!(etag, EntityTag {
weak: true,
tag: "\u{0065}\u{0062}".to_string()
});

etag = "W/\"\"".parse().unwrap();
assert_eq!(etag, EntityTag {
weak: true,
tag: "".to_string()
});
}

#[test]
fn test_etag_failures() {
// Expected failures
let mut etag: Result<EntityTag,()>;

etag = "no-dquotes".parse();
assert_eq!(etag, Err(()));

etag = "w/\"the-first-w-is-case-sensitive\"".parse();
assert_eq!(etag, Err(()));

etag = "".parse();
assert_eq!(etag, Err(()));

etag = "\"unmatched-dquotes1".parse();
assert_eq!(etag, Err(()));

etag = "unmatched-dquotes2\"".parse();
assert_eq!(etag, Err(()));

etag = "matched-\"dquotes\"".parse();
assert_eq!(etag, Err(()));
}
}
2 changes: 2 additions & 0 deletions src/header/shared/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub use self::encoding::Encoding;
pub use self::entity::EntityTag;
pub use self::quality_item::{QualityItem, qitem};

mod encoding;
mod entity;
mod quality_item;

0 comments on commit 28fd5c8

Please sign in to comment.