1use std::path::Path;
6
7use anyhow::bail;
8
9use crate::client::Client;
10use crate::{cachefile, exit};
11
12const CACHE: &str = "server-ids.cache";
13
14pub fn is_id(s: &str) -> bool {
16 let s = s.trim();
17 s.len() == 32 && s.bytes().all(|b| b.is_ascii_hexdigit())
18}
19
20pub fn normal(s: &str) -> String {
22 s.trim().to_ascii_lowercase()
23}
24
25fn load(dir: &Path, origin: &str) -> Option<String> {
26 let text = std::fs::read_to_string(dir.join(CACHE)).ok()?;
27 text.lines().find_map(|l| {
28 let (o, id) = l.split_once('\t')?;
29 (o == origin && is_id(id)).then(|| normal(id))
30 })
31}
32
33fn save(dir: &Path, origin: &str, id: &str) -> anyhow::Result<()> {
34 cachefile::rewrite(dir, CACHE, |text| {
35 let mut out: String = text
36 .lines()
37 .filter(|l| !l.is_empty() && l.split_once('\t').is_none_or(|(o, _)| o != origin))
38 .map(|l| format!("{l}\n"))
39 .collect();
40 out.push_str(&format!("{origin}\t{id}\n"));
41 out
42 })
43}
44
45pub async fn of(client: &Client, config_dir: &Path, fresh: bool) -> anyhow::Result<String> {
48 let origin = client.origin();
49 if !fresh && let Some(id) = load(config_dir, &origin) {
50 return Ok(id);
51 }
52 let older = || format!("the server at {origin} is older than this CLI; update it");
53 let who = match client.whoami().await {
54 Ok(w) => w,
55 Err(e) if exit::is_not_found(&e) => bail!(older()),
56 Err(e) => return Err(e),
57 };
58 let Some(id) = who.server_id.filter(|id| is_id(id)).map(|id| normal(&id)) else {
59 bail!(older());
60 };
61 let _ = save(config_dir, &origin, &id);
62 Ok(id)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 const ID: &str = "3f9c0e7a5b1d4e2f8a6c9b0d1e2f3a4b";
70
71 #[test]
72 fn ids_are_32_hex_in_any_case() {
73 assert!(is_id(ID));
74 assert!(is_id(&format!(" {} ", ID.to_uppercase())));
75 assert_eq!(normal(&format!(" {} ", ID.to_uppercase())), ID);
76 for bad in [
77 &ID[..31],
78 &format!("{ID}0"),
79 "https://a.example.com",
80 &"g".repeat(32),
81 ] {
82 assert!(!is_id(bad), "{bad}");
83 }
84 }
85
86 #[test]
87 fn the_cache_keeps_one_id_per_server() {
88 let d = tempfile::tempdir().unwrap();
89 assert_eq!(load(d.path(), "https://a.example.com"), None);
90 save(d.path(), "https://a.example.com", ID).unwrap();
91 save(d.path(), "https://b.example.com", &"0".repeat(32)).unwrap();
92 save(d.path(), "https://a.example.com", &"1".repeat(32)).unwrap();
93 assert_eq!(
94 load(d.path(), "https://a.example.com"),
95 Some("1".repeat(32))
96 );
97 assert_eq!(
98 load(d.path(), "https://b.example.com"),
99 Some("0".repeat(32))
100 );
101 }
102
103 #[test]
104 fn an_unwritable_cache_does_not_stop_the_lookup() {
105 let d = tempfile::tempdir().unwrap();
106 let file = d.path().join("not-a-folder");
107 std::fs::write(&file, "x").unwrap();
108 assert!(save(&file, "https://a.example.com", ID).is_err());
109 assert_eq!(load(&file, "https://a.example.com"), None);
110 }
111}