v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
Vendored
+232
@@ -0,0 +1,232 @@
|
||||
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 {
|
||||
// Freshness and storage retention are deliberately separate. Expiry only
|
||||
// changes whether an object can be served without validation; it must not
|
||||
// physically evict the representation because a later request may carry
|
||||
// Cache-Control: max-stale. Capacity/LRU and Cold Cache GC remain the
|
||||
// mechanisms that decide how long an otherwise valid object is retained.
|
||||
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);
|
||||
if stale_while_revalidate > 0 && stale_for <= stale_while_revalidate {
|
||||
return Some(true);
|
||||
}
|
||||
match client_max_stale(request_headers) {
|
||||
Some(None) => Some(false),
|
||||
Some(Some(limit)) if stale_for <= limit => Some(false),
|
||||
_ => 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)
|
||||
}
|
||||
Reference in new issue
Block a user