1use std::collections::HashMap;
49use std::fs::File;
50use std::io::Write as IoWrite;
51use std::path::{Path, PathBuf};
52use std::sync::OnceLock;
53use std::time::{SystemTime, UNIX_EPOCH};
54
55use memmap2::Mmap;
56use parking_lot::Mutex;
57use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
58use std::os::unix::fs::MetadataExt;
59
60pub const SHARD_MAGIC: u32 = 0x5A525343;
63pub const SHARD_FORMAT_VERSION: u32 = 5;
71#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
73#[archive(check_bytes)]
74pub struct ShardHeader {
75 pub magic: u32,
77 pub format_version: u32,
79 pub zshrs_version: String,
81 pub pointer_width: u32,
83 pub built_at_secs: u64,
85}
86#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
88#[archive(check_bytes)]
89pub struct ScriptEntry {
90 pub mtime_secs: i64,
92 pub mtime_nsecs: i64,
94 pub binary_mtime_at_cache: i64,
96 pub binary_len_at_cache: u64,
102 pub cached_at_secs: i64,
104 pub env_fingerprint: u64,
109 pub chunk_blob: Vec<u8>,
111}
112#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
114#[archive(check_bytes)]
115pub struct ScriptShard {
116 pub header: ShardHeader,
118 pub entries: HashMap<String, ScriptEntry>,
120}
121
122pub struct MmappedShard {
125 _mmap: Mmap,
127 archived: *const ArchivedScriptShard,
129 identity: (u64, u64, i64),
134}
135
136unsafe impl Send for MmappedShard {}
139unsafe impl Sync for MmappedShard {}
140
141impl MmappedShard {
142 pub fn open(path: &Path) -> Option<Self> {
144 let file = File::open(path).ok()?;
145 let identity = file_identity(&file.metadata().ok()?);
146 let mmap = unsafe { Mmap::map(&file).ok()? };
147 let archived = rkyv::check_archived_root::<ScriptShard>(&mmap[..]).ok()?;
148 let archived_ptr = archived as *const ArchivedScriptShard;
149 Some(Self {
150 _mmap: mmap,
151 archived: archived_ptr,
152 identity,
153 })
154 }
155
156 fn shard(&self) -> &ArchivedScriptShard {
157 unsafe { &*self.archived }
159 }
160
161 fn header_ok(&self) -> bool {
162 let h = &self.shard().header;
163 let magic: u32 = h.magic.into();
164 let fv: u32 = h.format_version.into();
165 let pw: u32 = h.pointer_width.into();
166 magic == SHARD_MAGIC
167 && fv == SHARD_FORMAT_VERSION
168 && pw as usize == std::mem::size_of::<usize>()
169 && h.zshrs_version.as_str() == env!("CARGO_PKG_VERSION")
170 }
171
172 fn lookup(&self, path: &str) -> Option<&ArchivedScriptEntry> {
173 self.shard().entries.get(path)
174 }
175
176 fn entry_count(&self) -> usize {
177 self.shard().entries.len()
178 }
179}
180
181#[derive(Clone)]
183pub struct PendingPut {
184 path: String,
185 mtime_secs: i64,
186 mtime_nsecs: i64,
187 env_fingerprint: u64,
188 blob: Vec<u8>,
190}
191
192static PENDING: Mutex<Vec<PendingPut>> = Mutex::new(Vec::new());
194
195static PENDING_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
199
200static RAN: Mutex<Vec<(String, i64)>> = Mutex::new(Vec::new());
205
206static RAN_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
209
210const RUNS_COMPACT_BYTES: u64 = 64 * 1024;
212
213const PENDING_FLUSH_MAX: usize = 128;
216
217pub struct ScriptCache {
219 path: PathBuf,
221 lock_path: PathBuf,
223 mmap: Mutex<Option<MmappedShard>>,
225}
226
227impl ScriptCache {
228 pub fn open(path: &Path) -> std::io::Result<Self> {
230 if let Some(parent) = path.parent() {
231 std::fs::create_dir_all(parent)?;
232 }
233 let parent = path.parent().unwrap_or_else(|| Path::new("/tmp"));
234 let lock_path = parent.join(format!(
235 "{}.lock",
236 path.file_name()
237 .and_then(|s| s.to_str())
238 .unwrap_or("scripts.rkyv")
239 ));
240 Ok(Self {
241 path: path.to_path_buf(),
242 lock_path,
243 mmap: Mutex::new(None),
244 })
245 }
246
247 fn ensure_mmap(&self) {
248 let mut guard = self.mmap.lock();
249 let on_disk = std::fs::metadata(&self.path).ok().map(|m| file_identity(&m));
254 let stale = match (guard.as_ref(), on_disk) {
255 (Some(m), Some(id)) => m.identity != id,
256 (Some(_), None) => true,
257 (None, _) => false,
258 };
259 if stale {
260 *guard = None;
261 }
262 if guard.is_none() {
263 *guard = MmappedShard::open(&self.path);
264 }
265 }
266
267 fn invalidate_mmap(&self) {
268 let mut guard = self.mmap.lock();
269 *guard = None;
270 }
271
272 pub fn get(
275 &self,
276 path: &str,
277 mtime_secs: i64,
278 mtime_nsecs: i64,
279 env_fingerprint: u64,
280 ) -> Option<Vec<u8>> {
281 self.ensure_mmap();
282 let guard = self.mmap.lock();
283 let shard = guard.as_ref()?;
284 if !shard.header_ok() {
285 return None;
286 }
287 let entry = shard.lookup(path)?;
288
289 let entry_mtime_s: i64 = entry.mtime_secs.into();
290 let entry_mtime_ns: i64 = entry.mtime_nsecs.into();
291 if entry_mtime_s != mtime_secs || entry_mtime_ns != mtime_nsecs {
292 return None;
293 }
294 let entry_fp: u64 = entry.env_fingerprint.into();
295 if entry_fp != env_fingerprint {
296 return None;
297 }
298
299 match current_binary_identity() {
312 Some((bin_mtime, bin_len)) => {
313 let cached_bin_mtime: i64 = entry.binary_mtime_at_cache.into();
314 let cached_bin_len: u64 = entry.binary_len_at_cache.into();
315 if cached_bin_mtime != bin_mtime || cached_bin_len != bin_len {
316 return None;
317 }
318 }
319 None => return None,
321 }
322
323 Some(entry.chunk_blob.as_slice().to_vec())
324 }
325
326 pub fn put(
328 &self,
329 path: &str,
330 mtime_secs: i64,
331 mtime_nsecs: i64,
332 env_fingerprint: u64,
333 chunk_blob: Vec<u8>,
334 ) -> Result<(), String> {
335 let _lock = match acquire_lock(&self.lock_path) {
336 Some(l) => l,
337 None => return Ok(()),
338 };
339
340 let mut shard = match read_owned_shard(&self.path) {
341 Some(s)
342 if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
343 && s.header.pointer_width as usize == std::mem::size_of::<usize>()
344 && s.header.format_version == SHARD_FORMAT_VERSION =>
345 {
346 s
347 }
348 _ => fresh_shard(),
349 };
350
351 self.fold_runs(&mut shard);
352 let (bin_mtime, bin_len) = current_binary_identity().unwrap_or((0, 0));
353 let entry = ScriptEntry {
354 mtime_secs,
355 mtime_nsecs,
356 binary_mtime_at_cache: bin_mtime,
357 binary_len_at_cache: bin_len,
358 cached_at_secs: now_secs(),
359 env_fingerprint,
360 chunk_blob,
361 };
362 shard.entries.insert(path.to_string(), entry);
363 shard.header.built_at_secs = now_secs() as u64;
364
365 write_shard_atomic(&self.path, &shard)?;
366 let _ = std::fs::remove_file(self.runs_path());
367 self.invalidate_mmap();
368 Ok(())
369 }
370
371 pub fn put_many(&self, batch: Vec<PendingPut>) -> Result<(), String> {
376 if batch.is_empty() {
377 return Ok(());
378 }
379 let _lock = match acquire_lock(&self.lock_path) {
380 Some(l) => l,
381 None => return Ok(()),
382 };
383 let mut shard = match read_owned_shard(&self.path) {
384 Some(s)
385 if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
386 && s.header.pointer_width as usize == std::mem::size_of::<usize>()
387 && s.header.format_version == SHARD_FORMAT_VERSION =>
388 {
389 s
390 }
391 _ => fresh_shard(),
392 };
393 self.fold_runs(&mut shard);
394 let (bin_mtime, bin_len) = current_binary_identity().unwrap_or((0, 0));
395 for p in batch {
396 shard.entries.insert(
397 p.path,
398 ScriptEntry {
399 mtime_secs: p.mtime_secs,
400 mtime_nsecs: p.mtime_nsecs,
401 binary_mtime_at_cache: bin_mtime,
402 binary_len_at_cache: bin_len,
403 cached_at_secs: now_secs(),
404 env_fingerprint: p.env_fingerprint,
405 chunk_blob: p.blob,
406 },
407 );
408 }
409 shard.header.built_at_secs = now_secs() as u64;
410 write_shard_atomic(&self.path, &shard)?;
411 let _ = std::fs::remove_file(self.runs_path());
412 self.invalidate_mmap();
413 Ok(())
414 }
415
416 pub fn stats(&self) -> (i64, i64) {
418 self.ensure_mmap();
419 let guard = self.mmap.lock();
420 let (mut count, mut bytes) = guard.as_ref().map_or((0, 0), |shard| {
421 let bytes: i64 = shard
422 .shard()
423 .entries
424 .values()
425 .map(|e| e.chunk_blob.len() as i64)
426 .sum();
427 (shard.entry_count() as i64, bytes)
428 });
429 let mut seen = std::collections::HashSet::new();
431 for p in PENDING.lock().iter() {
432 let in_shard = guard
433 .as_ref()
434 .is_some_and(|s| s.shard().entries.get(p.path.as_str()).is_some());
435 if !in_shard && seen.insert(p.path.clone()) {
436 count += 1;
437 bytes += p.blob.len() as i64;
438 }
439 }
440 (count, bytes)
441 }
442
443 pub fn list_scripts(&self) -> Vec<(String, f64, String, String)> {
446 self.ensure_mmap();
447 let guard = self.mmap.lock();
448 let v = guard
449 .as_ref()
450 .map(|s| s.shard().header.zshrs_version.as_str().to_string())
451 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
452 let mut out: Vec<(String, f64, String, String, i64)> = guard
453 .as_ref()
454 .map(|shard| {
455 shard
456 .shard()
457 .entries
458 .iter()
459 .map(|(k, e)| {
460 let chunk_kb = e.chunk_blob.len() as f64 / 1024.0;
461 let cached_at: i64 = e.cached_at_secs.into();
462 let ts = format_local_ts(cached_at);
463 (k.as_str().to_string(), chunk_kb, v.clone(), ts, cached_at)
464 })
465 .collect()
466 })
467 .unwrap_or_default();
468 self.overlay_runs(&mut out);
469 let now = now_secs();
472 for p in PENDING.lock().iter() {
473 out.retain(|row| row.0 != p.path);
474 out.push((
475 p.path.clone(),
476 p.blob.len() as f64 / 1024.0,
477 v.clone(),
478 format_local_ts(now),
479 now,
480 ));
481 }
482 out.sort_by_key(|x| std::cmp::Reverse(x.4));
483 out.into_iter()
484 .map(|(p, ck, ver, ts, _)| (p, ck, ver, ts))
485 .collect()
486 }
487
488 fn runs_path(&self) -> PathBuf {
490 self.path.with_file_name("scripts.runs")
491 }
492
493 fn overlay_runs(&self, rows: &mut [(String, f64, String, String, i64)]) {
496 let mut ran = read_runs(&self.runs_path());
497 for (path, ts) in RAN.lock().iter() {
498 let t = ran.entry(path.clone()).or_insert(*ts);
499 *t = (*t).max(*ts);
500 }
501 for row in rows.iter_mut() {
502 if let Some(&ts) = ran.get(&row.0).filter(|&&ts| ts > row.4) {
503 row.3 = format_local_ts(ts);
504 row.4 = ts;
505 }
506 }
507 }
508
509 fn fold_runs(&self, shard: &mut ScriptShard) {
512 for (path, ts) in read_runs(&self.runs_path()) {
513 if let Some(e) = shard.entries.get_mut(&path) {
514 e.cached_at_secs = e.cached_at_secs.max(ts);
515 }
516 }
517 }
518
519 fn mirror_path(&self) -> PathBuf {
522 self.path.with_file_name("scripts.db")
523 }
524
525 fn refresh_mirror(&self, conn: &rusqlite::Connection) -> rusqlite::Result<()> {
530 use rusqlite::OptionalExtension;
531 conn.execute_batch(
532 "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
533 CREATE TABLE IF NOT EXISTS scripts (
534 path TEXT PRIMARY KEY,
535 kind TEXT NOT NULL,
536 events INTEGER NOT NULL,
537 bytes INTEGER NOT NULL,
538 version TEXT NOT NULL,
539 cached_at INTEGER NOT NULL,
540 source_mtime_secs INTEGER NOT NULL,
541 source_mtime_nsecs INTEGER NOT NULL,
542 env_fingerprint TEXT NOT NULL,
543 binary_len INTEGER NOT NULL
544 );
545 CREATE INDEX IF NOT EXISTS scripts_cached_at ON scripts(cached_at DESC);",
546 )?;
547 let on_disk = std::fs::metadata(&self.path).ok().map(|m| {
548 let (ino, len, mtime_ns) = file_identity(&m);
549 format!("{ino}:{len}:{mtime_ns}")
550 });
551 let recorded: Option<String> = conn
552 .query_row("SELECT value FROM meta WHERE key = 'shard'", [], |r| r.get(0))
553 .optional()?;
554 if recorded == on_disk {
555 return Ok(());
556 }
557 let tx = conn.unchecked_transaction()?;
558 tx.execute("DELETE FROM scripts", [])?;
559 if let Some(shard) = read_owned_shard(&self.path) {
560 let mut insert = tx.prepare(
561 "INSERT OR REPLACE INTO scripts
562 (path, kind, events, bytes, version, cached_at,
563 source_mtime_secs, source_mtime_nsecs, env_fingerprint, binary_len)
564 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
565 )?;
566 for (path, e) in &shard.entries {
567 let (kind, events) = match e.chunk_blob.first() {
568 Some(1) if e.chunk_blob.len() >= 9 => {
570 let n = u64::from_le_bytes(e.chunk_blob[1..9].try_into().unwrap());
571 ("events", n as i64)
572 }
573 Some(0) => ("script", 1),
574 _ => ("unknown", 0),
575 };
576 insert.execute(rusqlite::params![
577 path,
578 kind,
579 events,
580 e.chunk_blob.len() as i64,
581 shard.header.zshrs_version,
582 e.cached_at_secs,
583 e.mtime_secs,
584 e.mtime_nsecs,
585 format!("{:016x}", e.env_fingerprint),
586 e.binary_len_at_cache as i64,
587 ])?;
588 }
589 }
590 match on_disk {
591 Some(id) => tx.execute("INSERT OR REPLACE INTO meta (key, value) VALUES ('shard', ?1)", [id])?,
592 None => tx.execute("DELETE FROM meta WHERE key = 'shard'", [])?,
593 };
594 tx.commit()
595 }
596
597 pub fn mirror_rows(&self, pattern: &str) -> Option<Vec<(String, f64, String, String)>> {
603 let _lowfd = crate::lowfd::LowFdGuard::new();
604 let conn = rusqlite::Connection::open(self.mirror_path()).ok()?;
605 conn.busy_timeout(std::time::Duration::from_secs(5)).ok()?;
606 let _ = conn.pragma_update(None, "journal_mode", "WAL");
607 crate::lowfd::register_internal_fds();
608 self.refresh_mirror(&conn).ok()?;
609 let mut stmt = conn
610 .prepare(
611 "SELECT path, bytes, version, cached_at FROM scripts
612 WHERE instr(path, ?1) > 0 ORDER BY cached_at DESC, path",
613 )
614 .ok()?;
615 let mut out: Vec<(String, f64, String, String, i64)> = stmt
616 .query_map([pattern], |r| {
617 let path: String = r.get(0)?;
618 let bytes: i64 = r.get(1)?;
619 let version: String = r.get(2)?;
620 let cached_at: i64 = r.get(3)?;
621 Ok((path, bytes as f64 / 1024.0, version, format_local_ts(cached_at), cached_at))
622 })
623 .ok()?
624 .filter_map(Result::ok)
625 .collect();
626 self.overlay_runs(&mut out);
627 let now = now_secs();
629 for p in PENDING.lock().iter().filter(|p| p.path.contains(pattern)) {
630 out.retain(|row| row.0 != p.path);
631 out.push((
632 p.path.clone(),
633 p.blob.len() as f64 / 1024.0,
634 env!("CARGO_PKG_VERSION").to_string(),
635 format_local_ts(now),
636 now,
637 ));
638 }
639 out.sort_by_key(|x| std::cmp::Reverse(x.4));
640 Some(out.into_iter().map(|(p, kb, v, ts, _)| (p, kb, v, ts)).collect())
641 }
642
643 pub fn evict_stale(&self) -> usize {
645 let _lock = match acquire_lock(&self.lock_path) {
646 Some(l) => l,
647 None => return 0,
648 };
649 let mut shard = match read_owned_shard(&self.path) {
650 Some(s) => s,
651 None => return 0,
652 };
653 let before = shard.entries.len();
654 shard.entries.retain(|p, e| match file_mtime(Path::new(p)) {
655 Some((s, ns)) => s == e.mtime_secs && ns == e.mtime_nsecs,
656 None => false,
657 });
658 let evicted = before - shard.entries.len();
659 if evicted > 0 {
660 let _ = write_shard_atomic(&self.path, &shard);
661 self.invalidate_mmap();
662 }
663 evicted
664 }
665 pub fn clear(&self) -> std::io::Result<()> {
667 let _lock = acquire_lock(&self.lock_path);
668 let res = match std::fs::remove_file(&self.path) {
669 Ok(()) => Ok(()),
670 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
671 Err(e) => Err(e),
672 };
673 let _ = std::fs::remove_file(self.runs_path());
674 self.invalidate_mmap();
675 res
676 }
677}
678
679fn file_identity(m: &std::fs::Metadata) -> (u64, u64, i64) {
682 (m.ino(), m.len(), m.mtime() * 1_000_000_000 + m.mtime_nsec())
683}
684
685fn acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
686 let f = File::options()
687 .read(true)
688 .write(true)
689 .create(true)
690 .truncate(false)
691 .open(path)
692 .ok()?;
693 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusive).ok()
694}
695
696fn fresh_shard() -> ScriptShard {
697 ScriptShard {
698 header: ShardHeader {
699 magic: SHARD_MAGIC,
700 format_version: SHARD_FORMAT_VERSION,
701 zshrs_version: env!("CARGO_PKG_VERSION").to_string(),
702 pointer_width: std::mem::size_of::<usize>() as u32,
703 built_at_secs: now_secs() as u64,
704 },
705 entries: HashMap::new(),
706 }
707}
708
709fn read_owned_shard(path: &Path) -> Option<ScriptShard> {
710 let bytes = std::fs::read(path).ok()?;
711 let archived = rkyv::check_archived_root::<ScriptShard>(&bytes[..]).ok()?;
712 archived.deserialize(&mut rkyv::Infallible).ok()
713}
714
715fn write_shard_atomic(path: &Path, shard: &ScriptShard) -> Result<(), String> {
716 let bytes = rkyv::to_bytes::<_, 4096>(shard).map_err(|e| format!("rkyv serialize: {}", e))?;
717 crate::atomic_write::write_bytes_atomic(path, &bytes)
721}
722
723fn now_secs() -> i64 {
724 SystemTime::now()
725 .duration_since(UNIX_EPOCH)
726 .map(|d| d.as_secs() as i64)
727 .unwrap_or(0)
728}
729
730fn format_local_ts(secs: i64) -> String {
731 let dt = chrono::DateTime::<chrono::Local>::from(
732 UNIX_EPOCH + std::time::Duration::from_secs(secs.max(0) as u64),
733 );
734 dt.format("%Y-%m-%d %H:%M:%S").to_string()
735}
736pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
738 let meta = std::fs::metadata(path).ok()?;
739 Some((meta.mtime(), meta.mtime_nsec()))
740}
741
742fn current_binary_identity() -> Option<(i64, u64)> {
745 static BIN_ID: OnceLock<Option<(i64, u64)>> = OnceLock::new();
746 *BIN_ID.get_or_init(|| {
747 let exe = std::env::current_exe().ok()?;
748 let meta = std::fs::metadata(&exe).ok()?;
749 Some((meta.mtime(), meta.len()))
750 })
751}
752
753pub fn default_cache_path() -> PathBuf {
764 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
765 PathBuf::from(custom)
766 } else {
767 dirs::home_dir()
768 .unwrap_or_else(|| PathBuf::from("/tmp"))
769 .join(".zshrs")
770 };
771 root.join("scripts.rkyv")
772}
773
774pub static CACHE_DISABLED: std::sync::atomic::AtomicBool =
779 std::sync::atomic::AtomicBool::new(false);
780
781pub fn cache_enabled() -> bool {
784 if CACHE_DISABLED.load(std::sync::atomic::Ordering::Relaxed) {
785 return false;
786 }
787 !matches!(
788 std::env::var("ZSHRS_CACHE").as_deref(),
789 Ok("0") | Ok("false") | Ok("no")
790 )
791}
792
793pub static CACHE: once_cell::sync::Lazy<Option<ScriptCache>> = once_cell::sync::Lazy::new(|| {
796 if !cache_enabled() {
797 return None;
798 }
799 ScriptCache::open(&default_cache_path()).ok()
800});
801
802struct Capture {
807 path: PathBuf,
808 env_fp: u64,
809 alias_base: u64,
812 toplevel: bool,
815 pid: u32,
818 events: Vec<GuardedEvent>,
819}
820
821#[derive(serde::Serialize, serde::Deserialize)]
833pub struct GuardedEvent {
834 pub chunk: fusevm::Chunk,
835 pub alias_delta: u64,
836 pub start: u64,
837}
838
839thread_local! {
840 static CAPTURES: std::cell::RefCell<Vec<Capture>> = const { std::cell::RefCell::new(Vec::new()) };
841}
842
843pub fn capture_begin(path: &Path, env_fp: u64, toplevel: bool) {
845 let cap = Capture {
848 path: path.canonicalize().unwrap_or_else(|_| path.to_path_buf()),
849 env_fp,
850 alias_base: env_generation().0,
851 toplevel,
852 pid: std::process::id(),
853 events: Vec::new(),
854 };
855 CAPTURES.with(|c| c.borrow_mut().push(cap));
856}
857
858pub fn capture_push(chunk: &fusevm::Chunk, start: usize) {
863 let now = env_generation().0;
864 CAPTURES.with(|c| {
865 if let Some(top) = c.borrow_mut().last_mut() {
866 top.events.push(GuardedEvent {
867 chunk: chunk.clone(),
868 alias_delta: now.wrapping_sub(top.alias_base),
869 start: start as u64,
870 });
871 }
872 });
873}
874
875pub fn capture_commit(complete: bool) {
879 let Some(cap) = CAPTURES.with(|c| c.borrow_mut().pop()) else {
880 return;
881 };
882 store_capture(cap, complete);
883}
884
885pub fn capture_commit_all_on_exit(errored: bool) {
889 let caps: Vec<Capture> = CAPTURES.with(|c| std::mem::take(&mut *c.borrow_mut()));
890 for cap in caps.into_iter().rev() {
891 store_capture(cap, !errored);
892 }
893}
894
895fn store_capture(cap: Capture, complete: bool) {
896 if !complete || cap.events.is_empty() || cap.pid != std::process::id() {
901 return;
902 }
903 let _ = cap.toplevel;
904 if let Ok(blob) = bincode::serialize(&cap.events) {
905 let _ = try_save_bytes(&cap.path, BlobKind::Guarded, cap.env_fp, &blob);
906 }
907}
908
909#[derive(Clone, Copy, PartialEq, Eq, Debug)]
913pub enum BlobKind {
914 Script = 0,
916 Events = 1,
919 Guarded = 2,
922}
923
924pub fn env_fingerprint() -> u64 {
932 use std::sync::atomic::Ordering;
933 static MEMO: Mutex<Option<(u64, u64, u64, u64)>> = Mutex::new(None);
934 let alias_gen = crate::ported::hashtable::ALIAS_GEN.load(Ordering::Relaxed);
935 let opt_gen = crate::opts_cache::generation();
936 let mut memo = MEMO.lock();
937 if let Some((ag, og, alias_hash, opt_hash)) = *memo {
938 if ag == alias_gen && og == opt_gen {
939 return fnv_mix(alias_hash, opt_hash);
940 }
941 }
942 let alias_hash = hash_alias_tables();
943 let opt_hash = hash_option_store();
944 *memo = Some((alias_gen, opt_gen, alias_hash, opt_hash));
945 fnv_mix(alias_hash, opt_hash)
946}
947
948pub fn env_generation() -> (u64, u64) {
951 (
952 crate::ported::hashtable::ALIAS_GEN.load(std::sync::atomic::Ordering::Relaxed),
953 crate::opts_cache::generation(),
954 )
955}
956
957fn fnv_mix(a: u64, b: u64) -> u64 {
958 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
959 for byte in a.to_le_bytes().into_iter().chain(b.to_le_bytes()) {
960 h ^= u64::from(byte);
961 h = h.wrapping_mul(0x0000_0100_0000_01b3);
962 }
963 h
964}
965
966fn fnv_bytes(mut h: u64, bytes: &[u8]) -> u64 {
967 for &byte in bytes {
968 h ^= u64::from(byte);
969 h = h.wrapping_mul(0x0000_0100_0000_01b3);
970 }
971 h ^= 0xff;
973 h.wrapping_mul(0x0000_0100_0000_01b3)
974}
975
976fn hash_alias_tables() -> u64 {
977 use crate::ported::hashtable::{aliastab_lock, sufaliastab_lock};
978 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
979 for table in [aliastab_lock(), sufaliastab_lock()] {
980 let Ok(t) = table.read() else { continue };
981 let mut rows: Vec<(&String, &crate::ported::zsh_h::alias)> = t.iter().collect();
984 rows.sort_by(|a, b| a.0.cmp(b.0));
985 for (name, a) in rows {
986 h = fnv_bytes(h, name.as_bytes());
987 h = fnv_bytes(h, a.text.as_bytes());
988 h = fnv_bytes(h, &a.node.flags.to_le_bytes());
989 }
990 h = fnv_bytes(h, b"|");
991 }
992 h
993}
994
995fn hash_option_store() -> u64 {
996 let mut rows: Vec<(String, bool)> = crate::ported::options::opt_state_snapshot()
997 .into_iter()
998 .collect();
999 rows.sort();
1000 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1001 for (name, on) in rows {
1002 h = fnv_bytes(h, name.as_bytes());
1003 h = fnv_bytes(h, &[on as u8]);
1004 }
1005 let emul = crate::ported::options::emulation.load(std::sync::atomic::Ordering::Relaxed);
1006 fnv_bytes(h, &emul.to_le_bytes())
1007}
1008
1009fn read_runs(path: &Path) -> HashMap<String, i64> {
1011 let mut out = HashMap::new();
1012 let text = std::fs::read(path).map(|b| String::from_utf8_lossy(&b).into_owned()).unwrap_or_default();
1013 for line in text.lines() {
1014 let Some((ts, p)) = line.split_once('\t') else { continue };
1015 let Ok(ts) = ts.parse::<i64>() else { continue };
1016 let t = out.entry(p.to_string()).or_insert(ts);
1017 *t = (*t).max(ts);
1018 }
1019 out
1020}
1021
1022fn append_runs(path: &Path, batch: &[(String, i64)]) {
1025 let lines: String = batch.iter().map(|(p, ts)| format!("{ts}\t{p}\n")).collect();
1026 let appended = std::fs::OpenOptions::new()
1027 .create(true)
1028 .append(true)
1029 .open(path)
1030 .and_then(|mut f| f.write_all(lines.as_bytes()));
1031 if appended.is_err() || std::fs::metadata(path).map_or(0, |m| m.len()) < RUNS_COMPACT_BYTES {
1032 return;
1033 }
1034 let deduped: String = read_runs(path).into_iter().map(|(p, ts)| format!("{ts}\t{p}\n")).collect();
1035 let _ = crate::atomic_write::write_bytes_atomic(path, deduped.as_bytes());
1036}
1037
1038pub fn try_load_bytes(path: &Path, kind: BlobKind, env_fp: u64) -> Option<Vec<u8>> {
1041 let cache = CACHE.as_ref()?;
1042 let canonical = path.canonicalize().ok()?;
1043 let path_str = canonical.to_string_lossy();
1044 let (mtime_s, mtime_ns) = file_mtime(&canonical)?;
1045 let buffered = PENDING.lock().iter().rev().find_map(|p| {
1048 (p.path == path_str && p.mtime_secs == mtime_s && p.mtime_nsecs == mtime_ns && p.env_fingerprint == env_fp)
1049 .then(|| p.blob.clone())
1050 });
1051 let blob = match buffered {
1052 Some(b) => b,
1053 None => cache.get(&path_str, mtime_s, mtime_ns, env_fp)?,
1054 };
1055 let (tag, body) = blob.split_first()?;
1056 if *tag != kind as u8 {
1057 return None;
1058 }
1059 RAN_PID.store(std::process::id(), std::sync::atomic::Ordering::Relaxed);
1060 RAN.lock().push((path_str.into_owned(), now_secs()));
1061 Some(body.to_vec())
1062}
1063
1064pub fn try_save_bytes(
1070 path: &Path,
1071 kind: BlobKind,
1072 env_fp: u64,
1073 chunk_blob: &[u8],
1074) -> Result<(), String> {
1075 let Some(cache) = CACHE.as_ref() else {
1076 return Ok(());
1077 };
1078 let canonical = match path.canonicalize() {
1079 Ok(p) => p,
1080 Err(_) => return Ok(()),
1081 };
1082 let path_str = canonical.to_string_lossy();
1083 let (mtime_s, mtime_ns) = match file_mtime(&canonical) {
1084 Some(m) => m,
1085 None => return Ok(()),
1086 };
1087 let mut tagged = Vec::with_capacity(chunk_blob.len() + 1);
1088 tagged.push(kind as u8);
1089 tagged.extend_from_slice(chunk_blob);
1090 PENDING_PID.store(std::process::id(), std::sync::atomic::Ordering::Relaxed);
1091 let over = {
1092 let mut pending = PENDING.lock();
1093 pending.push(PendingPut {
1094 path: path_str.into_owned(),
1095 mtime_secs: mtime_s,
1096 mtime_nsecs: mtime_ns,
1097 env_fingerprint: env_fp,
1098 blob: tagged,
1099 });
1100 pending.len() >= PENDING_FLUSH_MAX
1101 };
1102 if over {
1103 try_flush_pending();
1104 }
1105 Ok(())
1106}
1107
1108pub fn try_flush_pending() {
1112 if RAN_PID.load(std::sync::atomic::Ordering::Relaxed) == std::process::id() {
1115 let ran = std::mem::take(&mut *RAN.lock());
1116 if let (false, Some(cache)) = (ran.is_empty(), CACHE.as_ref()) {
1117 append_runs(&cache.runs_path(), &ran);
1118 }
1119 }
1120 if PENDING_PID.load(std::sync::atomic::Ordering::Relaxed) != std::process::id() {
1123 return;
1124 }
1125 let batch = std::mem::take(&mut *PENDING.lock());
1126 if batch.is_empty() {
1127 return;
1128 }
1129 if let Some(cache) = CACHE.as_ref() {
1130 let _ = cache.put_many(batch);
1131 }
1132}
1133pub fn stats() -> Option<(i64, i64)> {
1135 CACHE.as_ref().map(|c| c.stats())
1136}
1137pub fn evict_stale() -> usize {
1139 CACHE.as_ref().map(|c| c.evict_stale()).unwrap_or(0)
1140}
1141pub fn clear() -> bool {
1143 CACHE.as_ref().map(|c| c.clear().is_ok()).unwrap_or(false)
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148 use super::*;
1149 use tempfile::tempdir;
1150
1151 #[test]
1152 fn round_trip() {
1153 let _g = crate::test_util::global_state_lock();
1154 let dir = tempdir().unwrap();
1155 let cache_path = dir.path().join("scripts.rkyv");
1156 let cache = ScriptCache::open(&cache_path).unwrap();
1157
1158 let script_path = dir.path().join("test.zsh");
1159 std::fs::write(&script_path, "echo hi").unwrap();
1160
1161 let (mtime_s, mtime_ns) = file_mtime(&script_path).unwrap();
1162 let path_str = script_path.to_string_lossy().to_string();
1163
1164 let blob = vec![1u8, 2, 3, 4, 5];
1165 cache
1166 .put(&path_str, mtime_s, mtime_ns, 0, blob.clone())
1167 .unwrap();
1168
1169 let loaded = cache.get(&path_str, mtime_s, mtime_ns, 0).unwrap();
1170 assert_eq!(loaded, blob);
1171
1172 let (count, _bytes) = cache.stats();
1173 assert_eq!(count, 1);
1174 }
1175
1176 #[test]
1181 fn a_write_from_another_process_shows_up_in_a_mapped_reader() {
1182 let _g = crate::test_util::global_state_lock();
1183 let dir = tempdir().unwrap();
1184 let cache_path = dir.path().join("scripts.rkyv");
1185 let reader = ScriptCache::open(&cache_path).unwrap();
1186 let writer = ScriptCache::open(&cache_path).unwrap();
1187
1188 let first = dir.path().join("first.zsh");
1189 let second = dir.path().join("second.zsh");
1190 std::fs::write(&first, "echo 1").unwrap();
1191 std::fs::write(&second, "echo 2").unwrap();
1192 let (s1, n1) = file_mtime(&first).unwrap();
1193 let (s2, n2) = file_mtime(&second).unwrap();
1194
1195 writer.put(&first.to_string_lossy(), s1, n1, 0, vec![1]).unwrap();
1196 assert_eq!(reader.list_scripts().len(), 1, "reader maps the shard");
1197
1198 writer.put(&second.to_string_lossy(), s2, n2, 0, vec![2]).unwrap();
1199 let rows = reader.list_scripts();
1200 assert_eq!(rows.len(), 2, "the replaced shard must be remapped: {rows:?}");
1201 assert_eq!(reader.stats().0, 2);
1202 }
1203
1204 #[test]
1208 fn the_sqlite_mirror_follows_the_shard() {
1209 let _g = crate::test_util::global_state_lock();
1210 let dir = tempdir().unwrap();
1211 let cache = ScriptCache::open(&dir.path().join("scripts.rkyv")).unwrap();
1212 let one = dir.path().join("one.zsh");
1213 let two = dir.path().join("two.zsh");
1214 std::fs::write(&one, "echo 1").unwrap();
1215 std::fs::write(&two, "echo 2").unwrap();
1216 let (s1, n1) = file_mtime(&one).unwrap();
1217 let (s2, n2) = file_mtime(&two).unwrap();
1218
1219 assert_eq!(cache.mirror_rows("").unwrap().len(), 0, "empty before any write");
1220
1221 cache.put(&one.to_string_lossy(), s1, n1, 0, vec![1u8]).unwrap();
1222 let rows = cache.mirror_rows("").unwrap();
1223 assert_eq!(rows.len(), 1);
1224 assert!(rows[0].0.ends_with("one.zsh"));
1225 assert!(dir.path().join("scripts.db").is_file());
1226
1227 cache.put(&two.to_string_lossy(), s2, n2, 0, vec![2u8; 4096]).unwrap();
1228 assert_eq!(cache.mirror_rows("").unwrap().len(), 2);
1229 let only_two = cache.mirror_rows("two.zsh").unwrap();
1230 assert_eq!(only_two.len(), 1);
1231 assert!(only_two[0].1 > 3.9, "kb is the blob size: {only_two:?}");
1232
1233 cache.clear().unwrap();
1234 assert_eq!(cache.mirror_rows("").unwrap().len(), 0, "deleted shard empties the mirror");
1235 }
1236
1237 #[test]
1242 fn a_cache_hit_moves_the_listed_time_and_survives_a_rewrite() {
1243 let _g = crate::test_util::global_state_lock();
1244 let dir = tempdir().unwrap();
1245 let cache = ScriptCache::open(&dir.path().join("scripts.rkyv")).unwrap();
1246 let one = dir.path().join("one.zsh");
1247 let two = dir.path().join("two.zsh");
1248 std::fs::write(&one, "echo 1").unwrap();
1249 std::fs::write(&two, "echo 2").unwrap();
1250 let (s1, n1) = file_mtime(&one).unwrap();
1251 let (s2, n2) = file_mtime(&two).unwrap();
1252 let one_key = one.to_string_lossy().to_string();
1253 cache.put(&one_key, s1, n1, 0, vec![1u8]).unwrap();
1254 cache.put(&two.to_string_lossy(), s2, n2, 0, vec![2u8]).unwrap();
1255
1256 let ran_at = now_secs() + 1000;
1257 append_runs(&cache.runs_path(), &[(one_key.clone(), ran_at)]);
1258
1259 let by_path = |rows: Vec<(String, f64, String, String)>| {
1260 rows.into_iter().map(|r| (r.0, r.3)).collect::<Vec<_>>()
1261 };
1262 let listed = by_path(cache.list_scripts());
1263 assert!(listed[0].0.ends_with("one.zsh"), "newest run first: {listed:?}");
1264 assert_eq!(listed[0].1, format_local_ts(ran_at));
1265 let mirrored = by_path(cache.mirror_rows("").unwrap());
1266 assert_eq!(mirrored[0], listed[0]);
1267
1268 let three = dir.path().join("three.zsh");
1270 std::fs::write(&three, "echo 3").unwrap();
1271 let (s3, n3) = file_mtime(&three).unwrap();
1272 cache.put(&three.to_string_lossy(), s3, n3, 0, vec![3u8]).unwrap();
1273 assert!(!cache.runs_path().exists());
1274 let after = by_path(cache.list_scripts());
1275 assert!(after[0].0.ends_with("one.zsh"), "{after:?}");
1276 assert_eq!(after[0].1, format_local_ts(ran_at));
1277 }
1278
1279 #[test]
1280 fn mtime_invalidation() {
1281 let _g = crate::test_util::global_state_lock();
1282 let dir = tempdir().unwrap();
1283 let cache_path = dir.path().join("scripts.rkyv");
1284 let cache = ScriptCache::open(&cache_path).unwrap();
1285
1286 let script_path = dir.path().join("test.zsh");
1287 std::fs::write(&script_path, "echo hi").unwrap();
1288
1289 let (mtime_s, mtime_ns) = file_mtime(&script_path).unwrap();
1290 let path_str = script_path.to_string_lossy().to_string();
1291 cache.put(&path_str, mtime_s, mtime_ns, 0, vec![9u8]).unwrap();
1292
1293 assert!(cache.get(&path_str, mtime_s + 1, mtime_ns, 0).is_none());
1294 }
1295
1296 #[test]
1303 fn an_entry_from_another_binary_is_never_served() {
1304 let _g = crate::test_util::global_state_lock();
1305 let dir = tempdir().unwrap();
1306 let cache_path = dir.path().join("scripts.rkyv");
1307 let cache = ScriptCache::open(&cache_path).unwrap();
1308
1309 let script_path = dir.path().join("test.zsh");
1310 std::fs::write(&script_path, "echo hi").unwrap();
1311 let (mtime_s, mtime_ns) = file_mtime(&script_path).unwrap();
1312 let path_str = script_path.to_string_lossy().to_string();
1313 cache.put(&path_str, mtime_s, mtime_ns, 0, vec![9u8]).unwrap();
1314 assert_eq!(
1315 cache.get(&path_str, mtime_s, mtime_ns, 0),
1316 Some(vec![9u8]),
1317 "the emitting binary must hit its own entry",
1318 );
1319
1320 let mut shard = read_owned_shard(&cache_path).expect("shard readable");
1323 shard
1324 .entries
1325 .get_mut(&path_str)
1326 .expect("entry present")
1327 .binary_mtime_at_cache += 10_000;
1328 write_shard_atomic(&cache_path, &shard).unwrap();
1329 let reopened = ScriptCache::open(&cache_path).unwrap();
1330 assert!(
1331 reopened.get(&path_str, mtime_s, mtime_ns, 0).is_none(),
1332 "a chunk from a newer build was accepted",
1333 );
1334
1335 let mut shard = read_owned_shard(&cache_path).expect("shard readable");
1338 let entry = shard.entries.get_mut(&path_str).expect("entry present");
1339 entry.binary_mtime_at_cache -= 10_000;
1340 entry.binary_len_at_cache += 1;
1341 write_shard_atomic(&cache_path, &shard).unwrap();
1342 let reopened = ScriptCache::open(&cache_path).unwrap();
1343 assert!(
1344 reopened.get(&path_str, mtime_s, mtime_ns, 0).is_none(),
1345 "a chunk from a same-second build of a different size was accepted",
1346 );
1347 }
1348
1349 #[test]
1350 fn second_put_replaces_first() {
1351 let _g = crate::test_util::global_state_lock();
1352 let dir = tempdir().unwrap();
1353 let cache_path = dir.path().join("scripts.rkyv");
1354 let cache = ScriptCache::open(&cache_path).unwrap();
1355
1356 let p1 = dir.path().join("a.zsh");
1357 let p2 = dir.path().join("b.zsh");
1358 std::fs::write(&p1, "1").unwrap();
1359 std::fs::write(&p2, "2").unwrap();
1360
1361 let (m1s, m1n) = file_mtime(&p1).unwrap();
1362 let (m2s, m2n) = file_mtime(&p2).unwrap();
1363
1364 cache
1365 .put(&p1.to_string_lossy(), m1s, m1n, 0, vec![1u8])
1366 .unwrap();
1367 cache
1368 .put(&p2.to_string_lossy(), m2s, m2n, 0, vec![2u8])
1369 .unwrap();
1370
1371 let (count, _) = cache.stats();
1372 assert_eq!(count, 2);
1373 assert!(cache.get(&p1.to_string_lossy(), m1s, m1n, 0).is_some());
1374 assert!(cache.get(&p2.to_string_lossy(), m2s, m2n, 0).is_some());
1375 }
1376
1377 #[test]
1378 fn corrupt_file_returns_no_mmap() {
1379 let _g = crate::test_util::global_state_lock();
1380 let dir = tempdir().unwrap();
1381 let cache_path = dir.path().join("scripts.rkyv");
1382 std::fs::write(&cache_path, b"this is not a valid rkyv archive").unwrap();
1383 let cache = ScriptCache::open(&cache_path).unwrap();
1384 assert!(cache.get("/nope", 0, 0, 0).is_none());
1385 }
1386
1387 #[test]
1388 fn clear_removes_file() {
1389 let _g = crate::test_util::global_state_lock();
1390 let dir = tempdir().unwrap();
1391 let cache_path = dir.path().join("scripts.rkyv");
1392 let cache = ScriptCache::open(&cache_path).unwrap();
1393
1394 let script_path = dir.path().join("test.zsh");
1395 std::fs::write(&script_path, "echo hi").unwrap();
1396 let (mtime_s, mtime_ns) = file_mtime(&script_path).unwrap();
1397 cache
1398 .put(&script_path.to_string_lossy(), mtime_s, mtime_ns, 0, vec![7u8])
1399 .unwrap();
1400 assert!(cache_path.exists());
1401
1402 cache.clear().unwrap();
1403 assert!(!cache_path.exists());
1404 }
1405
1406 #[test]
1411 fn now_secs_is_positive_and_within_realistic_range() {
1412 let _g = crate::test_util::global_state_lock();
1413 let n = now_secs();
1414 assert!(
1416 (1_577_836_800..4_102_444_800).contains(&n),
1417 "now_secs out of plausible range: {}",
1418 n
1419 );
1420 }
1421
1422 #[test]
1423 fn now_secs_does_not_go_backwards_in_quick_succession() {
1424 let _g = crate::test_util::global_state_lock();
1425 let a = now_secs();
1426 let b = now_secs();
1427 assert!(b >= a, "now_secs went backwards: {} -> {}", a, b);
1428 }
1429
1430 #[test]
1435 fn format_local_ts_includes_year_and_punctuation() {
1436 let _g = crate::test_util::global_state_lock();
1437 let s = format_local_ts(1_704_067_200);
1440 assert!(s.starts_with("202"), "expected 21st century year: {}", s);
1441 assert!(s.contains('-'), "expected dash separator: {}", s);
1442 assert!(s.contains(':'), "expected colon separator: {}", s);
1443 }
1444
1445 #[test]
1446 fn format_local_ts_length_matches_pattern() {
1447 let _g = crate::test_util::global_state_lock();
1448 let s = format_local_ts(1_700_000_000);
1449 assert_eq!(s.len(), 19, "unexpected width: {}", s);
1451 }
1452
1453 #[test]
1454 fn format_local_ts_handles_zero_secs_via_clamp() {
1455 let _g = crate::test_util::global_state_lock();
1456 let s = format_local_ts(0);
1458 assert_eq!(s.len(), 19);
1459 assert!(s.starts_with("19"), "expected 1970-ish year: {}", s);
1460 }
1461
1462 #[test]
1463 fn format_local_ts_negative_clamped_to_zero() {
1464 let _g = crate::test_util::global_state_lock();
1465 let s = format_local_ts(-1_000_000);
1467 assert_eq!(s.len(), 19);
1468 }
1469
1470 #[test]
1475 fn file_mtime_returns_some_for_real_file() {
1476 let _g = crate::test_util::global_state_lock();
1477 let dir = tempdir().unwrap();
1478 let p = dir.path().join("foo.zsh");
1479 std::fs::write(&p, b"x").unwrap();
1480 let (s, _ns) = file_mtime(&p).unwrap();
1481 assert!(s > 0);
1482 }
1483
1484 #[test]
1485 fn file_mtime_returns_none_for_missing_path() {
1486 let _g = crate::test_util::global_state_lock();
1487 assert!(file_mtime(Path::new("/nonexistent/zshrs/script_cache_missing.bin")).is_none());
1488 }
1489
1490 #[test]
1495 fn default_cache_path_ends_in_scripts_rkyv() {
1496 let _g = crate::test_util::global_state_lock();
1497 let p = default_cache_path();
1498 assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("scripts.rkyv"));
1499 }
1500
1501 #[test]
1502 fn cache_enabled_true_when_env_unset() {
1503 let _g = crate::test_util::global_state_lock();
1504 let prev = std::env::var_os("ZSHRS_CACHE");
1505 std::env::remove_var("ZSHRS_CACHE");
1506 let on = cache_enabled();
1507 if let Some(v) = prev {
1508 std::env::set_var("ZSHRS_CACHE", v);
1509 }
1510 assert!(on, "cache should be enabled when ZSHRS_CACHE is unset");
1511 }
1512
1513 #[test]
1514 fn cache_enabled_false_when_env_is_zero_false_or_no() {
1515 let _g = crate::test_util::global_state_lock();
1516 let prev = std::env::var_os("ZSHRS_CACHE");
1517 for v in ["0", "false", "no"] {
1518 std::env::set_var("ZSHRS_CACHE", v);
1519 assert!(!cache_enabled(), "ZSHRS_CACHE={} must disable cache", v);
1520 }
1521 if let Some(v) = prev {
1522 std::env::set_var("ZSHRS_CACHE", v);
1523 } else {
1524 std::env::remove_var("ZSHRS_CACHE");
1525 }
1526 }
1527
1528 #[test]
1529 fn cache_enabled_true_for_other_env_values() {
1530 let _g = crate::test_util::global_state_lock();
1533 let prev = std::env::var_os("ZSHRS_CACHE");
1534 for v in ["1", "true", "yes", "on", ""] {
1535 std::env::set_var("ZSHRS_CACHE", v);
1536 assert!(
1537 cache_enabled(),
1538 "ZSHRS_CACHE={:?} must NOT disable cache",
1539 v
1540 );
1541 }
1542 if let Some(v) = prev {
1543 std::env::set_var("ZSHRS_CACHE", v);
1544 } else {
1545 std::env::remove_var("ZSHRS_CACHE");
1546 }
1547 }
1548}