1use std::collections::HashMap;
29use std::sync::{Arc, Mutex, OnceLock};
30
31use sha2::{Digest, Sha256};
32
33use crate::error::XfaError;
34
35const KEY_ENV: &[&str] = &[
39 "XFA_JS_EXECUTION_MODE",
40 "XFA_JS_HARVEST_MODE",
41 "XFA_SUPPRESSION_TRUST_LAYOUT",
42 "XFA_FORMDOM_ADMIT_DATABOUND",
43 "XFA_PRESENCE_PROV",
44];
45
46pub const DEFAULT_MAX_ENTRIES: usize = 16;
48pub const DEFAULT_MAX_BYTES: usize = 128 * 1024 * 1024;
50
51#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
53pub struct FlattenKey([u8; 32]);
54
55impl FlattenKey {
56 pub fn for_bytes(pdf_bytes: &[u8]) -> Self {
58 Self::for_bytes_with_env(pdf_bytes, |name| std::env::var(name).ok())
59 }
60
61 pub fn for_bytes_with_env(pdf_bytes: &[u8], env: impl Fn(&str) -> Option<String>) -> Self {
64 let mut h = Sha256::new();
65 for name in KEY_ENV {
66 h.update(name.as_bytes());
67 h.update([0]);
68 if let Some(v) = env(name) {
69 h.update(v.as_bytes());
70 }
71 h.update([1]);
72 }
73 h.update((pdf_bytes.len() as u64).to_le_bytes());
74 h.update(pdf_bytes);
75 Self(h.finalize().into())
76 }
77}
78
79#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
81pub struct FlattenCacheStats {
82 pub hits: u64,
84 pub misses: u64,
86 pub entries: usize,
88 pub bytes: usize,
90}
91
92struct Entry {
93 bytes: Arc<Vec<u8>>,
94 last_used: u64,
95}
96
97#[derive(Default)]
98struct Inner {
99 map: HashMap<FlattenKey, Entry>,
100 tick: u64,
101 total_bytes: usize,
102 hits: u64,
103 misses: u64,
104}
105
106pub struct FlattenCache {
110 max_entries: usize,
111 max_bytes: usize,
112 inner: Mutex<Inner>,
113}
114
115impl FlattenCache {
116 pub fn new(max_entries: usize, max_bytes: usize) -> Self {
118 Self {
119 max_entries,
120 max_bytes,
121 inner: Mutex::new(Inner::default()),
122 }
123 }
124
125 fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
126 self.inner.lock().unwrap_or_else(|p| p.into_inner())
130 }
131
132 pub fn get_or_flatten<F>(&self, key: FlattenKey, flatten: F) -> Result<Arc<Vec<u8>>, XfaError>
137 where
138 F: FnOnce() -> Result<Vec<u8>, XfaError>,
139 {
140 {
141 let mut g = self.lock();
142 g.tick += 1;
143 let tick = g.tick;
144 if let Some(e) = g.map.get_mut(&key) {
145 e.last_used = tick;
146 let hit = Arc::clone(&e.bytes);
147 g.hits += 1;
148 return Ok(hit);
149 }
150 g.misses += 1;
151 }
152
153 let out = Arc::new(flatten()?);
154
155 if out.len() <= self.max_bytes {
156 let mut g = self.lock();
157 g.tick += 1;
158 let tick = g.tick;
159 if let Some(old) = g.map.insert(
160 key,
161 Entry {
162 bytes: Arc::clone(&out),
163 last_used: tick,
164 },
165 ) {
166 g.total_bytes -= old.bytes.len();
167 }
168 g.total_bytes += out.len();
169 while g.map.len() > self.max_entries || g.total_bytes > self.max_bytes {
170 let Some(victim) = g
171 .map
172 .iter()
173 .filter(|(k, _)| **k != key)
174 .min_by_key(|(_, e)| e.last_used)
175 .map(|(k, _)| *k)
176 else {
177 break;
178 };
179 if let Some(e) = g.map.remove(&victim) {
180 g.total_bytes -= e.bytes.len();
181 }
182 }
183 }
184 Ok(out)
185 }
186
187 pub fn invalidate(&self, key: &FlattenKey) -> bool {
189 let mut g = self.lock();
190 match g.map.remove(key) {
191 Some(e) => {
192 g.total_bytes -= e.bytes.len();
193 true
194 }
195 None => false,
196 }
197 }
198
199 pub fn clear(&self) {
201 let mut g = self.lock();
202 g.map.clear();
203 g.total_bytes = 0;
204 }
205
206 pub fn stats(&self) -> FlattenCacheStats {
208 let g = self.lock();
209 FlattenCacheStats {
210 hits: g.hits,
211 misses: g.misses,
212 entries: g.map.len(),
213 bytes: g.total_bytes,
214 }
215 }
216}
217
218fn global() -> &'static FlattenCache {
219 static CACHE: OnceLock<FlattenCache> = OnceLock::new();
220 CACHE.get_or_init(|| FlattenCache::new(DEFAULT_MAX_ENTRIES, DEFAULT_MAX_BYTES))
221}
222
223pub fn flatten_cached(pdf_bytes: &[u8]) -> Result<Arc<Vec<u8>>, XfaError> {
228 global().get_or_flatten(FlattenKey::for_bytes(pdf_bytes), || {
229 crate::flatten::flatten_xfa_to_pdf(pdf_bytes)
230 })
231}
232
233pub fn invalidate(pdf_bytes: &[u8]) -> bool {
238 global().invalidate(&FlattenKey::for_bytes(pdf_bytes))
239}
240
241pub fn clear() {
243 global().clear();
244}
245
246pub fn stats() -> FlattenCacheStats {
248 global().stats()
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use std::cell::Cell;
255
256 fn key(n: u8) -> FlattenKey {
257 FlattenKey::for_bytes(&[n; 8])
258 }
259
260 #[test]
261 fn second_lookup_is_a_hit_and_does_not_run_the_flatten() {
262 let c = FlattenCache::new(4, 1 << 20);
263 let runs = Cell::new(0);
264 let f = || {
265 runs.set(runs.get() + 1);
266 Ok(vec![1, 2, 3])
267 };
268 let a = c.get_or_flatten(key(1), f).unwrap();
269 let b = c.get_or_flatten(key(1), f).unwrap();
270 assert_eq!(runs.get(), 1);
271 assert!(Arc::ptr_eq(&a, &b));
272 let s = c.stats();
273 assert_eq!((s.hits, s.misses, s.entries, s.bytes), (1, 1, 1, 3));
274 }
275
276 #[test]
277 fn changed_input_is_a_different_key() {
278 let mut bytes = vec![7u8; 4096];
280 let before = FlattenKey::for_bytes(&bytes);
281 bytes[2048] ^= 1;
282 assert_ne!(before, FlattenKey::for_bytes(&bytes));
283 assert_ne!(
286 FlattenKey::for_bytes(&[1, 2, 3]),
287 FlattenKey::for_bytes(&[1, 2, 4])
288 );
289 }
290
291 #[test]
292 fn explicit_invalidation_forces_a_new_flatten() {
293 let c = FlattenCache::new(4, 1 << 20);
294 let runs = Cell::new(0);
295 let f = || {
296 runs.set(runs.get() + 1);
297 Ok(vec![9])
298 };
299 c.get_or_flatten(key(2), f).unwrap();
300 assert!(c.invalidate(&key(2)));
301 assert!(!c.invalidate(&key(2)));
302 assert_eq!(c.stats().bytes, 0);
303 c.get_or_flatten(key(2), f).unwrap();
304 assert_eq!(runs.get(), 2);
305 }
306
307 #[test]
308 fn errors_are_not_cached() {
309 let c = FlattenCache::new(4, 1 << 20);
310 let r = c.get_or_flatten(key(3), || Err(XfaError::LayoutFailed("boom".into())));
311 assert!(r.is_err());
312 assert_eq!(c.stats().entries, 0);
313 let ok = c.get_or_flatten(key(3), || Ok(vec![1])).unwrap();
314 assert_eq!(ok.as_slice(), &[1]);
315 }
316
317 #[test]
318 fn evicts_least_recently_used_by_count() {
319 let c = FlattenCache::new(2, 1 << 20);
320 c.get_or_flatten(key(1), || Ok(vec![1])).unwrap();
321 c.get_or_flatten(key(2), || Ok(vec![2])).unwrap();
322 c.get_or_flatten(key(1), || Ok(vec![0])).unwrap();
324 c.get_or_flatten(key(3), || Ok(vec![3])).unwrap();
325 let runs = Cell::new(0);
326 let f = |v: u8| {
327 let runs = &runs;
328 move || {
329 runs.set(runs.get() + 1);
330 Ok(vec![v])
331 }
332 };
333 c.get_or_flatten(key(1), f(1)).unwrap();
334 c.get_or_flatten(key(3), f(3)).unwrap();
335 assert_eq!(runs.get(), 0, "1 and 3 must still be cached");
336 c.get_or_flatten(key(2), f(2)).unwrap();
337 assert_eq!(runs.get(), 1, "2 must have been evicted");
338 }
339
340 #[test]
341 fn byte_budget_bounds_total_and_skips_oversized_entries() {
342 let c = FlattenCache::new(10, 10);
343 c.get_or_flatten(key(1), || Ok(vec![0; 6])).unwrap();
344 c.get_or_flatten(key(2), || Ok(vec![0; 6])).unwrap();
345 let s = c.stats();
346 assert!(s.bytes <= 10, "budget exceeded: {}", s.bytes);
347 assert_eq!(s.entries, 1);
348 let big = c.get_or_flatten(key(3), || Ok(vec![0; 64])).unwrap();
350 assert_eq!(big.len(), 64);
351 assert!(c.stats().bytes <= 10);
352 }
353
354 #[test]
355 fn every_environment_switch_changes_the_key() {
356 let bytes = [5u8; 32];
357 let base = FlattenKey::for_bytes_with_env(&bytes, |_| None);
358 for name in KEY_ENV {
359 let with = FlattenKey::for_bytes_with_env(&bytes, |n| {
360 (n == *name).then(|| "sandboxed".to_string())
361 });
362 assert_ne!(base, with, "{name} does not reach the key");
363 }
364 let a =
366 FlattenKey::for_bytes_with_env(&bytes, |n| (n == KEY_ENV[0]).then(|| "1".to_string()));
367 let b =
368 FlattenKey::for_bytes_with_env(&bytes, |n| (n == KEY_ENV[1]).then(|| "1".to_string()));
369 assert_ne!(a, b);
370 }
371}