use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn dir() -> PathBuf {
let base = std::env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".cache")
});
base.join("flq")
}
fn key(url: &str) -> String {
let mut h: u64 = 0xcbf29ce484222325;
for b in url.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
format!("{h:016x}")
}
fn path(url: &str) -> PathBuf {
dir().join(key(url))
}
fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn get(url: &str, ttl: u64) -> Option<String> {
let p = path(url);
let raw = fs::read_to_string(&p).ok()?;
let (ts, body) = raw.split_once('\n')?;
let ts: u64 = ts.parse().ok()?;
if now().saturating_sub(ts) > ttl {
return None;
}
Some(body.to_string())
}
pub fn get_stale(url: &str) -> Option<String> {
let raw = fs::read_to_string(path(url)).ok()?;
raw.split_once('\n').map(|(_, b)| b.to_string())
}
pub fn put(url: &str, body: &str) {
let d = dir();
if fs::create_dir_all(&d).is_err() {
return;
}
let _ = fs::write(path(url), format!("{}\n{}", now(), body));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_is_hex16_and_stable() {
let k = key("https://example.com");
assert_eq!(k.len(), 16);
assert!(k.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(k, key("https://example.com"));
}
#[test]
fn key_differs_by_url() {
assert_ne!(key("https://a.com"), key("https://b.com"));
}
#[test]
fn put_get_stale_and_ttl() {
let tmp = std::env::temp_dir().join("flq-cache-test");
let _ = fs::remove_dir_all(&tmp);
std::env::set_var("XDG_CACHE_HOME", &tmp);
let url = "https://roundtrip.test/page";
assert_eq!(get(url, 3600), None);
put(url, "cached body\nline2");
assert_eq!(get(url, 3600).as_deref(), Some("cached body\nline2"));
assert_eq!(get_stale(url).as_deref(), Some("cached body\nline2"));
let old = now().saturating_sub(1000);
fs::write(path(url), format!("{old}\nstale body")).unwrap();
assert_eq!(get(url, 10), None); assert_eq!(get_stale(url).as_deref(), Some("stale body"));
assert_eq!(get("https://never.cached/x", 3600), None);
assert_eq!(get_stale("https://never.cached/x"), None);
std::env::remove_var("XDG_CACHE_HOME");
let _ = fs::remove_dir_all(&tmp);
}
}