use crate::class_file::ClassFile;
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
const CACHE_MAGIC: u32 = 0x4A564D43; const CACHE_VERSION: u32 = 2;
fn fnv1a_32(bytes: &[u8]) -> u32 {
const FNV_OFFSET_BASIS: u32 = 0x811c_9dc5;
const FNV_PRIME: u32 = 0x0100_0193;
let mut hash = FNV_OFFSET_BASIS;
for &b in bytes {
hash ^= b as u32;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct CacheHeader {
version: u32,
checksum: u32,
mtime_sec: u64,
mtime_nano: u32,
data_len: u32,
}
impl CacheHeader {
fn write_to<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
writer.write_u32::<BE>(CACHE_MAGIC)?;
writer.write_u32::<BE>(self.version)?;
writer.write_u32::<BE>(self.checksum)?;
writer.write_u64::<BE>(self.mtime_sec)?;
writer.write_u32::<BE>(self.mtime_nano)?;
writer.write_u32::<BE>(self.data_len)?;
Ok(())
}
fn read_from<R: Read>(reader: &mut R) -> std::io::Result<Option<Self>> {
let magic = reader.read_u32::<BE>()?;
if magic != CACHE_MAGIC {
return Ok(None);
}
let version = reader.read_u32::<BE>()?;
if version == 1 {
let data_len = reader.read_u32::<BE>()?;
return Ok(Some(CacheHeader {
version: 1,
checksum: 0,
mtime_sec: 0,
mtime_nano: 0,
data_len,
}));
}
if version != 2 {
return Ok(None); }
let checksum = reader.read_u32::<BE>()?;
let mtime_sec = reader.read_u64::<BE>()?;
let mtime_nano = reader.read_u32::<BE>()?;
let data_len = reader.read_u32::<BE>()?;
Ok(Some(CacheHeader {
version: 2,
checksum,
mtime_sec,
mtime_nano,
data_len,
}))
}
}
fn source_metadata(path: &Path) -> std::io::Result<(u32, u64, u32)> {
let bytes = std::fs::read(path)?;
let checksum = fnv1a_32(&bytes);
let meta = std::fs::metadata(path)?;
let mtime = meta.modified()?;
let duration = mtime.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
Ok((checksum, duration.as_secs(), duration.subsec_nanos()))
}
pub fn write_to_cache(
cache_dir: &Path,
class_name: &str,
source_path: &Path,
) -> std::io::Result<PathBuf> {
std::fs::create_dir_all(cache_dir)?;
let safe_name = class_name.replace('.', "_").replace('/', "_");
let path = cache_dir.join(format!("{}.jvmc", safe_name));
let bytes = std::fs::read(source_path)?;
let (checksum, mtime_sec, mtime_nano) = source_metadata(source_path)?;
let header = CacheHeader {
version: CACHE_VERSION,
checksum,
mtime_sec,
mtime_nano,
data_len: bytes.len() as u32,
};
let mut f = std::fs::File::create(&path)?;
header.write_to(&mut f)?;
f.write_all(&bytes)?;
Ok(path)
}
pub fn read_from_cache(
cache_dir: &Path,
class_name: &str,
source_path: &Path,
) -> std::io::Result<Option<ClassFile>> {
let safe_name = class_name.replace('.', "_").replace('/', "_");
let path = cache_dir.join(format!("{}.jvmc", safe_name));
if !path.exists() {
return Ok(None);
}
let mut f = std::fs::File::open(&path)?;
let Some(header) = CacheHeader::read_from(&mut f)? else {
return Ok(None);
};
if header.version == 2 {
if source_path.exists() {
let expected = source_metadata(source_path)?;
if header.checksum != expected.0
|| header.mtime_sec != expected.1
|| header.mtime_nano != expected.2
{
return Ok(None);
}
} else {
return Ok(None);
}
}
let len = header.data_len as usize;
let mut buf = vec![0u8; len];
f.read_exact(&mut buf)?;
ClassFile::parse(&buf)
.map(Some)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{:?}", e)))
}
pub fn find_source_file(classpath: &[PathBuf], class_name: &str) -> Option<PathBuf> {
let class_file_name = format!("{}.class", class_name.replace('.', "/"));
for dir in classpath {
let path = dir.join(&class_file_name);
if path.exists() {
return Some(path);
}
}
None
}
pub fn default_cache_dir() -> PathBuf {
std::env::temp_dir().join("jvmrs_class_cache")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn make_test_class_bytes() -> Vec<u8> {
let mut bytes = vec![];
bytes.extend_from_slice(&0xCAFEBABE_u32.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&1u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes.extend_from_slice(&0u16.to_be_bytes()); bytes
}
#[test]
fn test_cache_roundtrip_valid() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let source_path = tmp.path().join("Test.class");
let bytes = make_test_class_bytes();
std::fs::write(&source_path, &bytes).unwrap();
write_to_cache(&cache_dir, "Test", &source_path).unwrap();
let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
assert!(result.is_some());
}
#[test]
fn test_cache_stale_after_source_modified() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let source_path = tmp.path().join("Test.class");
let bytes = make_test_class_bytes();
std::fs::write(&source_path, &bytes).unwrap();
write_to_cache(&cache_dir, "Test", &source_path).unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let mut bytes2 = bytes.clone();
bytes2.push(0);
std::fs::write(&source_path, &bytes2).unwrap();
let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
assert!(result.is_none(), "cache should be stale after source modification");
}
#[test]
fn test_cache_corrupted_magic() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let source_path = tmp.path().join("Test.class");
let bytes = make_test_class_bytes();
std::fs::write(&source_path, &bytes).unwrap();
write_to_cache(&cache_dir, "Test", &source_path).unwrap();
let cache_path = cache_dir.join("Test.jvmc");
let mut data = std::fs::read(&cache_path).unwrap();
data[0] = 0xFF; std::fs::write(&cache_path, &data).unwrap();
let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
assert!(result.is_none(), "corrupted magic should invalidate cache");
}
#[test]
fn test_cache_missing_source() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let source_path = tmp.path().join("Test.class");
let bytes = make_test_class_bytes();
std::fs::write(&source_path, &bytes).unwrap();
write_to_cache(&cache_dir, "Test", &source_path).unwrap();
std::fs::remove_file(&source_path).unwrap();
let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
assert!(result.is_none(), "missing source should invalidate cache");
}
#[test]
fn test_fnv1a_32_known_value() {
assert_eq!(fnv1a_32(b"hello"), 0x4f9f2cab);
}
#[test]
fn test_cache_v1_backward_compat() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let source_path = tmp.path().join("Test.class");
let bytes = make_test_class_bytes();
std::fs::write(&source_path, &bytes).unwrap();
std::fs::create_dir_all(&cache_dir).unwrap();
let cache_path = cache_dir.join("Test.jvmc");
let mut f = std::fs::File::create(&cache_path).unwrap();
f.write_u32::<BE>(CACHE_MAGIC).unwrap();
f.write_u32::<BE>(1).unwrap(); f.write_u32::<BE>(bytes.len() as u32).unwrap();
f.write_all(&bytes).unwrap();
drop(f);
let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
assert!(result.is_some(), "v1 cache should be readable for backward compat");
}
}