Skip to content

Commit 6aa6605

Browse files
committed
Merge pull request #296 from hugoduncan/feature/factor-entity-tag
Factor out EntityTag from Etag header
2 parents 93ac2c8 + 28fd5c8 commit 6aa6605

File tree

4 files changed

+166
-71
lines changed

4 files changed

+166
-71
lines changed

src/header/common/etag.rs

+18-70
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use header::{Header, HeaderFormat};
1+
use header::{EntityTag, Header, HeaderFormat};
22
use std::fmt::{self};
33
use header::parsing::from_one_raw_str;
44

@@ -9,123 +9,71 @@ use header::parsing::from_one_raw_str;
99
/// which always looks like this: W/
1010
/// See also: https://tools.ietf.org/html/rfc7232#section-2.3
1111
#[derive(Clone, PartialEq, Debug)]
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-
}
12+
pub struct Etag(pub EntityTag);
13+
14+
deref!(Etag => EntityTag);
1815

1916
impl Header for Etag {
2017
fn header_name() -> &'static str {
2118
"Etag"
2219
}
2320

2421
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-
3922

4023
from_one_raw_str(raw).and_then(|s: String| {
41-
let length: usize = 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).to_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).to_string()
70-
});
71-
} else {
72-
return None;
73-
}
74-
}
75-
76-
None
24+
s.parse::<EntityTag>().and_then(|x| Ok(Etag(x))).ok()
7725
})
7826
}
7927
}
8028

8129
impl HeaderFormat for Etag {
8230
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
83-
if self.weak {
31+
if self.0.weak {
8432
try!(fmt.write_str("W/"));
8533
}
86-
write!(fmt, "\"{}\"", self.tag)
34+
write!(fmt, "\"{}\"", self.0.tag)
8735
}
8836
}
8937

9038
#[cfg(test)]
9139
mod tests {
9240
use super::Etag;
93-
use header::Header;
41+
use header::{Header,EntityTag};
9442

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

10048
etag = Header::parse_header([b"\"foobar\"".to_vec()].as_slice());
101-
assert_eq!(etag, Some(Etag {
49+
assert_eq!(etag, Some(Etag(EntityTag{
10250
weak: false,
10351
tag: "foobar".to_string()
104-
}));
52+
})));
10553

10654
etag = Header::parse_header([b"\"\"".to_vec()].as_slice());
107-
assert_eq!(etag, Some(Etag {
55+
assert_eq!(etag, Some(Etag(EntityTag{
10856
weak: false,
10957
tag: "".to_string()
110-
}));
58+
})));
11159

11260
etag = Header::parse_header([b"W/\"weak-etag\"".to_vec()].as_slice());
113-
assert_eq!(etag, Some(Etag {
61+
assert_eq!(etag, Some(Etag(EntityTag{
11462
weak: true,
11563
tag: "weak-etag".to_string()
116-
}));
64+
})));
11765

11866
etag = Header::parse_header([b"W/\"\x65\x62\"".to_vec()].as_slice());
119-
assert_eq!(etag, Some(Etag {
67+
assert_eq!(etag, Some(Etag(EntityTag{
12068
weak: true,
12169
tag: "\u{0065}\u{0062}".to_string()
122-
}));
70+
})));
12371

12472
etag = Header::parse_header([b"W/\"\"".to_vec()].as_slice());
125-
assert_eq!(etag, Some(Etag {
73+
assert_eq!(etag, Some(Etag(EntityTag{
12674
weak: true,
12775
tag: "".to_string()
128-
}));
76+
})));
12977
}
13078

13179
#[test]

src/header/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use unicase::UniCase;
2222
use self::cell::OptCell;
2323
use {http, HttpResult, HttpError};
2424

25-
pub use self::shared::{Encoding, QualityItem, qitem};
25+
pub use self::shared::{Encoding, EntityTag, QualityItem, qitem};
2626
pub use self::common::*;
2727

2828
mod cell;

src/header/shared/entity.rs

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

src/header/shared/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub use self::encoding::Encoding;
2+
pub use self::entity::EntityTag;
23
pub use self::quality_item::{QualityItem, qitem};
34

45
mod encoding;
6+
mod entity;
57
mod quality_item;

0 commit comments

Comments
 (0)