snapper_fmt/parser/pandoc/
cache.rs1use std::collections::HashMap;
14use std::fs;
15use std::path::PathBuf;
16use std::sync::{Arc, Mutex, OnceLock};
17
18use sha2::{Digest, Sha256};
19
20static MEM: OnceLock<Mutex<HashMap<[u8; 32], Arc<str>>>> = OnceLock::new();
21
22fn mem() -> &'static Mutex<HashMap<[u8; 32], Arc<str>>> {
23 MEM.get_or_init(|| Mutex::new(HashMap::new()))
24}
25
26pub fn cache_key(format: &str, input: &str) -> [u8; 32] {
28 let mut h = Sha256::new();
29 h.update(format.as_bytes());
30 h.update([0u8]);
31 h.update(input.as_bytes());
32 h.finalize().into()
33}
34
35fn cache_enabled() -> bool {
36 match std::env::var("SNAPPER_PANDOC_CACHE") {
37 Ok(v) => {
38 let v = v.to_ascii_lowercase();
39 !(v == "0" || v == "off" || v == "false" || v == "no")
40 }
41 Err(_) => true,
42 }
43}
44
45fn disk_dir() -> Option<PathBuf> {
46 if !cache_enabled() {
47 return None;
48 }
49 if let Ok(p) = std::env::var("SNAPPER_PANDOC_CACHE_DIR") {
50 return Some(PathBuf::from(p));
51 }
52 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
54 return Some(PathBuf::from(xdg).join("snapper/pandoc-ast"));
55 }
56 if let Ok(home) = std::env::var("HOME") {
57 return Some(PathBuf::from(home).join(".cache/snapper/pandoc-ast"));
58 }
59 Some(std::env::temp_dir().join("snapper-pandoc-ast"))
60}
61
62fn disk_path(key: &[u8; 32]) -> Option<PathBuf> {
63 let dir = disk_dir()?;
64 let name = key.iter().map(|b| format!("{b:02x}")).collect::<String>();
65 Some(dir.join(name).with_extension("json"))
66}
67
68pub fn get_json(format: &str, input: &str) -> Option<Arc<str>> {
70 if !cache_enabled() {
71 return None;
72 }
73 let key = cache_key(format, input);
74 if let Ok(guard) = mem().lock() {
75 if let Some(v) = guard.get(&key) {
76 return Some(Arc::clone(v));
77 }
78 }
79 let path = disk_path(&key)?;
80 let bytes = fs::read(&path).ok()?;
81 let s = String::from_utf8(bytes).ok()?;
82 let arc: Arc<str> = Arc::from(s);
83 if let Ok(mut guard) = mem().lock() {
84 guard.insert(key, Arc::clone(&arc));
85 }
86 Some(arc)
87}
88
89pub fn put_json(format: &str, input: &str, json: &str) {
91 if !cache_enabled() {
92 return;
93 }
94 let key = cache_key(format, input);
95 let arc: Arc<str> = Arc::from(json);
96 if let Ok(mut guard) = mem().lock() {
97 guard.insert(key, Arc::clone(&arc));
98 }
99 if let Some(path) = disk_path(&key) {
100 if let Some(parent) = path.parent() {
101 let _ = fs::create_dir_all(parent);
102 }
103 let _ = fs::write(path, arc.as_bytes());
104 }
105}
106
107pub fn clear_memory() {
109 if let Ok(mut guard) = mem().lock() {
110 guard.clear();
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn cache_roundtrip_memory() {
120 let fmt = "markdown";
122 let input = format!(
123 "Hello cache unit-{}-{}. Second.",
124 std::process::id(),
125 std::time::SystemTime::now()
126 .duration_since(std::time::UNIX_EPOCH)
127 .map(|d| d.as_nanos())
128 .unwrap_or(0)
129 );
130 let dir = std::env::temp_dir().join(format!("snapper-cache-ut-{}", std::process::id()));
131 let _ = fs::create_dir_all(&dir);
132 unsafe {
134 std::env::set_var("SNAPPER_PANDOC_CACHE", "1");
135 std::env::set_var("SNAPPER_PANDOC_CACHE_DIR", &dir);
136 }
137 clear_memory();
138 assert!(get_json(fmt, &input).is_none());
139 put_json(
140 fmt,
141 &input,
142 r#"{"pandoc-api-version":[1,23,1],"meta":{},"blocks":[]}"#,
143 );
144 let hit = get_json(fmt, &input).expect("memory hit");
145 assert!(hit.contains("pandoc-api-version"));
146 clear_memory();
147 let _ = fs::remove_dir_all(&dir);
148 }
149
150 #[test]
151 fn different_inputs_different_keys() {
152 assert_ne!(cache_key("markdown", "a"), cache_key("markdown", "b"));
153 assert_ne!(cache_key("org", "a"), cache_key("markdown", "a"));
154 }
155}