laterite_admin/
http_cache.rs1use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
19use axum::response::{IntoResponse, Response};
20
21pub const IMMUTABLE: &str = "public, max-age=31536000, immutable";
24
25pub const REVALIDATE: &str = "public, no-cache";
28
29pub const PRIVATE_NO_STORE: &str = "private, no-store";
33
34const DIGEST_LEN: usize = 8;
38
39pub fn digest(bytes: &[u8]) -> String {
41 blake3::hash(bytes).to_hex()[..DIGEST_LEN].to_string()
42}
43
44pub(crate) fn fingerprint(key: &str, digest: &str) -> String {
48 match key.rsplit_once('.') {
49 Some((stem, ext)) => format!("{stem}.{digest}.{ext}"),
50 None => format!("{key}.{digest}"),
51 }
52}
53
54pub(crate) fn strip_fingerprint(path: &str) -> (String, bool) {
60 let Some((rest, ext)) = path.rsplit_once('.') else {
61 return (path.to_string(), false);
62 };
63 match rest.rsplit_once('.') {
64 Some((stem, maybe_digest))
65 if maybe_digest.len() == DIGEST_LEN
66 && maybe_digest.bytes().all(|b| b.is_ascii_hexdigit()) =>
67 {
68 (format!("{stem}.{ext}"), true)
69 }
70 _ => (path.to_string(), false),
71 }
72}
73
74pub fn matches_etag(headers: &HeaderMap, etag: &str) -> bool {
80 let Some(raw) = headers
81 .get(header::IF_NONE_MATCH)
82 .and_then(|v| v.to_str().ok())
83 else {
84 return false;
85 };
86 raw.split(',').any(|candidate| {
87 let candidate = candidate.trim().trim_start_matches("W/").trim_matches('"');
88 candidate == etag || candidate == "*"
89 })
90}
91
92pub fn conditional(
99 headers: &HeaderMap,
100 content_type: &str,
101 cache_control: &str,
102 body: Vec<u8>,
103) -> Response {
104 let tag = digest(&body);
105 let quoted = format!("W/\"{tag}\"");
106 let etag = match HeaderValue::from_str("ed) {
107 Ok(v) => v,
108 Err(_) => return ([(header::CACHE_CONTROL, cache_control)], body).into_response(),
111 };
112 if matches_etag(headers, &tag) {
113 return (
114 StatusCode::NOT_MODIFIED,
115 [
116 (header::ETAG, etag),
117 (
118 header::CACHE_CONTROL,
119 HeaderValue::from_str(cache_control).unwrap(),
120 ),
121 ],
122 )
123 .into_response();
124 }
125 (
126 [
127 (
128 header::CONTENT_TYPE,
129 HeaderValue::from_str(content_type).unwrap(),
130 ),
131 (header::ETAG, etag),
132 (
133 header::CACHE_CONTROL,
134 HeaderValue::from_str(cache_control).unwrap(),
135 ),
136 ],
137 body,
138 )
139 .into_response()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn a_digest_round_trips_through_a_url() {
148 let d = digest(b"body");
149 let url = fingerprint("laterite.css", &d);
150 assert_eq!(url, format!("laterite.{d}.css"));
151 assert_eq!(strip_fingerprint(&url), ("laterite.css".to_string(), true));
152 }
153
154 #[test]
155 fn a_nested_key_keeps_its_directory() {
156 let url = fingerprint("fields/ref-picker.js", "a1b2c3d4");
157 assert_eq!(url, "fields/ref-picker.a1b2c3d4.js");
158 assert_eq!(
159 strip_fingerprint(&url),
160 ("fields/ref-picker.js".to_string(), true)
161 );
162 }
163
164 #[test]
165 fn an_unfingerprinted_path_is_reported_as_such() {
166 assert_eq!(
169 strip_fingerprint("laterite.css"),
170 ("laterite.css".to_string(), false)
171 );
172 assert_eq!(
173 strip_fingerprint("mark.svg"),
174 ("mark.svg".to_string(), false)
175 );
176 }
177
178 #[test]
179 fn a_hex_word_in_a_real_name_is_not_mistaken_for_a_digest() {
180 assert_eq!(
183 strip_fingerprint("vendor.deadbeefcafe.js"),
184 ("vendor.deadbeefcafe.js".to_string(), false)
185 );
186 }
187
188 #[test]
189 fn if_none_match_recognises_the_tag_among_several() {
190 let mut headers = HeaderMap::new();
191 headers.insert(
192 header::IF_NONE_MATCH,
193 HeaderValue::from_static("\"aaaaaaaa\", W/\"a1b2c3d4\""),
194 );
195 assert!(matches_etag(&headers, "a1b2c3d4"));
196 assert!(!matches_etag(&headers, "ffffffff"));
197 }
198
199 #[test]
200 fn an_absent_header_never_matches() {
201 assert!(!matches_etag(&HeaderMap::new(), "a1b2c3d4"));
202 }
203
204 #[test]
205 fn an_unchanged_body_comes_back_bodyless() {
206 let body = b"h1 { color: red }".to_vec();
207 let tag = digest(&body);
208 let mut headers = HeaderMap::new();
209 headers.insert(
210 header::IF_NONE_MATCH,
211 HeaderValue::from_str(&format!("W/\"{tag}\"")).unwrap(),
212 );
213 let resp = conditional(&headers, "text/css", REVALIDATE, body);
214 assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
215 }
216
217 #[test]
218 fn a_changed_body_comes_back_whole() {
219 let mut headers = HeaderMap::new();
220 headers.insert(
221 header::IF_NONE_MATCH,
222 HeaderValue::from_static("W/\"stale\""),
223 );
224 let resp = conditional(&headers, "text/css", REVALIDATE, b"new".to_vec());
225 assert_eq!(resp.status(), StatusCode::OK);
226 assert!(resp.headers().get(header::ETAG).is_some());
227 }
228}