Skip to content

Commit c4efa80

Browse files
committed
add etag header
1 parent ae88092 commit c4efa80

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

src/header/common/etag.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use header::{Header, HeaderFormat};
2+
use std::fmt::{mod};
3+
use super::util::from_one_raw_str;
4+
5+
/// The `Etag` header.
6+
///
7+
/// An Etag consists of a string enclosed by two literal double quotes.
8+
/// Preceding the first double quote is an optional weakness indicator,
9+
/// which always looks like this: W/
10+
/// See also: https://tools.ietf.org/html/rfc7232#section-2.3
11+
#[deriving(Clone, PartialEq, Show)]
12+
pub struct Etag {
13+
/// Weakness indicator for the tag
14+
pub weak: bool,
15+
/// The opaque string in between the DQUOTEs
16+
pub tag: String
17+
}
18+
19+
impl Header for Etag {
20+
fn header_name(_: Option<Etag>) -> &'static str {
21+
"Etag"
22+
}
23+
24+
fn parse_header(raw: &[Vec<u8>]) -> Option<Etag> {
25+
// check that each char in the slice is either:
26+
// 1. %x21, or
27+
// 2. in the range %x23 to %x7E, or
28+
// 3. in the range %x80 to %xFF
29+
fn check_slice_validity(slice: &str) -> bool {
30+
for c in slice.bytes() {
31+
match c {
32+
b'\x21' | b'\x23' ... b'\x7e' | b'\x80' ... b'\xff' => (),
33+
_ => { return false; }
34+
}
35+
}
36+
true
37+
}
38+
39+
40+
from_one_raw_str(raw).and_then(|s: String| {
41+
let length: uint = s.len();
42+
let slice = s[];
43+
44+
// Early exits:
45+
// 1. The string is empty, or,
46+
// 2. it doesn't terminate in a DQUOTE.
47+
if slice.is_empty() || !slice.ends_with("\"") {
48+
return None;
49+
}
50+
51+
// The etag is weak if its first char is not a DQUOTE.
52+
if slice.char_at(0) == '"' {
53+
// No need to check if the last char is a DQUOTE,
54+
// we already did that above.
55+
if check_slice_validity(slice.slice_chars(1, length-1)) {
56+
return Some(Etag {
57+
weak: false,
58+
tag: slice.slice_chars(1, length-1).into_string()
59+
});
60+
} else {
61+
return None;
62+
}
63+
}
64+
65+
if slice.slice_chars(0, 3) == "W/\"" {
66+
if check_slice_validity(slice.slice_chars(3, length-1)) {
67+
return Some(Etag {
68+
weak: true,
69+
tag: slice.slice_chars(3, length-1).into_string()
70+
});
71+
} else {
72+
return None;
73+
}
74+
}
75+
76+
None
77+
})
78+
}
79+
}
80+
81+
impl HeaderFormat for Etag {
82+
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
83+
if self.weak {
84+
try!(fmt.write(b"W/"));
85+
}
86+
write!(fmt, "\"{}\"", self.tag)
87+
}
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::Etag;
93+
use header::Header;
94+
95+
#[test]
96+
fn test_etag_successes() {
97+
// Expected successes
98+
let mut etag: Option<Etag>;
99+
100+
etag = Header::parse_header([b"\"foobar\"".to_vec()].as_slice());
101+
assert_eq!(etag, Some(Etag {
102+
weak: false,
103+
tag: "foobar".into_string()
104+
}));
105+
106+
etag = Header::parse_header([b"\"\"".to_vec()].as_slice());
107+
assert_eq!(etag, Some(Etag {
108+
weak: false,
109+
tag: "".into_string()
110+
}));
111+
112+
etag = Header::parse_header([b"W/\"weak-etag\"".to_vec()].as_slice());
113+
assert_eq!(etag, Some(Etag {
114+
weak: true,
115+
tag: "weak-etag".into_string()
116+
}));
117+
118+
etag = Header::parse_header([b"W/\"\x65\x62\"".to_vec()].as_slice());
119+
assert_eq!(etag, Some(Etag {
120+
weak: true,
121+
tag: "\u0065\u0062".into_string()
122+
}));
123+
124+
etag = Header::parse_header([b"W/\"\"".to_vec()].as_slice());
125+
assert_eq!(etag, Some(Etag {
126+
weak: true,
127+
tag: "".into_string()
128+
}));
129+
}
130+
131+
#[test]
132+
fn test_etag_failures() {
133+
// Expected failures
134+
let mut etag: Option<Etag>;
135+
136+
etag = Header::parse_header([b"no-dquotes".to_vec()].as_slice());
137+
assert_eq!(etag, None);
138+
139+
etag = Header::parse_header([b"w/\"the-first-w-is-case-sensitive\"".to_vec()].as_slice());
140+
assert_eq!(etag, None);
141+
142+
etag = Header::parse_header([b"".to_vec()].as_slice());
143+
assert_eq!(etag, None);
144+
145+
etag = Header::parse_header([b"\"unmatched-dquotes1".to_vec()].as_slice());
146+
assert_eq!(etag, None);
147+
148+
etag = Header::parse_header([b"unmatched-dquotes2\"".to_vec()].as_slice());
149+
assert_eq!(etag, None);
150+
151+
etag = Header::parse_header([b"matched-\"dquotes\"".to_vec()].as_slice());
152+
assert_eq!(etag, None);
153+
}
154+
}
155+
156+
bench_header!(bench, Etag, { vec![b"W/\"nonemptytag\"".to_vec()] })

src/header/common/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub use self::connection::Connection;
1414
pub use self::content_length::ContentLength;
1515
pub use self::content_type::ContentType;
1616
pub use self::date::Date;
17+
pub use self::etag::Etag;
1718
pub use self::expires::Expires;
1819
pub use self::host::Host;
1920
pub use self::last_modified::LastModified;
@@ -93,6 +94,9 @@ pub mod content_type;
9394
/// Exposes the Date header.
9495
pub mod date;
9596

97+
/// Exposes the Etag header.
98+
pub mod etag;
99+
96100
/// Exposes the Expires header.
97101
pub mod expires;
98102

0 commit comments

Comments
 (0)