tower_http/services/fs/serve_dir/
headers.rs1use http::header::HeaderValue;
2use httpdate::HttpDate;
3use std::time::SystemTime;
4
5#[derive(Clone, Debug)]
10pub(super) struct ETag(HeaderValue);
11
12impl ETag {
13 pub(super) fn from_metadata(size: u64, modified: SystemTime) -> Option<Self> {
17 let duration = modified.duration_since(SystemTime::UNIX_EPOCH).ok()?;
18 let value = format!(
21 "\"{:x}.{:08x}-{:x}\"",
22 duration.as_secs(),
23 duration.subsec_nanos(),
24 size
25 );
26 HeaderValue::from_str(&value).ok().map(ETag)
27 }
28
29 pub(super) fn into_header_value(self) -> HeaderValue {
30 self.0
31 }
32
33 fn strong_eq(&self, other: &[u8]) -> bool {
36 if other.starts_with(b"W/") {
37 return false;
38 }
39 self.0.as_bytes() == other
40 }
41
42 fn weak_eq(&self, other: &[u8]) -> bool {
45 let this = self.0.as_bytes();
46 let other = other.strip_prefix(b"W/").unwrap_or(other);
47 let this = this.strip_prefix(b"W/").unwrap_or(this);
48 this == other
49 }
50}
51
52pub(super) struct IfNoneMatch(HeaderValue);
54
55impl IfNoneMatch {
56 pub(super) fn from_header_value(value: &HeaderValue) -> Option<Self> {
57 if value.as_bytes().is_empty() {
59 return None;
60 }
61 Some(IfNoneMatch(value.clone()))
62 }
63
64 pub(super) fn precondition_passes(&self, etag: &ETag) -> bool {
69 let bytes = self.0.as_bytes();
70 if bytes == b"*" {
71 return false;
72 }
73 !for_each_etag(bytes, |tag| etag.weak_eq(tag))
74 }
75}
76
77pub(super) struct IfMatch(HeaderValue);
79
80impl IfMatch {
81 pub(super) fn from_header_value(value: &HeaderValue) -> Option<Self> {
82 if value.as_bytes().is_empty() {
83 return None;
84 }
85 Some(IfMatch(value.clone()))
86 }
87
88 pub(super) fn precondition_passes(&self, etag: &ETag) -> bool {
93 let bytes = self.0.as_bytes();
94 if bytes == b"*" {
95 return true;
96 }
97 for_each_etag(bytes, |tag| etag.strong_eq(tag))
98 }
99}
100
101fn for_each_etag(header: &[u8], mut predicate: impl FnMut(&[u8]) -> bool) -> bool {
106 let mut start = 0;
107 let mut in_quotes = false;
108 for i in 0..header.len() {
109 match header[i] {
110 b'"' => in_quotes = !in_quotes,
111 b',' if !in_quotes => {
112 let trimmed = trim_ows(&header[start..i]);
113 if !trimmed.is_empty() && predicate(trimmed) {
114 return true;
115 }
116 start = i + 1;
117 }
118 _ => {}
119 }
120 }
121 let trimmed = trim_ows(&header[start..]);
122 if !trimmed.is_empty() && predicate(trimmed) {
123 return true;
124 }
125 false
126}
127
128fn trim_ows(bytes: &[u8]) -> &[u8] {
130 let start = bytes
131 .iter()
132 .position(|&b| b != b' ' && b != b'\t')
133 .unwrap_or(bytes.len());
134 let end = bytes
135 .iter()
136 .rposition(|&b| b != b' ' && b != b'\t')
137 .map(|i| i + 1)
138 .unwrap_or(0);
139 if start >= end {
140 &[]
141 } else {
142 &bytes[start..end]
143 }
144}
145
146pub(super) struct LastModified(pub(super) HttpDate);
147
148impl From<SystemTime> for LastModified {
149 fn from(time: SystemTime) -> Self {
150 LastModified(time.into())
151 }
152}
153
154pub(super) struct IfModifiedSince(HttpDate);
155
156impl IfModifiedSince {
157 pub(super) fn is_modified(&self, last_modified: &LastModified) -> bool {
159 self.0 < last_modified.0
160 }
161
162 pub(super) fn from_header_value(value: &HeaderValue) -> Option<IfModifiedSince> {
164 std::str::from_utf8(value.as_bytes())
165 .ok()
166 .and_then(|value| httpdate::parse_http_date(value).ok())
167 .map(|time| IfModifiedSince(time.into()))
168 }
169}
170
171pub(super) struct IfUnmodifiedSince(HttpDate);
172
173impl IfUnmodifiedSince {
174 pub(super) fn precondition_passes(&self, last_modified: &LastModified) -> bool {
176 self.0 >= last_modified.0
177 }
178
179 pub(super) fn from_header_value(value: &HeaderValue) -> Option<IfUnmodifiedSince> {
181 std::str::from_utf8(value.as_bytes())
182 .ok()
183 .and_then(|value| httpdate::parse_http_date(value).ok())
184 .map(|time| IfUnmodifiedSince(time.into()))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn collect_etags(header: &[u8]) -> Vec<Vec<u8>> {
194 let mut tags = Vec::new();
195 for_each_etag(header, |tag| {
196 tags.push(tag.to_vec());
197 false });
199 tags
200 }
201
202 #[test]
203 fn for_each_etag_simple_list() {
204 let tags = collect_etags(b"\"foo\", \"bar\", \"baz\"");
205 assert_eq!(
206 tags,
207 vec![
208 b"\"foo\"".to_vec(),
209 b"\"bar\"".to_vec(),
210 b"\"baz\"".to_vec()
211 ]
212 );
213 }
214
215 #[test]
216 fn for_each_etag_comma_inside_quotes() {
217 let tags = collect_etags(b"\"foo,bar\", \"baz\"");
219 assert_eq!(tags, vec![b"\"foo,bar\"".to_vec(), b"\"baz\"".to_vec()]);
220 }
221
222 #[test]
223 fn for_each_etag_multiple_commas_inside_quotes() {
224 let tags = collect_etags(b"\"a,b,c\", \"d\"");
225 assert_eq!(tags, vec![b"\"a,b,c\"".to_vec(), b"\"d\"".to_vec()]);
226 }
227
228 #[test]
229 fn for_each_etag_weak_with_comma_inside() {
230 let tags = collect_etags(b"W/\"foo,bar\", \"baz\"");
231 assert_eq!(tags, vec![b"W/\"foo,bar\"".to_vec(), b"\"baz\"".to_vec()]);
232 }
233
234 #[test]
235 fn for_each_etag_single_tag() {
236 let tags = collect_etags(b"\"only\"");
237 assert_eq!(tags, vec![b"\"only\"".to_vec()]);
238 }
239
240 #[test]
241 fn for_each_etag_empty() {
242 let tags = collect_etags(b"");
243 assert!(tags.is_empty());
244 }
245
246 #[test]
247 fn for_each_etag_whitespace_only() {
248 let tags = collect_etags(b" , , ");
249 assert!(tags.is_empty());
250 }
251
252 #[test]
253 fn for_each_etag_short_circuits() {
254 let mut count = 0;
255 let found = for_each_etag(b"\"a\", \"b\", \"c\"", |_tag| {
256 count += 1;
257 count == 2 });
259 assert!(found);
260 assert_eq!(count, 2);
261 }
262}