use std::path::Path;
use anyhow::bail;
use crate::client::Client;
use crate::{cachefile, exit};
const CACHE: &str = "server-ids.cache";
pub fn is_id(s: &str) -> bool {
let s = s.trim();
s.len() == 32 && s.bytes().all(|b| b.is_ascii_hexdigit())
}
pub fn normal(s: &str) -> String {
s.trim().to_ascii_lowercase()
}
fn load(dir: &Path, origin: &str) -> Option<String> {
let text = std::fs::read_to_string(dir.join(CACHE)).ok()?;
text.lines().find_map(|l| {
let (o, id) = l.split_once('\t')?;
(o == origin && is_id(id)).then(|| normal(id))
})
}
fn save(dir: &Path, origin: &str, id: &str) -> anyhow::Result<()> {
cachefile::rewrite(dir, CACHE, |text| {
let mut out: String = text
.lines()
.filter(|l| !l.is_empty() && l.split_once('\t').is_none_or(|(o, _)| o != origin))
.map(|l| format!("{l}\n"))
.collect();
out.push_str(&format!("{origin}\t{id}\n"));
out
})
}
pub async fn of(client: &Client, config_dir: &Path, fresh: bool) -> anyhow::Result<String> {
let origin = client.origin();
if !fresh && let Some(id) = load(config_dir, &origin) {
return Ok(id);
}
let older = || format!("the server at {origin} is older than this CLI; update it");
let who = match client.whoami().await {
Ok(w) => w,
Err(e) if exit::is_not_found(&e) => bail!(older()),
Err(e) => return Err(e),
};
let Some(id) = who.server_id.filter(|id| is_id(id)).map(|id| normal(&id)) else {
bail!(older());
};
let _ = save(config_dir, &origin, &id);
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
const ID: &str = "3f9c0e7a5b1d4e2f8a6c9b0d1e2f3a4b";
#[test]
fn ids_are_32_hex_in_any_case() {
assert!(is_id(ID));
assert!(is_id(&format!(" {} ", ID.to_uppercase())));
assert_eq!(normal(&format!(" {} ", ID.to_uppercase())), ID);
for bad in [
&ID[..31],
&format!("{ID}0"),
"https://a.example.com",
&"g".repeat(32),
] {
assert!(!is_id(bad), "{bad}");
}
}
#[test]
fn the_cache_keeps_one_id_per_server() {
let d = tempfile::tempdir().unwrap();
assert_eq!(load(d.path(), "https://a.example.com"), None);
save(d.path(), "https://a.example.com", ID).unwrap();
save(d.path(), "https://b.example.com", &"0".repeat(32)).unwrap();
save(d.path(), "https://a.example.com", &"1".repeat(32)).unwrap();
assert_eq!(
load(d.path(), "https://a.example.com"),
Some("1".repeat(32))
);
assert_eq!(
load(d.path(), "https://b.example.com"),
Some("0".repeat(32))
);
}
#[test]
fn an_unwritable_cache_does_not_stop_the_lookup() {
let d = tempfile::tempdir().unwrap();
let file = d.path().join("not-a-folder");
std::fs::write(&file, "x").unwrap();
assert!(save(&file, "https://a.example.com", ID).is_err());
assert_eq!(load(&file, "https://a.example.com"), None);
}
}