use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use memmap2::Mmap;
use parking_lot::Mutex;
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
pub const SHARD_MAGIC: u32 = 0x5A52_4450;
pub const SHARD_FORMAT_VERSION: u32 = 3;
const PENDING_FLUSH_MAX: usize = 4096;
const SHARD_MAX_ENTRIES: usize = 100_000;
#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct DeparseShard {
pub magic: u32,
pub format_version: u32,
pub pointer_width: u32,
pub binary_mtime: i64,
pub binary_len: u64,
pub entries: HashMap<[u8; 32], String>,
}
struct MmappedShard {
_mmap: Mmap,
archived: *const ArchivedDeparseShard,
}
unsafe impl Send for MmappedShard {}
unsafe impl Sync for MmappedShard {}
impl MmappedShard {
fn open(path: &Path) -> Option<Self> {
let file = File::open(path).ok()?;
let mmap = unsafe { Mmap::map(&file).ok()? };
let archived = rkyv::check_archived_root::<DeparseShard>(&mmap[..]).ok()?;
let archived = archived as *const ArchivedDeparseShard;
let shard = Self { _mmap: mmap, archived };
shard.header_ok().then_some(shard)
}
fn shard(&self) -> &ArchivedDeparseShard {
unsafe { &*self.archived }
}
fn header_ok(&self) -> bool {
let s = self.shard();
let Some((mtime, len)) = current_binary_identity() else {
return false;
};
let magic: u32 = s.magic.into();
let fv: u32 = s.format_version.into();
let pw: u32 = s.pointer_width.into();
let bm: i64 = s.binary_mtime.into();
let bl: u64 = s.binary_len.into();
magic == SHARD_MAGIC
&& fv == SHARD_FORMAT_VERSION
&& pw as usize == std::mem::size_of::<usize>()
&& bm == mtime
&& bl == len
}
fn get(&self, key: &[u8; 32]) -> Option<String> {
self.shard().entries.get(key).map(|s| s.as_str().to_string())
}
}
pub struct DeparseCache {
path: PathBuf,
lock_path: PathBuf,
mmap: Mutex<Option<Option<MmappedShard>>>,
pending: Mutex<HashMap<[u8; 32], String>>,
}
impl DeparseCache {
fn open(path: &Path) -> Self {
let lock_path = path.with_extension("rkyv.lock");
Self {
path: path.to_path_buf(),
lock_path,
mmap: Mutex::new(None),
pending: Mutex::new(HashMap::new()),
}
}
fn get(&self, key: &[u8; 32]) -> Option<String> {
if let Some(hit) = self.pending.lock().get(key) {
return Some(hit.clone());
}
let mut guard = self.mmap.lock();
let shard = guard.get_or_insert_with(|| MmappedShard::open(&self.path));
shard.as_ref()?.get(key)
}
fn put(&self, key: [u8; 32], deparsed: String) {
let full = {
let mut pending = self.pending.lock();
pending.insert(key, deparsed);
pending.len() >= PENDING_FLUSH_MAX
};
if full {
let _ = self.flush_pending();
}
}
fn flush_pending(&self) -> Result<(), String> {
let batch = {
let mut pending = self.pending.lock();
if pending.is_empty() {
return Ok(());
}
std::mem::take(&mut *pending)
};
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let Some(_lock) = try_acquire_lock(&self.lock_path) else {
let mut pending = self.pending.lock();
for (key, deparsed) in batch {
if pending.len() >= PENDING_FLUSH_MAX {
break;
}
pending.entry(key).or_insert(deparsed);
}
return Ok(());
};
let Some((mtime, len)) = current_binary_identity() else {
return Ok(());
};
let mut shard = read_owned_shard(&self.path)
.filter(|s| {
s.magic == SHARD_MAGIC
&& s.format_version == SHARD_FORMAT_VERSION
&& s.pointer_width as usize == std::mem::size_of::<usize>()
&& s.binary_mtime == mtime
&& s.binary_len == len
&& s.entries.len() < SHARD_MAX_ENTRIES
})
.unwrap_or_else(|| DeparseShard {
magic: SHARD_MAGIC,
format_version: SHARD_FORMAT_VERSION,
pointer_width: std::mem::size_of::<usize>() as u32,
binary_mtime: mtime,
binary_len: len,
entries: HashMap::new(),
});
shard.entries.extend(batch);
let bytes =
rkyv::to_bytes::<_, 4096>(&shard).map_err(|e| format!("rkyv serialize: {}", e))?;
crate::atomic_write::write_bytes_atomic(&self.path, &bytes)?;
*self.mmap.lock() = None;
Ok(())
}
}
fn try_acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
let f = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.ok()?;
nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusiveNonblock).ok()
}
fn read_owned_shard(path: &Path) -> Option<DeparseShard> {
let bytes = std::fs::read(path).ok()?;
let archived = rkyv::check_archived_root::<DeparseShard>(&bytes[..]).ok()?;
archived.deserialize(&mut rkyv::Infallible).ok()
}
fn current_binary_identity() -> Option<(i64, u64)> {
use std::os::unix::fs::MetadataExt;
static BIN_ID: std::sync::OnceLock<Option<(i64, u64)>> = std::sync::OnceLock::new();
*BIN_ID.get_or_init(|| {
let meta = std::fs::metadata(std::env::current_exe().ok()?).ok()?;
Some((meta.mtime(), meta.len()))
})
}
fn default_cache_path() -> PathBuf {
let root = match std::env::var_os("ZSHRS_HOME") {
Some(custom) => PathBuf::from(custom),
None => dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".zshrs"),
};
root.join("deparse.rkyv")
}
static CACHE: once_cell::sync::Lazy<Option<DeparseCache>> = once_cell::sync::Lazy::new(|| {
crate::autoload_cache::cache_enabled().then(|| DeparseCache::open(&default_cache_path()))
});
pub fn key(name: &str, text: &str) -> Option<[u8; 32]> {
use sha2::Digest;
if !crate::ported::lex::noaliases() {
return None;
}
let mut bits = [0u8; (crate::ported::zsh_h::OPT_SIZE as usize).div_ceil(8)];
for optno in 1..crate::ported::zsh_h::OPT_SIZE {
if crate::ported::zsh_h::isset(optno) {
bits[optno as usize / 8] |= 1 << (optno % 8);
}
}
let emulation = crate::ported::options::emulation.load(std::sync::atomic::Ordering::Relaxed);
let mut h = sha2::Sha256::new();
h.update(name.as_bytes());
h.update([0]);
h.update(text.as_bytes());
h.update([0]);
h.update(bits);
h.update(emulation.to_le_bytes());
Some(h.finalize().into())
}
pub fn lookup(key: &[u8; 32]) -> Option<String> {
CACHE.as_ref()?.get(key)
}
pub fn record(key: [u8; 32], deparsed: &str) {
if let Some(cache) = CACHE.as_ref() {
cache.put(key, deparsed.to_string());
}
}
pub fn try_flush_pending() {
if let Some(cache) = CACHE.as_ref() {
if let Err(e) = cache.flush_pending() {
if !crate::atexit_teardown::active() {
tracing::warn!(error = %e, "deparse cache: could not flush");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flushed_entry_survives_into_a_new_handle() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("deparse.rkyv");
let key = [7u8; 32];
let writer = DeparseCache::open(&path);
writer.put(key, "print hi".to_string());
assert_eq!(writer.get(&key).as_deref(), Some("print hi"), "pending read");
writer.flush_pending().unwrap();
let reader = DeparseCache::open(&path);
assert_eq!(reader.get(&key).as_deref(), Some("print hi"));
assert_eq!(reader.get(&[8u8; 32]), None);
}
#[test]
fn flush_merges_with_the_shard_on_disk() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("deparse.rkyv");
let a = DeparseCache::open(&path);
a.put([1u8; 32], "one".to_string());
a.flush_pending().unwrap();
let b = DeparseCache::open(&path);
b.put([2u8; 32], "two".to_string());
b.flush_pending().unwrap();
let c = DeparseCache::open(&path);
assert_eq!(c.get(&[1u8; 32]).as_deref(), Some("one"));
assert_eq!(c.get(&[2u8; 32]).as_deref(), Some("two"));
}
#[test]
fn shard_from_another_binary_is_ignored() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("deparse.rkyv");
let (mtime, len) = current_binary_identity().unwrap();
let mut entries = HashMap::new();
entries.insert([3u8; 32], "stale".to_string());
let shard = DeparseShard {
magic: SHARD_MAGIC,
format_version: SHARD_FORMAT_VERSION,
pointer_width: std::mem::size_of::<usize>() as u32,
binary_mtime: mtime,
binary_len: len + 1,
entries,
};
let bytes = rkyv::to_bytes::<_, 4096>(&shard).unwrap();
std::fs::write(&path, &bytes).unwrap();
assert_eq!(DeparseCache::open(&path).get(&[3u8; 32]), None);
}
}