use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use padlock_core::ir::StructLayout;
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct CacheStore {
entries: HashMap<String, CacheEntry>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CacheEntry {
mtime_secs: u64,
layouts: Vec<StructLayout>,
}
pub struct ParseCache {
store: CacheStore,
cache_path: PathBuf,
dirty: bool,
}
impl ParseCache {
pub fn load() -> Self {
let cache_path = PathBuf::from(".padlock-cache").join("layouts.json");
let store = std::fs::read(&cache_path)
.ok()
.and_then(|data| serde_json::from_slice(&data).ok())
.unwrap_or_default();
ParseCache {
store,
cache_path,
dirty: false,
}
}
pub fn get(&self, path: &Path) -> Option<Vec<StructLayout>> {
let mtime = file_mtime(path)?;
let key = path.to_string_lossy().into_owned();
let entry = self.store.entries.get(&key)?;
if entry.mtime_secs == mtime {
Some(entry.layouts.clone())
} else {
None
}
}
pub fn insert(&mut self, path: &Path, layouts: Vec<StructLayout>) {
let Some(mtime) = file_mtime(path) else {
return;
};
let key = path.to_string_lossy().into_owned();
self.store.entries.insert(
key,
CacheEntry {
mtime_secs: mtime,
layouts,
},
);
self.dirty = true;
}
pub fn flush(&self) {
if !self.dirty {
return;
}
if let Some(dir) = self.cache_path.parent()
&& std::fs::create_dir_all(dir).is_err()
{
return;
}
if let Ok(json) = serde_json::to_string(&self.store) {
let _ = std::fs::write(&self.cache_path, json);
}
}
}
fn file_mtime(path: &Path) -> Option<u64> {
path.metadata()
.ok()?
.modified()
.ok()?
.duration_since(UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}
#[cfg(test)]
mod tests {
use super::*;
use padlock_core::arch::X86_64_SYSV;
use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
use std::fs;
use tempfile::TempDir;
fn simple_layout() -> StructLayout {
StructLayout {
name: "Foo".to_string(),
total_size: 8,
align: 8,
fields: vec![Field {
name: "x".to_string(),
ty: TypeInfo::Primitive {
name: "u64".to_string(),
size: 8,
align: 8,
},
offset: 0,
size: 8,
align: 8,
source_file: None,
source_line: None,
access: AccessPattern::Unknown,
}],
source_file: None,
source_line: None,
arch: &X86_64_SYSV,
is_packed: false,
is_union: false,
is_repr_rust: false,
suppressed_findings: Vec::new(),
uncertain_fields: Vec::new(),
}
}
#[test]
fn cache_miss_on_fresh_cache() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("a.rs");
fs::write(&src, "struct A { x: u64 }").unwrap();
let cache_path = dir.path().join(".padlock-cache").join("layouts.json");
let cache = ParseCache {
store: CacheStore::default(),
cache_path,
dirty: false,
};
assert!(cache.get(&src).is_none());
}
#[test]
fn cache_hit_after_insert_and_flush() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("b.rs");
fs::write(&src, "struct B { x: u64 }").unwrap();
let cache_path = dir.path().join(".padlock-cache").join("layouts.json");
{
let mut cache = ParseCache {
store: CacheStore::default(),
cache_path: cache_path.clone(),
dirty: false,
};
cache.insert(&src, vec![simple_layout()]);
cache.flush();
}
let store: CacheStore = serde_json::from_slice(&fs::read(&cache_path).unwrap()).unwrap();
let reload = ParseCache {
store,
cache_path,
dirty: false,
};
let result = reload.get(&src);
assert!(result.is_some());
let layouts = result.unwrap();
assert_eq!(layouts.len(), 1);
assert_eq!(layouts[0].name, "Foo");
}
#[test]
fn cache_miss_when_file_modified() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("c.rs");
fs::write(&src, "struct C { x: u64 }").unwrap();
let cache_path = dir.path().join(".padlock-cache").join("layouts.json");
let key = src.to_string_lossy().into_owned();
let mut store = CacheStore::default();
store.entries.insert(
key,
CacheEntry {
mtime_secs: 0, layouts: vec![simple_layout()],
},
);
let cache = ParseCache {
store,
cache_path,
dirty: false,
};
assert!(cache.get(&src).is_none());
}
#[test]
fn cache_flush_is_idempotent_when_not_dirty() {
let dir = TempDir::new().unwrap();
let cache_path = dir.path().join(".padlock-cache").join("layouts.json");
let cache = ParseCache {
store: CacheStore::default(),
cache_path: cache_path.clone(),
dirty: false,
};
cache.flush(); assert!(!cache_path.exists(), "no file written when not dirty");
}
}