Skip to content

Commit 4272252

Browse files
committed
Finalize etags
1 parent 37c7b02 commit 4272252

File tree

2 files changed

+75
-28
lines changed

2 files changed

+75
-28
lines changed

src/cache/etag.rs

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,41 @@ use std::option;
1212
/// # Specifications
1313
///
1414
/// - [RFC 7232 HTTP/1.1: Conditional Requests](https://tools.ietf.org/html/rfc7232#section-2.3)
15-
#[derive(Debug)]
16-
pub enum Etag {
17-
/// An Etag using strong validation.
15+
///
16+
/// # Examples
17+
///
18+
/// ```
19+
/// # fn main() -> http_types::Result<()> {
20+
/// #
21+
/// use http_types::Response;
22+
/// use http_types::cache::ETag;
23+
///
24+
/// let etag = ETag::new("0xcafebeef".to_string());
25+
///
26+
/// let mut res = Response::new(200);
27+
/// etag.apply(&mut res);
28+
///
29+
/// let etag = ETag::from_headers(res)?.unwrap();
30+
/// assert_eq!(etag, ETag::Strong(String::from("0xcafebeef")));
31+
/// #
32+
/// # Ok(()) }
33+
/// ```
34+
#[derive(Debug, Clone, Eq, PartialEq)]
35+
pub enum ETag {
36+
/// An ETag using strong validation.
1837
Strong(String),
1938
/// An ETag using weak validation.
2039
Weak(String),
2140
}
2241

23-
impl Etag {
24-
/// Create a new Etag that uses strong validation.
42+
impl ETag {
43+
/// Create a new ETag that uses strong validation.
2544
pub fn new(s: String) -> Self {
2645
debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars");
2746
Self::Strong(s)
2847
}
2948

30-
/// Create a new Etag that uses weak validation.
49+
/// Create a new ETag that uses weak validation.
3150
pub fn new_weak(s: String) -> Self {
3251
debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars");
3352
Self::Weak(s)
@@ -44,13 +63,15 @@ impl Etag {
4463
};
4564

4665
// If a header is returned we can assume at least one exists.
47-
let mut s = headers.iter().last().unwrap().as_str();
66+
let s = headers.iter().last().unwrap().as_str();
4867

49-
let weak = if s.starts_with("/W") {
50-
s = &s[2..];
51-
true
52-
} else {
53-
false
68+
let mut weak = false;
69+
let s = match s.strip_prefix("W/") {
70+
Some(s) => {
71+
weak = true;
72+
s
73+
}
74+
None => s,
5475
};
5576

5677
let s = match s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
@@ -86,9 +107,19 @@ impl Etag {
86107
// SAFETY: the internal string is validated to be ASCII.
87108
unsafe { HeaderValue::from_bytes_unchecked(s.into()) }
88109
}
110+
111+
/// Returns `true` if the ETag is a `Strong` value.
112+
pub fn is_strong(&self) -> bool {
113+
matches!(self, Self::Strong(_))
114+
}
115+
116+
/// Returns `true` if the ETag is a `Weak` value.
117+
pub fn is_weak(&self) -> bool {
118+
matches!(self, Self::Weak(_))
119+
}
89120
}
90121

91-
impl ToHeaderValues for Etag {
122+
impl ToHeaderValues for ETag {
92123
type Iter = option::IntoIter<HeaderValue>;
93124
fn to_header_values(&self) -> crate::Result<Self::Iter> {
94125
// A HeaderValue will always convert into itself.
@@ -99,38 +130,54 @@ impl ToHeaderValues for Etag {
99130
#[cfg(test)]
100131
mod test {
101132
use super::*;
102-
use crate::headers::{Headers, CACHE_CONTROL};
133+
use crate::headers::Headers;
103134

104135
#[test]
105136
fn smoke() -> crate::Result<()> {
106-
let mut etag = Etag::new("0xcafebeef");
137+
let etag = ETag::new("0xcafebeef".to_string());
107138

108139
let mut headers = Headers::new();
109-
entries.apply(&mut headers);
140+
etag.apply(&mut headers);
110141

111-
let entries = Etag::from_headers(headers)?.unwrap();
112-
let mut entries = entries.iter();
113-
assert_eq!(entries.next().unwrap(), &CacheDirective::Immutable);
114-
assert_eq!(entries.next().unwrap(), &CacheDirective::NoStore);
142+
let etag = ETag::from_headers(headers)?.unwrap();
143+
assert_eq!(etag, ETag::Strong(String::from("0xcafebeef")));
115144
Ok(())
116145
}
117146

118147
#[test]
119-
fn ignore_unkonwn_directives() -> crate::Result<()> {
148+
fn smoke_weak() -> crate::Result<()> {
149+
let etag = ETag::new_weak("0xcafebeef".to_string());
150+
120151
let mut headers = Headers::new();
121-
headers.insert(CACHE_CONTROL, "barrel_roll");
122-
let entries = Etag::from_headers(headers)?.unwrap();
123-
let mut entries = entries.iter();
124-
assert!(entries.next().is_none());
152+
etag.apply(&mut headers);
153+
154+
let etag = ETag::from_headers(headers)?.unwrap();
155+
assert_eq!(etag, ETag::Weak(String::from("0xcafebeef")));
125156
Ok(())
126157
}
127158

128159
#[test]
129160
fn bad_request_on_parse_error() -> crate::Result<()> {
130161
let mut headers = Headers::new();
131-
headers.insert(CACHE_CONTROL, "min-fresh=0.9"); // floats are not supported
132-
let err = Etag::from_headers(headers).unwrap_err();
162+
headers.insert(ETAG, "<nori ate the tag. yum.>");
163+
let err = ETag::from_headers(headers).unwrap_err();
133164
assert_eq!(err.status(), 400);
134165
Ok(())
135166
}
167+
168+
#[test]
169+
fn validate_quotes() -> crate::Result<()> {
170+
assert_entry_err(r#""hello"#, "Invalid ETag header");
171+
assert_entry_err(r#"hello""#, "Invalid ETag header");
172+
assert_entry_err(r#"/O"valid content""#, "Invalid ETag header");
173+
assert_entry_err(r#"/Wvalid content""#, "Invalid ETag header");
174+
Ok(())
175+
}
176+
177+
fn assert_entry_err(s: &str, msg: &str) {
178+
let mut headers = Headers::new();
179+
headers.insert(ETAG, s);
180+
let err = ETag::from_headers(headers).unwrap_err();
181+
assert_eq!(format!("{}", err), msg);
182+
}
136183
}

src/cache/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ mod etag;
1414

1515
pub use cache_control::CacheControl;
1616
pub use cache_control::CacheDirective;
17-
pub use etag::Etag;
17+
pub use etag::ETag;

0 commit comments

Comments
 (0)