use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
pub struct CacheEntry {
pub bytecode: Vec<u8>,
pub module_name: String,
pub source_map_json: Option<String>,
pub aux: Option<String>,
pub inputs: Vec<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct BytecodeCache {
dir: Option<PathBuf>,
}
impl BytecodeCache {
#[must_use]
pub fn disabled() -> Self {
Self { dir: None }
}
#[must_use]
pub fn at(base: impl AsRef<Path>) -> Self {
let dir = base.as_ref().join("bytecode").join(abi_tag());
match std::fs::create_dir_all(&dir) {
Ok(()) => Self { dir: Some(dir) },
Err(_) => Self::disabled(),
}
}
#[must_use]
pub fn for_app(app: &str) -> Self {
let base = user_cache_base().unwrap_or_else(std::env::temp_dir).join(app);
Self::at(base)
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.dir.is_some()
}
#[must_use]
pub fn dir(&self) -> Option<&Path> {
self.dir.as_deref()
}
fn paths(&self, key: u64) -> Option<(PathBuf, PathBuf)> {
let dir = self.dir.as_deref()?;
let hex = format!("{key:016x}");
Some((dir.join(format!("{hex}.bin")), dir.join(format!("{hex}.json"))))
}
}
#[must_use]
pub fn abi_tag() -> &'static str {
static TAG: OnceLock<String> = OnceLock::new();
TAG.get_or_init(|| {
#[allow(unsafe_code)]
let qjs = unsafe { std::ffi::CStr::from_ptr(rquickjs::qjs::JS_GetVersion()) }
.to_str()
.unwrap_or("unknown");
let endian = if cfg!(target_endian = "big") { "be" } else { "le" };
format!(
"fjbc1-v{}-qjs{qjs}-{}-{endian}-p{}",
env!("CARGO_PKG_VERSION"),
std::env::consts::ARCH,
std::mem::size_of::<usize>() * 8,
)
})
}
fn user_cache_base() -> Option<PathBuf> {
if let Some(x) = std::env::var_os("XDG_CACHE_HOME") {
return Some(PathBuf::from(x));
}
#[cfg(target_os = "macos")]
if let Some(h) = std::env::var_os("HOME") {
return Some(PathBuf::from(h).join("Library").join("Caches"));
}
std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache"))
}
#[must_use]
pub fn entry_key(kind: &str, entry_paths: &[PathBuf], cwd: &Path, salt: u64) -> u64 {
let mut canon: Vec<String> = entry_paths
.iter()
.map(|p| {
std::fs::canonicalize(p)
.unwrap_or_else(|_| p.clone())
.to_string_lossy()
.into_owned()
})
.collect();
canon.sort();
let mut h = std::collections::hash_map::DefaultHasher::new();
abi_tag().hash(&mut h);
kind.hash(&mut h);
salt.hash(&mut h);
std::fs::canonicalize(cwd)
.unwrap_or_else(|_| cwd.to_path_buf())
.hash(&mut h);
canon.hash(&mut h);
h.finish()
}
#[must_use]
pub fn input_set(entry_paths: &[PathBuf], modules: &[PathBuf]) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
let mut push = |p: &Path| {
let c = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
if !out.contains(&c) {
out.push(c);
}
};
for e in entry_paths {
push(e);
}
for m in modules {
if m.is_file() {
push(m);
}
}
out
}
#[must_use]
pub fn inputs_fingerprint(inputs: &[PathBuf]) -> Option<u64> {
let mut h = std::collections::hash_map::DefaultHasher::new();
for p in inputs {
p.hash(&mut h);
source_stamp(p)?.hash(&mut h);
}
Some(h.finish())
}
impl BytecodeCache {
#[must_use]
pub fn load(&self, key: u64) -> Option<CacheEntry> {
let (bin_path, _) = self.paths(key)?;
let raw = std::fs::read(bin_path).ok()?;
let mut r = Reader::new(&raw);
if r.take(4)? != BUNDLE_MAGIC {
return None;
}
let n_inputs = r.u32()? as usize;
let mut inputs = Vec::with_capacity(n_inputs);
for _ in 0..n_inputs {
let stamp = r.u64()?;
let path = PathBuf::from(std::str::from_utf8(r.slice()?).ok()?);
if source_stamp(&path)? != stamp {
return None;
}
inputs.push(path);
}
let module_name = std::str::from_utf8(r.slice()?).ok()?.to_string();
let source_map_json = r.opt_str().ok()?;
let aux = r.opt_str().ok()?;
let bytecode = r.slice()?.to_vec();
Some(CacheEntry {
bytecode,
module_name,
source_map_json,
aux,
inputs,
})
}
pub fn store(
&self,
key: u64,
bytecode: &[u8],
module_name: &str,
source_map_json: Option<&str>,
aux: Option<&str>,
inputs: &[PathBuf],
) {
let Some((bin_path, _)) = self.paths(key) else {
return;
};
let stamped: Vec<(String, u64)> = inputs
.iter()
.filter_map(|p| Some((p.to_string_lossy().into_owned(), source_stamp(p)?)))
.collect();
let mut buf = Vec::with_capacity(bytecode.len() + source_map_json.map_or(0, str::len) + 4096);
buf.extend_from_slice(BUNDLE_MAGIC);
buf.extend_from_slice(&u32::try_from(stamped.len()).unwrap_or(0).to_le_bytes());
for (path, stamp) in &stamped {
buf.extend_from_slice(&stamp.to_le_bytes());
put_slice(&mut buf, path.as_bytes());
}
put_slice(&mut buf, module_name.as_bytes());
put_opt(&mut buf, source_map_json);
put_opt(&mut buf, aux);
put_slice(&mut buf, bytecode);
let _ = atomic_write(&bin_path, &buf);
}
}
const BUNDLE_MAGIC: &[u8; 4] = b"FJB1";
fn put_slice(buf: &mut Vec<u8>, bytes: &[u8]) {
buf.extend_from_slice(&u64::try_from(bytes.len()).unwrap_or(0).to_le_bytes());
buf.extend_from_slice(bytes);
}
fn put_opt(buf: &mut Vec<u8>, value: Option<&str>) {
match value {
Some(v) => {
buf.push(1);
put_slice(buf, v.as_bytes());
},
None => buf.push(0),
}
}
struct Reader<'a> {
raw: &'a [u8],
at: usize,
}
impl<'a> Reader<'a> {
fn new(raw: &'a [u8]) -> Self {
Self { raw, at: 0 }
}
fn take(&mut self, n: usize) -> Option<&'a [u8]> {
let end = self.at.checked_add(n)?;
let out = self.raw.get(self.at..end)?;
self.at = end;
Some(out)
}
fn u32(&mut self) -> Option<u32> {
Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
}
fn u64(&mut self) -> Option<u64> {
Some(u64::from_le_bytes(self.take(8)?.try_into().ok()?))
}
fn slice(&mut self) -> Option<&'a [u8]> {
let len = usize::try_from(self.u64()?).ok()?;
self.take(len)
}
fn opt_str(&mut self) -> Result<Option<String>, ()> {
match self.take(1).ok_or(())?[0] {
0 => Ok(None),
_ => Ok(Some(
std::str::from_utf8(self.slice().ok_or(())?)
.map_err(|_| ())?
.to_string(),
)),
}
}
}
fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
std::fs::write(&tmp, bytes)?;
std::fs::rename(&tmp, path)
}
#[must_use]
pub fn source_stamp(path: &Path) -> Option<u64> {
let meta = std::fs::metadata(path).ok()?;
let mtime = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
let mtime = u64::try_from(mtime).unwrap_or(u64::MAX);
let mut h = std::collections::hash_map::DefaultHasher::new();
mtime.hash(&mut h);
meta.len().hash(&mut h);
Some(h.finish())
}