use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use sha2::{Digest, Sha256};
use crate::error::XfaError;
const KEY_ENV: &[&str] = &[
"XFA_JS_EXECUTION_MODE",
"XFA_JS_HARVEST_MODE",
"XFA_SUPPRESSION_TRUST_LAYOUT",
"XFA_FORMDOM_ADMIT_DATABOUND",
"XFA_PRESENCE_PROV",
];
pub const DEFAULT_MAX_ENTRIES: usize = 16;
pub const DEFAULT_MAX_BYTES: usize = 128 * 1024 * 1024;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct FlattenKey([u8; 32]);
impl FlattenKey {
pub fn for_bytes(pdf_bytes: &[u8]) -> Self {
Self::for_bytes_with_env(pdf_bytes, |name| std::env::var(name).ok())
}
pub fn for_bytes_with_env(pdf_bytes: &[u8], env: impl Fn(&str) -> Option<String>) -> Self {
let mut h = Sha256::new();
for name in KEY_ENV {
h.update(name.as_bytes());
h.update([0]);
if let Some(v) = env(name) {
h.update(v.as_bytes());
}
h.update([1]);
}
h.update((pdf_bytes.len() as u64).to_le_bytes());
h.update(pdf_bytes);
Self(h.finalize().into())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct FlattenCacheStats {
pub hits: u64,
pub misses: u64,
pub entries: usize,
pub bytes: usize,
}
struct Entry {
bytes: Arc<Vec<u8>>,
last_used: u64,
}
#[derive(Default)]
struct Inner {
map: HashMap<FlattenKey, Entry>,
tick: u64,
total_bytes: usize,
hits: u64,
misses: u64,
}
pub struct FlattenCache {
max_entries: usize,
max_bytes: usize,
inner: Mutex<Inner>,
}
impl FlattenCache {
pub fn new(max_entries: usize, max_bytes: usize) -> Self {
Self {
max_entries,
max_bytes,
inner: Mutex::new(Inner::default()),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(|p| p.into_inner())
}
pub fn get_or_flatten<F>(&self, key: FlattenKey, flatten: F) -> Result<Arc<Vec<u8>>, XfaError>
where
F: FnOnce() -> Result<Vec<u8>, XfaError>,
{
{
let mut g = self.lock();
g.tick += 1;
let tick = g.tick;
if let Some(e) = g.map.get_mut(&key) {
e.last_used = tick;
let hit = Arc::clone(&e.bytes);
g.hits += 1;
return Ok(hit);
}
g.misses += 1;
}
let out = Arc::new(flatten()?);
if out.len() <= self.max_bytes {
let mut g = self.lock();
g.tick += 1;
let tick = g.tick;
if let Some(old) = g.map.insert(
key,
Entry {
bytes: Arc::clone(&out),
last_used: tick,
},
) {
g.total_bytes -= old.bytes.len();
}
g.total_bytes += out.len();
while g.map.len() > self.max_entries || g.total_bytes > self.max_bytes {
let Some(victim) = g
.map
.iter()
.filter(|(k, _)| **k != key)
.min_by_key(|(_, e)| e.last_used)
.map(|(k, _)| *k)
else {
break;
};
if let Some(e) = g.map.remove(&victim) {
g.total_bytes -= e.bytes.len();
}
}
}
Ok(out)
}
pub fn invalidate(&self, key: &FlattenKey) -> bool {
let mut g = self.lock();
match g.map.remove(key) {
Some(e) => {
g.total_bytes -= e.bytes.len();
true
}
None => false,
}
}
pub fn clear(&self) {
let mut g = self.lock();
g.map.clear();
g.total_bytes = 0;
}
pub fn stats(&self) -> FlattenCacheStats {
let g = self.lock();
FlattenCacheStats {
hits: g.hits,
misses: g.misses,
entries: g.map.len(),
bytes: g.total_bytes,
}
}
}
fn global() -> &'static FlattenCache {
static CACHE: OnceLock<FlattenCache> = OnceLock::new();
CACHE.get_or_init(|| FlattenCache::new(DEFAULT_MAX_ENTRIES, DEFAULT_MAX_BYTES))
}
pub fn flatten_cached(pdf_bytes: &[u8]) -> Result<Arc<Vec<u8>>, XfaError> {
global().get_or_flatten(FlattenKey::for_bytes(pdf_bytes), || {
crate::flatten::flatten_xfa_to_pdf(pdf_bytes)
})
}
pub fn invalidate(pdf_bytes: &[u8]) -> bool {
global().invalidate(&FlattenKey::for_bytes(pdf_bytes))
}
pub fn clear() {
global().clear();
}
pub fn stats() -> FlattenCacheStats {
global().stats()
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
fn key(n: u8) -> FlattenKey {
FlattenKey::for_bytes(&[n; 8])
}
#[test]
fn second_lookup_is_a_hit_and_does_not_run_the_flatten() {
let c = FlattenCache::new(4, 1 << 20);
let runs = Cell::new(0);
let f = || {
runs.set(runs.get() + 1);
Ok(vec![1, 2, 3])
};
let a = c.get_or_flatten(key(1), f).unwrap();
let b = c.get_or_flatten(key(1), f).unwrap();
assert_eq!(runs.get(), 1);
assert!(Arc::ptr_eq(&a, &b));
let s = c.stats();
assert_eq!((s.hits, s.misses, s.entries, s.bytes), (1, 1, 1, 3));
}
#[test]
fn changed_input_is_a_different_key() {
let mut bytes = vec![7u8; 4096];
let before = FlattenKey::for_bytes(&bytes);
bytes[2048] ^= 1;
assert_ne!(before, FlattenKey::for_bytes(&bytes));
assert_ne!(
FlattenKey::for_bytes(&[1, 2, 3]),
FlattenKey::for_bytes(&[1, 2, 4])
);
}
#[test]
fn explicit_invalidation_forces_a_new_flatten() {
let c = FlattenCache::new(4, 1 << 20);
let runs = Cell::new(0);
let f = || {
runs.set(runs.get() + 1);
Ok(vec![9])
};
c.get_or_flatten(key(2), f).unwrap();
assert!(c.invalidate(&key(2)));
assert!(!c.invalidate(&key(2)));
assert_eq!(c.stats().bytes, 0);
c.get_or_flatten(key(2), f).unwrap();
assert_eq!(runs.get(), 2);
}
#[test]
fn errors_are_not_cached() {
let c = FlattenCache::new(4, 1 << 20);
let r = c.get_or_flatten(key(3), || Err(XfaError::LayoutFailed("boom".into())));
assert!(r.is_err());
assert_eq!(c.stats().entries, 0);
let ok = c.get_or_flatten(key(3), || Ok(vec![1])).unwrap();
assert_eq!(ok.as_slice(), &[1]);
}
#[test]
fn evicts_least_recently_used_by_count() {
let c = FlattenCache::new(2, 1 << 20);
c.get_or_flatten(key(1), || Ok(vec![1])).unwrap();
c.get_or_flatten(key(2), || Ok(vec![2])).unwrap();
c.get_or_flatten(key(1), || Ok(vec![0])).unwrap();
c.get_or_flatten(key(3), || Ok(vec![3])).unwrap();
let runs = Cell::new(0);
let f = |v: u8| {
let runs = &runs;
move || {
runs.set(runs.get() + 1);
Ok(vec![v])
}
};
c.get_or_flatten(key(1), f(1)).unwrap();
c.get_or_flatten(key(3), f(3)).unwrap();
assert_eq!(runs.get(), 0, "1 and 3 must still be cached");
c.get_or_flatten(key(2), f(2)).unwrap();
assert_eq!(runs.get(), 1, "2 must have been evicted");
}
#[test]
fn byte_budget_bounds_total_and_skips_oversized_entries() {
let c = FlattenCache::new(10, 10);
c.get_or_flatten(key(1), || Ok(vec![0; 6])).unwrap();
c.get_or_flatten(key(2), || Ok(vec![0; 6])).unwrap();
let s = c.stats();
assert!(s.bytes <= 10, "budget exceeded: {}", s.bytes);
assert_eq!(s.entries, 1);
let big = c.get_or_flatten(key(3), || Ok(vec![0; 64])).unwrap();
assert_eq!(big.len(), 64);
assert!(c.stats().bytes <= 10);
}
#[test]
fn every_environment_switch_changes_the_key() {
let bytes = [5u8; 32];
let base = FlattenKey::for_bytes_with_env(&bytes, |_| None);
for name in KEY_ENV {
let with = FlattenKey::for_bytes_with_env(&bytes, |n| {
(n == *name).then(|| "sandboxed".to_string())
});
assert_ne!(base, with, "{name} does not reach the key");
}
let a =
FlattenKey::for_bytes_with_env(&bytes, |n| (n == KEY_ENV[0]).then(|| "1".to_string()));
let b =
FlattenKey::for_bytes_with_env(&bytes, |n| (n == KEY_ENV[1]).then(|| "1".to_string()));
assert_ne!(a, b);
}
}