Skip to content

Commit 37c7b02

Browse files
committed
init
1 parent 7b0a1b3 commit 37c7b02

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/cache/etag.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, ETAG};
2+
use crate::{Error, StatusCode};
3+
4+
use std::fmt::Debug;
5+
use std::option;
6+
7+
/// HTTP Entity Tags.
8+
///
9+
/// ETags provide an ID for a particular resource, enabling clients and servers
10+
/// to reason about caches and make conditional requests.
11+
///
12+
/// # Specifications
13+
///
14+
/// - [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.
18+
Strong(String),
19+
/// An ETag using weak validation.
20+
Weak(String),
21+
}
22+
23+
impl Etag {
24+
/// Create a new Etag that uses strong validation.
25+
pub fn new(s: String) -> Self {
26+
debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars");
27+
Self::Strong(s)
28+
}
29+
30+
/// Create a new Etag that uses weak validation.
31+
pub fn new_weak(s: String) -> Self {
32+
debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars");
33+
Self::Weak(s)
34+
}
35+
36+
/// Create a new instance from headers.
37+
///
38+
/// Only a single ETag per resource is assumed to exist. If multiple ETag
39+
/// headers are found the last one is used.
40+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
41+
let headers = match headers.as_ref().get(ETAG) {
42+
Some(headers) => headers,
43+
None => return Ok(None),
44+
};
45+
46+
// If a header is returned we can assume at least one exists.
47+
let mut s = headers.iter().last().unwrap().as_str();
48+
49+
let weak = if s.starts_with("/W") {
50+
s = &s[2..];
51+
true
52+
} else {
53+
false
54+
};
55+
56+
let s = match s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
57+
Some(s) => s.to_owned(),
58+
None => {
59+
return Err(Error::from_str(
60+
StatusCode::BadRequest,
61+
"Invalid ETag header",
62+
))
63+
}
64+
};
65+
66+
let etag = if weak { Self::Weak(s) } else { Self::Strong(s) };
67+
Ok(Some(etag))
68+
}
69+
70+
/// Sets the `ETag` header.
71+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
72+
headers.as_mut().insert(ETAG, self.value());
73+
}
74+
75+
/// Get the `HeaderName`.
76+
pub fn name(&self) -> HeaderName {
77+
ETAG
78+
}
79+
80+
/// Get the `HeaderValue`.
81+
pub fn value(&self) -> HeaderValue {
82+
let s = match self {
83+
Self::Strong(s) => format!(r#""{}""#, s),
84+
Self::Weak(s) => format!(r#"W/"{}""#, s),
85+
};
86+
// SAFETY: the internal string is validated to be ASCII.
87+
unsafe { HeaderValue::from_bytes_unchecked(s.into()) }
88+
}
89+
}
90+
91+
impl ToHeaderValues for Etag {
92+
type Iter = option::IntoIter<HeaderValue>;
93+
fn to_header_values(&self) -> crate::Result<Self::Iter> {
94+
// A HeaderValue will always convert into itself.
95+
Ok(self.value().to_header_values().unwrap())
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
mod test {
101+
use super::*;
102+
use crate::headers::{Headers, CACHE_CONTROL};
103+
104+
#[test]
105+
fn smoke() -> crate::Result<()> {
106+
let mut etag = Etag::new("0xcafebeef");
107+
108+
let mut headers = Headers::new();
109+
entries.apply(&mut headers);
110+
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);
115+
Ok(())
116+
}
117+
118+
#[test]
119+
fn ignore_unkonwn_directives() -> crate::Result<()> {
120+
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());
125+
Ok(())
126+
}
127+
128+
#[test]
129+
fn bad_request_on_parse_error() -> crate::Result<()> {
130+
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();
133+
assert_eq!(err.status(), 400);
134+
Ok(())
135+
}
136+
}

src/cache/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
1111
1212
mod cache_control;
13+
mod etag;
1314

1415
pub use cache_control::CacheControl;
1516
pub use cache_control::CacheDirective;
17+
pub use etag::Etag;

0 commit comments

Comments
 (0)