1use std::collections::HashMap;
37use std::fs::File;
38use std::io::Write as IoWrite;
39use std::path::{Path, PathBuf};
40use std::sync::OnceLock;
41use std::time::{SystemTime, UNIX_EPOCH};
42
43use memmap2::Mmap;
44use parking_lot::Mutex;
45use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
46use std::os::unix::fs::MetadataExt;
47
48pub const SHARD_MAGIC: u32 = 0x5A52414C;
50pub const SHARD_FORMAT_VERSION: u32 = 1;
52#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
54#[archive(check_bytes)]
55pub struct ShardHeader {
56 pub magic: u32,
58 pub format_version: u32,
60 pub zshrs_version: String,
62 pub pointer_width: u32,
64 pub built_at_secs: u64,
66}
67#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
69#[archive(check_bytes)]
70pub struct AutoloadEntry {
71 pub binary_mtime_at_cache: i64,
73 pub cached_at_secs: i64,
75 pub chunk_blob: Vec<u8>,
77}
78#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
80#[archive(check_bytes)]
81pub struct AutoloadShard {
82 pub header: ShardHeader,
84 pub entries: HashMap<String, AutoloadEntry>,
86}
87pub struct MmappedShard {
89 _mmap: Mmap,
91 archived: *const ArchivedAutoloadShard,
93}
94
95unsafe impl Send for MmappedShard {}
96unsafe impl Sync for MmappedShard {}
97
98impl MmappedShard {
99 pub fn open(path: &Path) -> Option<Self> {
101 let file = File::open(path).ok()?;
102 let mmap = unsafe { Mmap::map(&file).ok()? };
103 let archived = rkyv::check_archived_root::<AutoloadShard>(&mmap[..]).ok()?;
104 let archived_ptr = archived as *const ArchivedAutoloadShard;
105 Some(Self {
106 _mmap: mmap,
107 archived: archived_ptr,
108 })
109 }
110
111 fn shard(&self) -> &ArchivedAutoloadShard {
112 unsafe { &*self.archived }
113 }
114
115 fn header_ok(&self) -> bool {
116 let h = &self.shard().header;
117 let magic: u32 = h.magic.into();
118 let fv: u32 = h.format_version.into();
119 let pw: u32 = h.pointer_width.into();
120 magic == SHARD_MAGIC
121 && fv == SHARD_FORMAT_VERSION
122 && pw as usize == std::mem::size_of::<usize>()
123 && h.zshrs_version.as_str() == env!("CARGO_PKG_VERSION")
124 }
125
126 fn lookup(&self, name: &str) -> Option<&ArchivedAutoloadEntry> {
127 self.shard().entries.get(name)
128 }
129}
130pub struct AutoloadCache {
132 path: PathBuf,
134 lock_path: PathBuf,
136 mmap: Mutex<Option<MmappedShard>>,
138}
139
140impl AutoloadCache {
141 pub fn open(path: &Path) -> std::io::Result<Self> {
143 if let Some(parent) = path.parent() {
144 std::fs::create_dir_all(parent)?;
145 }
146 let parent = path.parent().unwrap_or_else(|| Path::new("/tmp"));
147 let lock_path = parent.join(format!(
148 "{}.lock",
149 path.file_name()
150 .and_then(|s| s.to_str())
151 .unwrap_or("autoloads.rkyv")
152 ));
153 Ok(Self {
154 path: path.to_path_buf(),
155 lock_path,
156 mmap: Mutex::new(None),
157 })
158 }
159
160 fn ensure_mmap(&self) {
161 let mut guard = self.mmap.lock();
162 if guard.is_none() {
163 *guard = MmappedShard::open(&self.path);
164 }
165 }
166
167 fn invalidate_mmap(&self) {
168 let mut guard = self.mmap.lock();
169 *guard = None;
170 }
171 pub fn get(&self, name: &str) -> Option<Vec<u8>> {
173 self.ensure_mmap();
174 let guard = self.mmap.lock();
175 let shard = guard.as_ref()?;
176 if !shard.header_ok() {
177 return None;
178 }
179 let entry = shard.lookup(name)?;
180 if let Some(bin_mtime) = current_binary_mtime_secs() {
181 let cached_bin_mtime: i64 = entry.binary_mtime_at_cache.into();
182 if cached_bin_mtime < bin_mtime {
183 return None;
184 }
185 }
186 Some(entry.chunk_blob.as_slice().to_vec())
187 }
188
189 pub fn put_one(&self, name: &str, chunk_blob: Vec<u8>) -> Result<(), String> {
193 let _lock = match acquire_lock(&self.lock_path) {
194 Some(l) => l,
195 None => return Ok(()),
196 };
197 let mut shard = match read_owned_shard(&self.path) {
198 Some(s)
199 if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
200 && s.header.pointer_width as usize == std::mem::size_of::<usize>()
201 && s.header.format_version == SHARD_FORMAT_VERSION =>
202 {
203 s
204 }
205 _ => fresh_shard(),
206 };
207 let bin_mtime = current_binary_mtime_secs().unwrap_or(0);
208 shard.entries.insert(
209 name.to_string(),
210 AutoloadEntry {
211 binary_mtime_at_cache: bin_mtime,
212 cached_at_secs: now_secs(),
213 chunk_blob,
214 },
215 );
216 shard.header.built_at_secs = now_secs() as u64;
217 write_shard_atomic(&self.path, &shard)?;
218 self.invalidate_mmap();
219 Ok(())
220 }
221
222 pub fn merge_in(&self, entries: HashMap<String, Vec<u8>>) -> Result<(), String> {
229 if entries.is_empty() {
230 return Ok(());
231 }
232 let _lock = match acquire_lock(&self.lock_path) {
233 Some(l) => l,
234 None => return Ok(()),
235 };
236 let mut shard = match read_owned_shard(&self.path) {
237 Some(s)
238 if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
239 && s.header.pointer_width as usize == std::mem::size_of::<usize>()
240 && s.header.format_version == SHARD_FORMAT_VERSION =>
241 {
242 s
243 }
244 _ => fresh_shard(),
245 };
246 let bin_mtime = current_binary_mtime_secs().unwrap_or(0);
247 let now = now_secs();
248 for (name, chunk_blob) in entries {
249 shard.entries.insert(
250 name,
251 AutoloadEntry {
252 binary_mtime_at_cache: bin_mtime,
253 cached_at_secs: now,
254 chunk_blob,
255 },
256 );
257 }
258 shard.header.built_at_secs = now as u64;
259 write_shard_atomic(&self.path, &shard)?;
260 self.invalidate_mmap();
261 Ok(())
262 }
263
264 pub fn replace_all(&self, entries: HashMap<String, Vec<u8>>) -> Result<(), String> {
268 let _lock = match acquire_lock(&self.lock_path) {
269 Some(l) => l,
270 None => return Ok(()),
271 };
272 let bin_mtime = current_binary_mtime_secs().unwrap_or(0);
273 let now = now_secs();
274 let mut shard = fresh_shard();
275 for (name, chunk_blob) in entries {
276 shard.entries.insert(
277 name,
278 AutoloadEntry {
279 binary_mtime_at_cache: bin_mtime,
280 cached_at_secs: now,
281 chunk_blob,
282 },
283 );
284 }
285 write_shard_atomic(&self.path, &shard)?;
286 self.invalidate_mmap();
287 Ok(())
288 }
289 pub fn entry_count(&self) -> usize {
291 self.ensure_mmap();
292 let guard = self.mmap.lock();
293 guard.as_ref().map(|s| s.shard().entries.len()).unwrap_or(0)
294 }
295
296 pub fn cached_names(&self) -> std::collections::HashSet<String> {
300 self.ensure_mmap();
301 let guard = self.mmap.lock();
302 let Some(shard) = guard.as_ref() else {
303 return std::collections::HashSet::new();
304 };
305 shard
306 .shard()
307 .entries
308 .keys()
309 .map(|k| k.as_str().to_string())
310 .collect()
311 }
312 pub fn stats(&self) -> (i64, i64) {
314 self.ensure_mmap();
315 let guard = self.mmap.lock();
316 let Some(shard) = guard.as_ref() else {
317 return (0, 0);
318 };
319 let count = shard.shard().entries.len() as i64;
320 let bytes: i64 = shard
321 .shard()
322 .entries
323 .values()
324 .map(|e| e.chunk_blob.len() as i64)
325 .sum();
326 (count, bytes)
327 }
328 pub fn clear(&self) -> std::io::Result<()> {
330 let _lock = acquire_lock(&self.lock_path);
331 let res = match std::fs::remove_file(&self.path) {
332 Ok(()) => Ok(()),
333 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
334 Err(e) => Err(e),
335 };
336 self.invalidate_mmap();
337 res
338 }
339}
340
341fn acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
342 let f = File::options()
343 .read(true)
344 .write(true)
345 .create(true)
346 .truncate(false)
347 .open(path)
348 .ok()?;
349 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusive).ok()
350}
351
352fn fresh_shard() -> AutoloadShard {
353 AutoloadShard {
354 header: ShardHeader {
355 magic: SHARD_MAGIC,
356 format_version: SHARD_FORMAT_VERSION,
357 zshrs_version: env!("CARGO_PKG_VERSION").to_string(),
358 pointer_width: std::mem::size_of::<usize>() as u32,
359 built_at_secs: now_secs() as u64,
360 },
361 entries: HashMap::new(),
362 }
363}
364
365fn read_owned_shard(path: &Path) -> Option<AutoloadShard> {
366 let bytes = std::fs::read(path).ok()?;
367 let archived = rkyv::check_archived_root::<AutoloadShard>(&bytes[..]).ok()?;
368 archived.deserialize(&mut rkyv::Infallible).ok()
369}
370
371fn write_shard_atomic(path: &Path, shard: &AutoloadShard) -> Result<(), String> {
372 let bytes = rkyv::to_bytes::<_, 4096>(shard).map_err(|e| format!("rkyv serialize: {}", e))?;
373 let parent = path.parent().expect("cache path has parent");
374 let _ = std::fs::create_dir_all(parent);
375 let pid = std::process::id();
376 let nanos = SystemTime::now()
377 .duration_since(UNIX_EPOCH)
378 .map(|d| d.as_nanos())
379 .unwrap_or(0);
380 let tmp_path = parent.join(format!(
381 "{}.tmp.{}.{}",
382 path.file_name()
383 .and_then(|s| s.to_str())
384 .unwrap_or("autoloads.rkyv"),
385 pid,
386 nanos
387 ));
388 {
389 let mut f = File::create(&tmp_path).map_err(|e| e.to_string())?;
390 f.write_all(&bytes).map_err(|e| e.to_string())?;
391 f.sync_all().map_err(|e| e.to_string())?;
392 }
393 std::fs::rename(&tmp_path, path).map_err(|e| e.to_string())?;
394 Ok(())
395}
396
397fn now_secs() -> i64 {
398 SystemTime::now()
399 .duration_since(UNIX_EPOCH)
400 .map(|d| d.as_secs() as i64)
401 .unwrap_or(0)
402}
403
404fn file_mtime(path: &Path) -> Option<(i64, i64)> {
405 let meta = std::fs::metadata(path).ok()?;
406 Some((meta.mtime(), meta.mtime_nsec()))
407}
408
409fn current_binary_mtime_secs() -> Option<i64> {
410 static BIN_MTIME: OnceLock<Option<i64>> = OnceLock::new();
411 *BIN_MTIME.get_or_init(|| {
412 let exe = std::env::current_exe().ok()?;
413 let (secs, _) = file_mtime(&exe)?;
414 Some(secs)
415 })
416}
417pub fn default_cache_path() -> PathBuf {
425 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
426 PathBuf::from(custom)
427 } else {
428 dirs::home_dir()
429 .unwrap_or_else(|| PathBuf::from("/tmp"))
430 .join(".zshrs")
431 };
432 root.join("autoloads.rkyv")
433}
434pub fn cache_enabled() -> bool {
439 if crate::extensions::script_cache::CACHE_DISABLED.load(std::sync::atomic::Ordering::Relaxed) {
440 return false;
441 }
442 !matches!(
443 std::env::var("ZSHRS_CACHE").as_deref(),
444 Ok("0") | Ok("false") | Ok("no")
445 )
446}
447pub static CACHE: once_cell::sync::Lazy<Option<AutoloadCache>> = once_cell::sync::Lazy::new(|| {
449 if !cache_enabled() {
450 return None;
451 }
452 AutoloadCache::open(&default_cache_path()).ok()
453});
454pub fn try_load(name: &str) -> Option<Vec<u8>> {
456 let cache = CACHE.as_ref()?;
457 cache.get(name)
458}
459pub fn try_save_one(name: &str, chunk_blob: &[u8]) -> Result<(), String> {
461 let Some(cache) = CACHE.as_ref() else {
462 return Ok(());
463 };
464 cache.put_one(name, chunk_blob.to_vec())
465}
466
467pub fn try_replace_all(entries: HashMap<String, Vec<u8>>) -> Result<(), String> {
471 let Some(cache) = CACHE.as_ref() else {
472 return Ok(());
473 };
474 cache.replace_all(entries)
475}
476
477pub fn try_merge_in(entries: HashMap<String, Vec<u8>>) -> Result<(), String> {
481 let Some(cache) = CACHE.as_ref() else {
482 return Ok(());
483 };
484 cache.merge_in(entries)
485}
486pub fn cached_names() -> std::collections::HashSet<String> {
488 CACHE.as_ref().map(|c| c.cached_names()).unwrap_or_default()
489}
490pub fn entry_count() -> usize {
492 CACHE.as_ref().map(|c| c.entry_count()).unwrap_or(0)
493}
494pub fn stats() -> Option<(i64, i64)> {
496 CACHE.as_ref().map(|c| c.stats())
497}
498pub fn clear() -> bool {
500 CACHE.as_ref().map(|c| c.clear().is_ok()).unwrap_or(false)
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use tempfile::tempdir;
507
508 #[test]
509 fn round_trip_one() {
510 let _g = crate::test_util::global_state_lock();
511 let dir = tempdir().unwrap();
512 let cache_path = dir.path().join("autoloads.rkyv");
513 let cache = AutoloadCache::open(&cache_path).unwrap();
514 cache.put_one("foo", vec![1, 2, 3]).unwrap();
515 assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
516 assert_eq!(cache.entry_count(), 1);
517 }
518
519 #[test]
520 fn replace_all_overwrites() {
521 let _g = crate::test_util::global_state_lock();
522 let dir = tempdir().unwrap();
523 let cache_path = dir.path().join("autoloads.rkyv");
524 let cache = AutoloadCache::open(&cache_path).unwrap();
525 cache.put_one("a", vec![10]).unwrap();
526 cache.put_one("b", vec![20]).unwrap();
527 assert_eq!(cache.entry_count(), 2);
528
529 let mut new_entries = HashMap::new();
530 new_entries.insert("c".to_string(), vec![30]);
531 new_entries.insert("d".to_string(), vec![40]);
532 cache.replace_all(new_entries).unwrap();
533
534 assert_eq!(cache.entry_count(), 2);
535 assert!(cache.get("a").is_none());
536 assert!(cache.get("b").is_none());
537 assert_eq!(cache.get("c"), Some(vec![30]));
538 assert_eq!(cache.get("d"), Some(vec![40]));
539 }
540
541 #[test]
542 fn cached_names_returns_keys() {
543 let _g = crate::test_util::global_state_lock();
544 let dir = tempdir().unwrap();
545 let cache_path = dir.path().join("autoloads.rkyv");
546 let cache = AutoloadCache::open(&cache_path).unwrap();
547 cache.put_one("alpha", vec![1]).unwrap();
548 cache.put_one("beta", vec![2]).unwrap();
549 let names = cache.cached_names();
550 assert!(names.contains("alpha"));
551 assert!(names.contains("beta"));
552 assert_eq!(names.len(), 2);
553 }
554
555 #[test]
556 fn corrupt_shard_returns_none() {
557 let _g = crate::test_util::global_state_lock();
558 let dir = tempdir().unwrap();
559 let cache_path = dir.path().join("autoloads.rkyv");
560 std::fs::write(&cache_path, b"garbage").unwrap();
561 let cache = AutoloadCache::open(&cache_path).unwrap();
562 assert!(cache.get("anything").is_none());
563 assert_eq!(cache.entry_count(), 0);
564 }
565
566 #[test]
567 fn clear_removes_file() {
568 let _g = crate::test_util::global_state_lock();
569 let dir = tempdir().unwrap();
570 let cache_path = dir.path().join("autoloads.rkyv");
571 let cache = AutoloadCache::open(&cache_path).unwrap();
572 cache.put_one("x", vec![1]).unwrap();
573 assert!(cache_path.exists());
574 cache.clear().unwrap();
575 assert!(!cache_path.exists());
576 }
577}