231 lines
6.1 KiB
Rust
231 lines
6.1 KiB
Rust
fn parse_key_hex(value: &str) -> Option<Key> {
|
|
if value.len() != 64 || !value.bytes().all(|b| b.is_ascii_hexdigit()) {
|
|
return None;
|
|
}
|
|
let mut key = [0_u8; 32];
|
|
for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
|
|
let text = std::str::from_utf8(chunk).ok()?;
|
|
key[index] = u8::from_str_radix(text, 16).ok()?;
|
|
}
|
|
Some(key)
|
|
}
|
|
|
|
fn default_cache_scheme() -> String {
|
|
"http".to_owned()
|
|
}
|
|
|
|
fn cache_tags(headers: &HeaderMap) -> Vec<String> {
|
|
let mut tags = Vec::new();
|
|
for value in headers.get_all("cache-tag") {
|
|
let Ok(value) = value.to_str() else {
|
|
continue;
|
|
};
|
|
for raw in value.split(|c: char| c == ',' || c.is_ascii_whitespace()) {
|
|
let tag = raw.trim();
|
|
if tag.is_empty()
|
|
|| tag.len() > 128
|
|
|| !tag.bytes().all(|b| {
|
|
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b':' | b'/')
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
if !tags.iter().any(|existing| existing == tag) {
|
|
tags.push(tag.to_owned());
|
|
if tags.len() >= 64 {
|
|
return tags;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tags
|
|
}
|
|
|
|
fn now_ms() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis()
|
|
.min(u64::MAX as u128) as u64
|
|
}
|
|
|
|
fn system_time_ms(value: u64) -> SystemTime {
|
|
UNIX_EPOCH
|
|
.checked_add(Duration::from_millis(value))
|
|
.unwrap_or(UNIX_EPOCH)
|
|
}
|
|
|
|
fn cold_age(entry: &ColdEntry) -> u64 {
|
|
entry
|
|
.age_at_store_seconds
|
|
.saturating_add(entry.stored_at.elapsed().unwrap_or_default().as_secs())
|
|
}
|
|
|
|
fn cold_resident_age(entry: &ColdEntry) -> u64 {
|
|
entry
|
|
.resident_age_at_store_seconds
|
|
.saturating_add(entry.stored_at.elapsed().unwrap_or_default().as_secs())
|
|
}
|
|
|
|
fn retention_deadline(
|
|
_ttl: u64,
|
|
_stale_while_revalidate: u64,
|
|
_stale_if_error: u64,
|
|
_has_validator: bool,
|
|
) -> u64 {
|
|
u64::MAX
|
|
}
|
|
|
|
fn has_validator(headers: &HeaderMap) -> bool {
|
|
headers.contains_key("etag") || headers.contains_key("last-modified")
|
|
}
|
|
|
|
fn stale_forbidden(must_revalidate: bool, proxy_revalidate: bool) -> bool {
|
|
must_revalidate || proxy_revalidate
|
|
}
|
|
|
|
fn client_max_stale(headers: &HeaderMap) -> Option<Option<u64>> {
|
|
let control = directives(headers)?;
|
|
let value = control.get("max-stale")?;
|
|
match value {
|
|
None => Some(None),
|
|
Some(value) => value.parse::<u64>().ok().map(Some),
|
|
}
|
|
}
|
|
|
|
fn request_freshness_allows(age: u64, ttl: u64, headers: &HeaderMap) -> bool {
|
|
let Some(control) = directives(headers) else {
|
|
return false;
|
|
};
|
|
if let Some(Some(value)) = control.get("max-age") {
|
|
let Ok(limit) = value.parse::<u64>() else {
|
|
return false;
|
|
};
|
|
if age > limit {
|
|
return false;
|
|
}
|
|
}
|
|
if let Some(Some(value)) = control.get("min-fresh") {
|
|
let Ok(required) = value.parse::<u64>() else {
|
|
return false;
|
|
};
|
|
if ttl.saturating_sub(age) < required {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn stale_mode(
|
|
age: u64,
|
|
ttl: u64,
|
|
stale_while_revalidate: u64,
|
|
must_revalidate: bool,
|
|
proxy_revalidate: bool,
|
|
requires_revalidation: bool,
|
|
request_headers: &HeaderMap,
|
|
) -> Option<bool> {
|
|
if age < ttl || requires_revalidation || stale_forbidden(must_revalidate, proxy_revalidate) {
|
|
return None;
|
|
}
|
|
if !request_freshness_allows(age, ttl, request_headers) {
|
|
return None;
|
|
}
|
|
let stale_for = age.saturating_sub(ttl);
|
|
let max_stale = client_max_stale(request_headers);
|
|
|
|
if matches!(max_stale, Some(Some(limit)) if stale_for > limit) {
|
|
return None;
|
|
}
|
|
if stale_while_revalidate > 0 && stale_for <= stale_while_revalidate {
|
|
return Some(true);
|
|
}
|
|
match max_stale {
|
|
Some(None) | Some(Some(_)) => Some(false),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum CacheState {
|
|
Fresh,
|
|
StaleButServable { background_revalidate: bool },
|
|
Expired,
|
|
}
|
|
|
|
impl CacheState {
|
|
fn servable(self) -> bool {
|
|
!matches!(self, Self::Expired)
|
|
}
|
|
|
|
fn stale(self) -> bool {
|
|
matches!(self, Self::StaleButServable { .. })
|
|
}
|
|
|
|
fn background_revalidate(self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::StaleButServable {
|
|
background_revalidate: true
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
fn cache_state(
|
|
age: u64,
|
|
ttl: u64,
|
|
stale_while_revalidate: u64,
|
|
must_revalidate: bool,
|
|
proxy_revalidate: bool,
|
|
requires_revalidation: bool,
|
|
request_headers: &HeaderMap,
|
|
) -> CacheState {
|
|
if age < ttl && !requires_revalidation && request_freshness_allows(age, ttl, request_headers) {
|
|
return CacheState::Fresh;
|
|
}
|
|
match stale_mode(
|
|
age,
|
|
ttl,
|
|
stale_while_revalidate,
|
|
must_revalidate,
|
|
proxy_revalidate,
|
|
requires_revalidation,
|
|
request_headers,
|
|
) {
|
|
Some(background_revalidate) => CacheState::StaleButServable {
|
|
background_revalidate,
|
|
},
|
|
None => CacheState::Expired,
|
|
}
|
|
}
|
|
|
|
fn cold_paths(root: &Path, key: &Key) -> (PathBuf, PathBuf, PathBuf, PathBuf) {
|
|
let key = key_hex(key);
|
|
let shard = root.join(&key[..2]);
|
|
(
|
|
shard.join(format!("{key}.body")),
|
|
shard.join(format!("{key}.json")),
|
|
shard.join(format!(".{key}.body.tmp")),
|
|
shard.join(format!(".{key}.json.tmp")),
|
|
)
|
|
}
|
|
|
|
fn cold_header_pairs(headers: &HeaderMap) -> Option<Vec<(String, String)>> {
|
|
let mut result = Vec::new();
|
|
for (name, value) in headers {
|
|
result.push((name.as_str().to_owned(), value.to_str().ok()?.to_owned()));
|
|
}
|
|
Some(result)
|
|
}
|
|
|
|
fn pairs_to_headers(values: &[(String, String)]) -> Option<HeaderMap> {
|
|
let mut headers = HeaderMap::new();
|
|
for (name, value) in values {
|
|
let name = name.parse::<http::HeaderName>().ok()?;
|
|
let value = HeaderValue::from_str(value).ok()?;
|
|
headers.append(name, value);
|
|
}
|
|
Some(headers)
|
|
}
|