use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use mir_codebase::storage::{deduplicate_params_in_slice, StubSlice};
use serde::{Deserialize, Serialize};
const MAGIC: u32 = 0x0152_494D;
const FORMAT_VERSION: u8 = 2;
#[derive(Serialize, Deserialize)]
struct Header {
magic: u32,
mir_version: u64,
format_version: u8,
php_version: u8,
content_hash: [u8; 32],
}
fn mir_version_hash() -> u64 {
use std::sync::OnceLock;
static HASH: OnceLock<u64> = OnceLock::new();
*HASH.get_or_init(|| {
let digest = blake3::hash(env!("CARGO_PKG_VERSION").as_bytes());
let bytes = digest.as_bytes();
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
})
}
pub struct StubSliceCache {
root: PathBuf,
hits: AtomicU64,
misses: AtomicU64,
writes: AtomicU64,
enabled: bool,
}
impl StubSliceCache {
pub fn open(cache_dir: &Path) -> Self {
let root = cache_dir.join("stubs");
let enabled = std::fs::create_dir_all(&root).is_ok();
Self {
root,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
writes: AtomicU64::new(0),
enabled,
}
}
fn shard_path(&self, path: &str) -> PathBuf {
let digest = blake3::hash(path.as_bytes());
let hex = digest.to_hex();
let s = hex.as_str();
self.root.join(&s[..2]).join(format!("{}.bin", s))
}
pub fn get(&self, path: &str, content_hash: &[u8; 32], php_version: u8) -> Option<StubSlice> {
if !self.enabled {
return None;
}
let entry_path = self.shard_path(path);
let bytes = std::fs::read(&entry_path).ok()?;
let mut cursor = Cursor::new(&bytes);
let header: Header = bincode::deserialize_from(&mut cursor).ok()?;
if header.magic != MAGIC
|| header.format_version != FORMAT_VERSION
|| header.mir_version != mir_version_hash()
|| header.php_version != php_version
|| &header.content_hash != content_hash
{
self.misses.fetch_add(1, Ordering::Relaxed);
return None;
}
match bincode::deserialize_from::<_, StubSlice>(&mut cursor) {
Ok(mut slice) => {
slice.file = Some(std::sync::Arc::from(path));
self.hits.fetch_add(1, Ordering::Relaxed);
Some(slice)
}
Err(_) => {
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
}
pub fn put(&self, path: &str, content_hash: &[u8; 32], php_version: u8, slice: &StubSlice) {
if !self.enabled {
return;
}
let entry_path = self.shard_path(path);
let Some(shard_dir) = entry_path.parent() else {
return;
};
if std::fs::create_dir_all(shard_dir).is_err() {
return;
}
let header = Header {
magic: MAGIC,
mir_version: mir_version_hash(),
format_version: FORMAT_VERSION,
php_version,
content_hash: *content_hash,
};
let mut buf = match bincode::serialize(&header) {
Ok(b) => b,
Err(_) => return,
};
let mut slice_for_disk = slice.clone();
slice_for_disk.file = None;
match bincode::serialize(&slice_for_disk) {
Ok(body) => buf.extend_from_slice(&body),
Err(_) => return,
}
let tmp = entry_path.with_extension(format!(
"tmp.{}.{}",
std::process::id(),
self.writes.fetch_add(1, Ordering::Relaxed),
));
if std::fs::write(&tmp, &buf).is_err() {
return;
}
let _ = std::fs::rename(&tmp, &entry_path);
}
pub fn hits(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
pub fn misses(&self) -> u64 {
self.misses.load(Ordering::Relaxed)
}
}
pub fn hash_source(source: &str) -> [u8; 32] {
*blake3::hash(source.as_bytes()).as_bytes()
}
pub fn prepare_for_ingest(slice: &mut StubSlice) {
if !slice.is_deduped {
deduplicate_params_in_slice(slice);
}
}
#[cfg(test)]
mod tests {
use super::*;
use mir_codebase::storage::StubSlice;
use tempfile::TempDir;
fn make_cache() -> (TempDir, StubSliceCache) {
let dir = TempDir::new().unwrap();
let cache = StubSliceCache::open(dir.path());
(dir, cache)
}
#[test]
fn roundtrip_returns_equivalent_slice() {
let (_dir, cache) = make_cache();
let hash = hash_source("<?php class A {}");
let slice = StubSlice::default();
cache.put("/x/a.php", &hash, 8, &slice);
let got = cache.get("/x/a.php", &hash, 8).expect("hit");
assert_eq!(
got.file.as_deref().map(|s| s.to_string()),
Some("/x/a.php".to_string())
);
assert_eq!(cache.hits(), 1);
}
#[test]
fn miss_on_content_hash_mismatch() {
let (_dir, cache) = make_cache();
let hash_a = hash_source("a");
let hash_b = hash_source("b");
cache.put("/x/a.php", &hash_a, 8, &StubSlice::default());
assert!(cache.get("/x/a.php", &hash_b, 8).is_none());
}
#[test]
fn miss_on_php_version_mismatch() {
let (_dir, cache) = make_cache();
let hash = hash_source("a");
cache.put("/x/a.php", &hash, 8, &StubSlice::default());
assert!(cache.get("/x/a.php", &hash, 7).is_none());
}
#[test]
fn miss_on_unknown_path_does_not_error() {
let (_dir, cache) = make_cache();
assert!(cache.get("/no/such/file.php", &[0u8; 32], 8).is_none());
}
#[test]
fn restores_file_field_from_path_not_disk() {
let (_dir, cache) = make_cache();
let hash = hash_source("a");
let slice = StubSlice {
file: Some(std::sync::Arc::from("/different/path.php")),
..Default::default()
};
cache.put("/x/a.php", &hash, 8, &slice);
let got = cache.get("/x/a.php", &hash, 8).unwrap();
assert_eq!(
got.file.as_deref().map(|s| s.to_string()),
Some("/x/a.php".to_string())
);
}
#[test]
fn corrupt_entry_is_treated_as_miss() {
let (dir, cache) = make_cache();
let hash = hash_source("a");
cache.put("/x/a.php", &hash, 8, &StubSlice::default());
let digest = blake3::hash("/x/a.php".as_bytes()).to_hex();
let s = digest.as_str();
let bad = dir
.path()
.join("stubs")
.join(&s[..2])
.join(format!("{}.bin", s));
std::fs::write(&bad, b"not a header").unwrap();
assert!(cache.get("/x/a.php", &hash, 8).is_none());
}
}