flq 1.0.0

fetch the web as lean, token-efficient text
Documentation
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 {
	/* FNV-1a 64bit — no external dep needed */
	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())
}

/// returns cached body regardless of ttl (for --diff)
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"));
	}

	// Single test owns XDG_CACHE_HOME so the env write can't race other tests.
	#[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";

		// nothing cached yet
		assert_eq!(get(url, 3600), None);

		// fresh write is readable within ttl; body may contain newlines
		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"));

		// deterministically-stale entry: old timestamp, small ttl -> expired
		let old = now().saturating_sub(1000);
		fs::write(path(url), format!("{old}\nstale body")).unwrap();
		assert_eq!(get(url, 10), None); // expired
		assert_eq!(get_stale(url).as_deref(), Some("stale body")); // ttl ignored

		// missing url -> None
		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);
	}
}