Files
RustProxy/src/cache/model/views.rs
T

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)
}