1mod procs;
46
47pub use procs::{ProcessRegistry, ProcessUsage};
48
49use std::collections::{HashMap, HashSet};
50use std::fs;
51use std::io::{Read, Seek, SeekFrom, Write};
52use std::ops::Deref;
53use std::path::{Path, PathBuf};
54use std::sync::{Arc, OnceLock};
55use std::time::{Duration, SystemTime, UNIX_EPOCH};
56
57#[derive(Debug, Clone)]
66pub struct ApkKey {
67 id: String,
68 archives: Arc<[ArchiveKey]>,
70}
71
72#[derive(Debug)]
74struct ArchiveKey {
75 label: String,
77 path: PathBuf,
78 key: ApkKey,
79}
80
81impl PartialEq for ApkKey {
82 fn eq(&self, other: &ApkKey) -> bool {
83 self.id == other.id
84 }
85}
86
87impl Eq for ApkKey {}
88
89impl std::hash::Hash for ApkKey {
90 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
91 self.id.hash(state);
92 }
93}
94
95fn is_key_char(c: char) -> bool {
97 c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'
98}
99const STEM_MAX: usize = 40;
101const HASH_HEX_LEN: usize = 16;
103
104impl ApkKey {
105 pub fn for_apk(apk: &asc_apk::Apk) -> asc_apk::Result<ApkKey> {
110 let ids = apk.directories()?;
111 let mut key = 0u64;
112 let mut archives = Vec::with_capacity(if ids.len() > 1 { ids.len() } else { 0 });
113 for (i, id) in ids.iter().enumerate() {
114 let meta = fs::metadata(id.path).map_err(|source| asc_apk::ApkError::Io { path: id.path.to_path_buf(), source })?;
115 let mtime = meta.modified().ok().and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()).map(|d| d.as_nanos()).unwrap_or(0);
116 let mut h = fnv64(id.directory);
117 h ^= meta.len().wrapping_mul(0x9E37_79B9_7F4A_7C15);
118 h ^= (mtime as u64).rotate_left(17);
119 if ids.len() > 1 {
120 let stem = id.path.file_stem().map(|s| s.to_string_lossy()).unwrap_or_default();
122 let key = ApkKey::lone(&stem, h);
123 archives.push(ArchiveKey { label: id.label.to_string(), path: id.path.to_path_buf(), key });
124 }
125 key = if i == 0 { h } else { key.rotate_left(11) ^ h ^ fnv64(id.path.as_os_str().as_encoded_bytes()).rotate_left(29) };
127 }
128 Ok(ApkKey { archives: archives.into(), ..ApkKey::lone(&apk.stem(), key) })
129 }
130
131 fn lone(stem: &str, hash: u64) -> ApkKey {
133 let stem: String = stem.chars().filter(|c| is_key_char(*c)).take(STEM_MAX).collect();
134 ApkKey::from_id(format!("{stem}-{hash:0width$x}", width = HASH_HEX_LEN))
135 }
136
137 fn from_id(id: String) -> ApkKey {
138 ApkKey { id, archives: Arc::new([]) }
139 }
140
141 pub fn as_str(&self) -> &str {
142 &self.id
143 }
144
145 pub fn locate<'n>(&self, name: &'n str) -> (&ApkKey, &'n str) {
150 let found = asc_apk::archive_of(name).and_then(|label| {
151 let a = self.archives.iter().find(|a| a.label == label)?;
152 Some((&a.key, &name[label.len() + 1..]))
153 });
154 found.unwrap_or((self, name))
155 }
156}
157
158fn fnv64(bytes: &[u8]) -> u64 {
161 const PRIME: u64 = 0x0100_0000_01b3;
162 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
163 let (chunks, rest) = bytes.as_chunks::<8>();
164 for c in chunks {
165 h ^= u64::from_le_bytes(*c);
166 h = h.wrapping_mul(PRIME);
167 }
168 for &b in rest {
169 h ^= b as u64;
170 h = h.wrapping_mul(PRIME);
171 }
172 h
173}
174
175pub enum DexData {
177 Owned(Vec<u8>),
178 Mapped(memmap2::Mmap),
179}
180
181impl Deref for DexData {
182 type Target = [u8];
183 fn deref(&self) -> &[u8] {
184 match self {
185 DexData::Owned(v) => v,
186 DexData::Mapped(m) => m,
187 }
188 }
189}
190
191impl DexData {
192 pub fn map_region(path: &Path, offset: u64, len: usize) -> std::io::Result<DexData> {
198 let file = fs::File::open(path)?;
199 if file.metadata()?.len() < offset.saturating_add(len as u64) {
200 return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "region past the end of the file"));
201 }
202 let map = unsafe { memmap2::MmapOptions::new().offset(offset).len(len).map(&file) }?;
205 Ok(DexData::Mapped(map))
206 }
207}
208
209impl AsRef<[u8]> for DexData {
210 fn as_ref(&self) -> &[u8] {
211 self
212 }
213}
214
215#[derive(Debug, thiserror::Error)]
216pub enum CacheError {
217 #[error("cache io error at {path}: {source}")]
218 Io { path: PathBuf, source: std::io::Error },
219 #[error("{path} is not a asc cache directory (no {tag} marker); refusing to touch it")]
220 NotACacheDir { path: PathBuf, tag: &'static str },
221 #[error("{path}: too little free disk space left to cache {bytes} bytes")]
222 LowDiskSpace { path: PathBuf, bytes: u64 },
223}
224
225fn io_err(path: &Path) -> impl Fn(std::io::Error) -> CacheError + '_ {
226 move |source| CacheError::Io { path: path.to_path_buf(), source }
227}
228
229pub struct DiskCache {
231 dir: PathBuf,
232 cap: DiskCapacity,
233 min_dex_bytes: u64,
234 min_derived_cost: Duration,
235 tiers: [bool; 3],
237 written: std::sync::atomic::AtomicU64,
239 boot: Option<SystemTime>,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
246pub enum Tier {
247 Dex,
249 Derived,
251 Rendered,
253}
254
255impl Tier {
256 pub const ALL: [Tier; 3] = [Tier::Dex, Tier::Derived, Tier::Rendered];
257
258 pub fn name(self) -> &'static str {
260 match self {
261 Tier::Dex => "dex",
262 Tier::Derived => "derived",
263 Tier::Rendered => "rendered",
264 }
265 }
266
267 pub fn label(self) -> &'static str {
268 match self {
269 Tier::Dex => "L1 dex",
270 Tier::Derived => "L2 derived",
271 Tier::Rendered => "L3 rendered",
272 }
273 }
274}
275
276const DERIVED_DIR: &str = "derived";
278const RENDERED_DIR: &str = "rendered";
280const SOURCES_DIR: &str = "sources";
282pub const SPILL_DIR: &str = "out";
285
286pub const RENDERED_MAX: u64 = 128 << 20;
289pub const RENDERED_BUDGET_DIVISOR: u64 = 8;
290pub const DERIVED_BUDGET_DIVISOR: u64 = 2;
292
293pub const DEFAULT_MIN_DEX_BYTES: u64 = 4 << 20;
296pub const DEFAULT_MIN_DERIVED_COST: Duration = Duration::from_millis(2);
299
300const DERIVED_MAGIC: &[u8; 8] = b"ASCL2\0\0\0";
303const DERIVED_HEADER: usize = 64;
304const DERIVED_FORMAT: u32 = 2;
306const RENDERED_MAGIC: &[u8; 8] = b"ASCL3\0\0\0";
307const RENDERED_FORMAT: u32 = 2;
309const L1_MAGIC: &[u8; 4] = b"ASC1";
312const L1_TRAILER: u64 = 8;
313
314pub struct Derived {
317 map: memmap2::Mmap,
318 pub build_cost: Duration,
320}
321
322#[derive(Debug)]
324pub struct Rendered {
325 map: memmap2::Mmap,
326 body: usize,
327}
328
329impl AsRef<[u8]> for Rendered {
330 fn as_ref(&self) -> &[u8] {
331 &self.map[self.body..]
332 }
333}
334
335impl Deref for Derived {
336 type Target = [u8];
337 fn deref(&self) -> &[u8] {
338 &self.map[DERIVED_HEADER..]
339 }
340}
341
342impl AsRef<[u8]> for Derived {
343 fn as_ref(&self) -> &[u8] {
344 self
345 }
346}
347
348#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
350pub struct TierStats {
351 pub apks: usize,
353 pub files: usize,
354 pub bytes: u64,
355}
356
357struct Item {
359 tier: Tier,
360 key: String,
361 path: PathBuf,
362 mtime: SystemTime,
363 files: usize,
364 bytes: u64,
365 cost_us: u64,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub enum DiskCapacity {
372 Auto(AutoDisk),
374 Fixed(u64),
376}
377
378impl Default for DiskCapacity {
379 fn default() -> DiskCapacity {
380 DiskCapacity::Auto(AutoDisk::default())
381 }
382}
383
384pub const AUTO_DISK_UNKNOWN: u64 = 4 << 30;
386pub const AUTO_DISK_RESERVE_MIN: u64 = 1 << 30;
389pub const AUTO_DISK_RESERVE_PERCENT: u64 = 5;
390
391pub fn auto_disk_reserve(total: u64) -> u64 {
394 AUTO_DISK_RESERVE_MIN.max(total / 100 * AUTO_DISK_RESERVE_PERCENT)
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub struct AutoDisk {
402 pub percent: u8,
404 pub ceiling: u64,
406}
407
408impl AutoDisk {
409 pub const PERCENT: u8 = 20;
411 pub const CEILING: u64 = 32 << 30;
414
415 pub fn bytes_for(&self, space: Option<(u64, u64)>, used: u64) -> u64 {
423 let Some((free, total)) = space else { return AUTO_DISK_UNKNOWN.min(self.ceiling) };
424 let room = used.saturating_add(free);
425 let budget = (room / 100 * u64::from(self.percent)).min(self.ceiling);
426 budget.min(room.saturating_sub(auto_disk_reserve(total)))
427 }
428}
429
430impl Default for AutoDisk {
431 fn default() -> AutoDisk {
432 AutoDisk { percent: AutoDisk::PERCENT, ceiling: AutoDisk::CEILING }
433 }
434}
435
436pub fn disk_space(path: &Path) -> Option<(u64, u64)> {
439 let mut p = path;
440 loop {
441 if p.exists() {
442 return fs_space(p);
443 }
444 p = p.parent()?;
445 }
446}
447
448#[cfg(unix)]
449fn fs_space(path: &Path) -> Option<(u64, u64)> {
450 use std::os::unix::ffi::OsStrExt;
451 let c = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
452 let mut st = std::mem::MaybeUninit::<libc::statvfs>::uninit();
453 if unsafe { libc::statvfs(c.as_ptr(), st.as_mut_ptr()) } != 0 {
455 return None;
456 }
457 let st = unsafe { st.assume_init() };
459 #[allow(clippy::useless_conversion, clippy::unnecessary_cast)]
461 let (frsize, bavail, blocks) = (st.f_frsize as u64, st.f_bavail as u64, st.f_blocks as u64);
462 Some((bavail.saturating_mul(frsize), blocks.saturating_mul(frsize)))
463}
464
465#[cfg(not(unix))]
466fn fs_space(_path: &Path) -> Option<(u64, u64)> {
467 None
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub struct DiskBudget {
473 pub capacity: DiskCapacity,
474 pub cap_bytes: u64,
476 pub used: u64,
478 pub space: Option<(u64, u64)>,
480}
481
482const STALE_TMP: Duration = Duration::from_secs(10 * 60);
484
485pub const CACHEDIR_TAG: &str = "CACHEDIR.TAG";
493const CACHEDIR_TAG_CONTENT: &str = "Signature: 8a477f597d28d172789f06886806bc55\n\
494# This file is a cache directory tag created by asc.\n\
495# For information about cache directory tags, see: https://bford.info/cachedir/\n";
496
497fn is_cache_dir_name(name: &str) -> bool {
502 let Some((stem, hash)) = name.rsplit_once('-') else { return false };
503 hash.len() == HASH_HEX_LEN
504 && hash.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
505 && stem.len() <= STEM_MAX
506 && stem.chars().all(is_key_char)
507}
508
509impl DiskCache {
510 pub fn default_dir() -> Option<PathBuf> {
513 if let Some(d) = std::env::var_os("XDG_CACHE_HOME") {
514 return Some(PathBuf::from(d).join("asc"));
515 }
516 std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache").join("asc"))
517 }
518
519 pub fn open(dir: impl Into<PathBuf>) -> DiskCache {
523 DiskCache {
524 dir: dir.into(),
525 cap: DiskCapacity::default(),
526 min_dex_bytes: DEFAULT_MIN_DEX_BYTES,
527 min_derived_cost: DEFAULT_MIN_DERIVED_COST,
528 tiers: [true; 3],
529 written: Default::default(),
530 boot: boot_time(),
531 }
532 }
533
534 pub fn with_tiers(mut self, tiers: &[Tier]) -> DiskCache {
537 self.tiers = Tier::ALL.map(|t| tiers.contains(&t));
538 self
539 }
540
541 pub fn uses(&self, tier: Tier) -> bool {
542 self.tiers[tier as usize]
543 }
544
545 pub fn with_min_dex_bytes(mut self, bytes: u64) -> DiskCache {
547 self.min_dex_bytes = bytes;
548 self
549 }
550
551 pub fn min_dex_bytes(&self) -> u64 {
555 self.min_dex_bytes
556 }
557
558 pub fn admits_dex(&self, len: u64) -> bool {
559 len >= self.min_dex_bytes
560 }
561
562 pub fn with_min_derived_cost(mut self, cost: Duration) -> DiskCache {
565 self.min_derived_cost = cost;
566 self
567 }
568
569 pub fn admits_derived(&self, cost: Duration) -> bool {
572 cost >= self.min_derived_cost
573 }
574
575 pub fn open_default() -> Option<DiskCache> {
577 Self::default_dir().map(Self::open)
578 }
579
580 pub fn with_capacity(self, cap_bytes: u64) -> DiskCache {
582 self.with_capacity_mode(DiskCapacity::Fixed(cap_bytes))
583 }
584
585 pub fn with_capacity_mode(mut self, cap: DiskCapacity) -> DiskCache {
586 self.cap = cap;
587 self
588 }
589
590 pub fn capacity_mode(&self) -> DiskCapacity {
591 self.cap
592 }
593
594 pub fn dir(&self) -> &Path {
595 &self.dir
596 }
597
598 pub fn capacity_for(&self, used: u64) -> u64 {
601 match self.cap {
602 DiskCapacity::Fixed(b) => b,
603 DiskCapacity::Auto(auto) => auto.bytes_for(disk_space(&self.dir), used),
604 }
605 }
606
607 pub fn budget(&self) -> DiskBudget {
609 let used = self.stats().2;
610 let space = disk_space(&self.dir);
611 let cap_bytes = match self.cap {
612 DiskCapacity::Fixed(b) => b,
613 DiskCapacity::Auto(auto) => auto.bytes_for(space, used),
614 };
615 DiskBudget { capacity: self.cap, cap_bytes, used, space }
616 }
617
618 fn predates_boot(&self, meta: &fs::Metadata) -> bool {
620 predates_boot(meta.modified().ok(), self.boot, SystemTime::now())
621 }
622
623 fn discard<T>(&self, path: &Path) -> Option<T> {
625 let _ = fs::remove_file(path);
628 None
629 }
630
631 fn entry_path(&self, key: &ApkKey, entry_name: &str) -> PathBuf {
632 let (key, entry_name) = key.locate(entry_name);
633 self.dir.join(key.as_str()).join(safe_name(entry_name))
634 }
635
636 fn derived_path(&self, key: &ApkKey, name: &str, kind: &str, version: u32) -> PathBuf {
637 let (key, name) = key.locate(name);
638 let file = format!("{}~{}.v{DERIVED_FORMAT}.{version}", safe_name(name), safe_name(kind));
639 self.dir.join(DERIVED_DIR).join(key.as_str()).join(file)
640 }
641
642 fn rendered_path(&self, key: &ApkKey, query: &str) -> PathBuf {
643 self.dir.join(RENDERED_DIR).join(key.as_str()).join(format!("{:016x}", fnv64(query.as_bytes())))
644 }
645
646 pub fn get(&self, key: &ApkKey, entry_name: &str, expected_len: u64) -> Option<DexData> {
650 if !self.uses(Tier::Dex) {
651 return None;
652 }
653 let path = self.entry_path(key, entry_name);
654 let file = fs::File::open(&path).ok()?;
655 let meta = file.metadata().ok()?;
656 let len = meta.len().checked_sub(L1_TRAILER).filter(|&l| l > 0)?;
657 if expected_len != 0 && len != expected_len {
658 return None;
659 }
660 let mut trailer = [0u8; L1_TRAILER as usize];
661 (&file).seek(SeekFrom::Start(len)).ok()?;
662 (&file).read_exact(&mut trailer).ok()?;
663 let map = map_prefix(&file, len)?;
666 if &trailer[..4] != L1_MAGIC {
667 return self.discard(&path);
668 }
669 if self.predates_boot(&meta) {
670 if crc32fast::hash(&map) != u32::from_le_bytes(trailer[4..].try_into().expect("4 bytes")) {
671 return self.discard(&path);
672 }
673 let _ = touch_file(&file);
674 }
675 let _ = touch_dir(path.parent()?);
677 Some(DexData::Mapped(map))
678 }
679
680 pub fn put(&self, key: &ApkKey, entry_name: &str, bytes: &[u8]) -> Result<(), CacheError> {
684 if !self.uses(Tier::Dex) {
685 return Ok(());
686 }
687 let mut trailer = [0u8; L1_TRAILER as usize];
688 trailer[..4].copy_from_slice(L1_MAGIC);
689 trailer[4..].copy_from_slice(&crc32fast::hash(bytes).to_le_bytes());
690 self.write_atomic(&self.entry_path(key, entry_name), &[bytes, &trailer])
691 }
692
693 pub fn get_derived(&self, key: &ApkKey, name: &str, kind: &str, version: u32) -> Option<Derived> {
698 if !self.uses(Tier::Derived) {
699 return None;
700 }
701 let path = self.derived_path(key, name, kind, version);
702 let file = fs::File::open(&path).ok()?;
703 let meta = file.metadata().ok()?;
704 let map = map_file(&file)?;
705 let header = map.get(..DERIVED_HEADER)?;
706 let u32_at = |o: usize| u32::from_le_bytes(header[o..o + 4].try_into().expect("4 bytes"));
707 let u64_at = |o: usize| u64::from_le_bytes(header[o..o + 8].try_into().expect("8 bytes"));
708 if &header[..8] != DERIVED_MAGIC
709 || u32_at(8) != DERIVED_FORMAT
710 || u32_at(12) != version
711 || u64_at(24) != (map.len() - DERIVED_HEADER) as u64
712 {
713 return None;
714 }
715 if self.predates_boot(&meta) && crc32fast::hash(&map[DERIVED_HEADER..]) != u32_at(32) {
716 return self.discard(&path);
717 }
718 let _ = touch_file(&file);
719 Some(Derived { build_cost: Duration::from_micros(u64_at(16)), map })
720 }
721
722 pub fn put_derived(
726 &self,
727 key: &ApkKey,
728 name: &str,
729 kind: &str,
730 version: u32,
731 build_cost: Duration,
732 payload: &[u8],
733 ) -> Result<(), CacheError> {
734 if !self.uses(Tier::Derived) {
735 return Ok(());
736 }
737 let mut header = [0u8; DERIVED_HEADER];
738 header[..8].copy_from_slice(DERIVED_MAGIC);
739 header[8..12].copy_from_slice(&DERIVED_FORMAT.to_le_bytes());
740 header[12..16].copy_from_slice(&version.to_le_bytes());
741 header[16..24].copy_from_slice(&(build_cost.as_micros() as u64).to_le_bytes());
742 header[24..32].copy_from_slice(&(payload.len() as u64).to_le_bytes());
743 header[32..36].copy_from_slice(&crc32fast::hash(payload).to_le_bytes());
744 self.write_atomic(&self.derived_path(key, name, kind, version), &[&header, payload])
745 }
746
747 pub fn get_rendered(&self, key: &ApkKey, query: &str) -> Option<Vec<u8>> {
750 self.get_rendered_view(key, query).map(|view| view.as_ref().to_vec())
751 }
752
753 pub fn get_rendered_view(&self, key: &ApkKey, query: &str) -> Option<Rendered> {
757 if !self.uses(Tier::Rendered) {
758 return None;
759 }
760 let path = self.rendered_path(key, query);
761 let file = fs::File::open(&path).ok()?;
762 let data = map_file(&file)?;
763 let key_len = u32::from_le_bytes(data.get(8..12)?.try_into().ok()?) as usize;
764 let crc = u32::from_le_bytes(data.get(12..16)?.try_into().ok()?);
765 let body = 16usize.checked_add(key_len)?;
766 if data.get(..8)? != RENDERED_MAGIC || crc32fast::hash(&data[16..]) != crc {
767 drop(data);
768 return self.discard(&path);
769 }
770 if data.get(16..body)? != query.as_bytes() {
772 return None;
773 }
774 let _ = touch_file(&file);
775 Some(Rendered { map: data, body })
776 }
777
778 pub fn put_rendered(&self, key: &ApkKey, query: &str, payload: &[u8]) -> Result<(), CacheError> {
780 if !self.uses(Tier::Rendered) {
781 return Ok(());
782 }
783 let mut crc = crc32fast::Hasher::new();
784 crc.update(query.as_bytes());
785 crc.update(payload);
786 let mut header = Vec::with_capacity(16 + query.len());
787 header.extend_from_slice(RENDERED_MAGIC);
788 header.extend_from_slice(&(query.len() as u32).to_le_bytes());
789 header.extend_from_slice(&crc.finalize().to_le_bytes());
790 header.extend_from_slice(query.as_bytes());
791 self.write_atomic(&self.rendered_path(key, query), &[&header, payload])
792 }
793
794 pub fn note_source(&self, key: &ApkKey, apk_path: &Path) -> Result<(), CacheError> {
799 if !key.archives.is_empty() {
800 return key.archives.iter().try_for_each(|a| self.note_source(&a.key, &a.path));
801 }
802 let path = self.dir.join(SOURCES_DIR).join(key.as_str());
803 let Some(stamp) = source_stamp(apk_path) else { return Ok(()) };
804 if fs::read_to_string(&path).is_ok_and(|s| s == stamp) {
805 return Ok(());
806 }
807 self.write_atomic(&path, &[stamp.as_bytes()])
808 }
809
810 fn write_atomic(&self, path: &Path, parts: &[&[u8]]) -> Result<(), CacheError> {
815 if !self.is_cache_dir() {
818 return Err(self.not_a_cache_dir());
819 }
820 let len: u64 = parts.iter().map(|p| p.len() as u64).sum();
821 if matches!(self.cap, DiskCapacity::Auto(_))
825 && let Some((free, total)) = disk_space(&self.dir)
826 && free.saturating_sub(len) < auto_disk_reserve(total)
827 {
828 return Err(CacheError::LowDiskSpace { path: self.dir.clone(), bytes: len });
829 }
830 let parent = path.parent().expect("cache path has a parent");
831 fs::create_dir_all(parent).map_err(io_err(parent))?;
832 let _ = self.ensure_tag();
835 let tmp = parent.join(format!(".{}.{}.tmp", path.file_name().unwrap_or_default().to_string_lossy(), std::process::id()));
836 {
837 let mut f = fs::File::create(&tmp).map_err(io_err(&tmp))?;
838 for p in parts {
839 f.write_all(p).map_err(io_err(&tmp))?;
840 }
841 }
842 fs::rename(&tmp, path).map_err(io_err(path))?;
843 self.written.fetch_add(len, std::sync::atomic::Ordering::Relaxed);
844 Ok(())
845 }
846
847 pub fn stats(&self) -> (usize, usize, u64) {
850 let items = self.items();
851 let keys: HashSet<&str> = items.iter().map(|i| i.key.as_str()).collect();
852 (keys.len(), items.iter().map(|i| i.files).sum(), items.iter().map(|i| i.bytes).sum())
853 }
854
855 pub fn tier_stats(&self) -> [TierStats; 3] {
857 let items = self.items();
858 Tier::ALL.map(|tier| {
859 let of_tier = || items.iter().filter(move |i| i.tier == tier);
860 let keys: HashSet<&str> = of_tier().map(|i| i.key.as_str()).collect();
861 TierStats { apks: keys.len(), files: of_tier().map(|i| i.files).sum(), bytes: of_tier().map(|i| i.bytes).sum() }
862 })
863 }
864
865 pub fn clear(&self) -> Result<u64, CacheError> {
869 if !self.dir.exists() {
870 return Ok(0);
871 }
872 if !self.is_cache_dir() {
873 return Err(self.not_a_cache_dir());
874 }
875 let mut bytes = 0;
876 for item in self.items() {
877 remove_item(&item).map_err(io_err(&item.path))?;
878 bytes += item.bytes;
879 }
880 for sub in [DERIVED_DIR, RENDERED_DIR, SOURCES_DIR, SPILL_DIR] {
881 let path = self.dir.join(sub);
882 if path.is_dir() {
883 fs::remove_dir_all(&path).map_err(io_err(&path))?;
884 }
885 }
886 Ok(bytes)
887 }
888
889 pub fn check_usable(&self) -> Result<(), CacheError> {
894 if self.is_cache_dir() { Ok(()) } else { Err(self.not_a_cache_dir()) }
895 }
896
897 fn not_a_cache_dir(&self) -> CacheError {
898 CacheError::NotACacheDir { path: self.dir.clone(), tag: CACHEDIR_TAG }
899 }
900
901 fn ensure_tag(&self) -> Result<(), CacheError> {
903 if self.has_tag() {
904 return Ok(());
905 }
906 let tag = self.dir.join(CACHEDIR_TAG);
907 match fs::OpenOptions::new().write(true).create_new(true).open(&tag) {
909 Ok(mut f) => f.write_all(CACHEDIR_TAG_CONTENT.as_bytes()).map_err(|e| {
912 drop(f);
913 let _ = fs::remove_file(&tag);
914 io_err(&tag)(e)
915 }),
916 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
917 Err(e) => Err(io_err(&tag)(e)),
918 }
919 }
920
921 fn has_tag(&self) -> bool {
922 self.dir.join(CACHEDIR_TAG).is_file()
923 }
924
925 fn is_cache_dir(&self) -> bool {
931 if self.has_tag() {
932 return true;
933 }
934 match fs::read_dir(&self.dir) {
935 Ok(rd) => rd.flatten().all(|d| {
936 let name = d.file_name();
937 let name = name.to_string_lossy();
938 match d.file_type() {
939 Ok(t) if t.is_dir() => is_cache_dir_name(&name) || TIER_DIRS.contains(&&*name) || name == SPILL_DIR,
940 Ok(t) if t.is_file() => name == ".DS_Store" || name == EVICT_MARKER,
941 _ => false,
942 }
943 }),
944 Err(e) => e.kind() == std::io::ErrorKind::NotFound,
945 }
946 }
947
948 fn apk_dirs(&self) -> Vec<(SystemTime, PathBuf, usize, u64)> {
953 apk_dirs_in(&self.dir)
954 }
955
956 fn items(&self) -> Vec<Item> {
958 let mut out: Vec<Item> = self
959 .apk_dirs()
960 .into_iter()
961 .map(|(mtime, path, files, bytes)| Item {
962 tier: Tier::Dex,
963 key: path.file_name().unwrap_or_default().to_string_lossy().into_owned(),
964 path,
965 mtime,
966 files,
967 bytes,
968 cost_us: 0,
969 })
970 .collect();
971 for (tier, sub) in [(Tier::Derived, DERIVED_DIR), (Tier::Rendered, RENDERED_DIR)] {
972 for (_, dir, _, _) in apk_dirs_in(&self.dir.join(sub)) {
973 let key = dir.file_name().unwrap_or_default().to_string_lossy().into_owned();
974 let Ok(rd) = fs::read_dir(&dir) else { continue };
975 for f in rd.flatten() {
976 let Ok(m) = f.metadata() else { continue };
977 if !m.is_file() || f.file_name().to_string_lossy().ends_with(".tmp") {
978 continue;
979 }
980 let cost_us = if tier == Tier::Derived { derived_cost_us(&f.path()) } else { 0 };
981 let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
982 out.push(Item { tier, key: key.clone(), path: f.path(), mtime, files: 1, bytes: m.len(), cost_us });
983 }
984 }
985 }
986 out
987 }
988
989 fn dead_keys(&self) -> HashSet<String> {
992 let mut out = HashSet::new();
993 let Ok(rd) = fs::read_dir(self.dir.join(SOURCES_DIR)) else { return out };
994 for f in rd.flatten() {
995 let Ok(stamp) = fs::read_to_string(f.path()) else { continue };
996 let Some(path) = stamp.lines().nth(2) else { continue };
997 if source_stamp(Path::new(path)).as_deref() != Some(stamp.as_str()) {
998 out.insert(f.file_name().to_string_lossy().into_owned());
999 }
1000 }
1001 out
1002 }
1003}
1004
1005fn apk_dirs_in(root: &Path) -> Vec<(SystemTime, PathBuf, usize, u64)> {
1010 let mut out = Vec::new();
1011 let Ok(rd) = fs::read_dir(root) else { return out };
1012 let now = SystemTime::now();
1013 for d in rd.flatten() {
1014 let path = d.path();
1015 if !is_cache_dir_name(&d.file_name().to_string_lossy()) || !path.is_dir() {
1016 continue;
1017 }
1018 let mtime = d.metadata().and_then(|m| m.modified()).unwrap_or(SystemTime::UNIX_EPOCH);
1019 let (mut files, mut size) = (0usize, 0u64);
1020 if let Ok(fd) = fs::read_dir(&path) {
1021 for f in fd.flatten() {
1022 let Ok(m) = f.metadata() else { continue };
1023 let name = f.file_name();
1024 let is_tmp = name.to_string_lossy().ends_with(".tmp");
1025 if is_tmp && m.modified().ok().and_then(|t| now.duration_since(t).ok()).is_some_and(|age| age > STALE_TMP) {
1026 let _ = fs::remove_file(f.path());
1027 continue;
1028 }
1029 files += 1;
1030 size += m.len();
1031 }
1032 }
1033 out.push((mtime, path, files, size));
1034 }
1035 out
1036}
1037
1038impl DiskCache {
1039 pub fn evict(&self) {
1051 if !self.is_cache_dir() {
1052 return;
1053 }
1054 self.written.store(0, std::sync::atomic::Ordering::Relaxed);
1055 let _ = fs::File::create(self.dir.join(EVICT_MARKER));
1056 let items = self.items();
1057 let mut total: u64 = items.iter().map(|i| i.bytes).sum();
1058 let cap = self.capacity_for(total);
1059 let caps = [cap, cap / DERIVED_BUDGET_DIVISOR, (cap / RENDERED_BUDGET_DIVISOR).min(RENDERED_MAX)];
1060 let mut tier_used = [0u64; 3];
1061 for i in &items {
1062 tier_used[i.tier as usize] += i.bytes;
1063 }
1064 if total > cap || tier_used[1] > caps[1] || tier_used[2] > caps[2] {
1065 let order = eviction_order(&items, &self.dead_keys());
1066 let mut gone = vec![false; items.len()];
1067 let mut remove = |idx: usize, total: &mut u64, tier_used: &mut [u64; 3]| {
1068 if !gone[idx] && remove_item(&items[idx]).is_ok() {
1069 gone[idx] = true;
1070 *total = total.saturating_sub(items[idx].bytes);
1071 let t = items[idx].tier as usize;
1072 tier_used[t] = tier_used[t].saturating_sub(items[idx].bytes);
1073 }
1074 };
1075 for tier in [Tier::Rendered, Tier::Derived] {
1077 for &idx in order.iter().filter(|&&i| items[i].tier == tier) {
1078 if tier_used[tier as usize] <= caps[tier as usize] {
1079 break;
1080 }
1081 remove(idx, &mut total, &mut tier_used);
1082 }
1083 }
1084 for &idx in &order {
1085 if total <= cap {
1086 break;
1087 }
1088 remove(idx, &mut total, &mut tier_used);
1089 }
1090 }
1091 if let Ok(rd) = fs::read_dir(self.dir.join(SOURCES_DIR)) {
1093 let live: HashSet<String> = self.items().into_iter().map(|i| i.key).collect();
1094 for f in rd.flatten() {
1095 if !live.contains(&*f.file_name().to_string_lossy()) {
1096 let _ = fs::remove_file(f.path());
1097 }
1098 }
1099 }
1100 }
1101}
1102
1103impl DiskCache {
1104 pub fn evict_lazily(&self) {
1110 if !self.recently_evicted() {
1111 self.evict();
1112 }
1113 }
1114
1115 fn recently_evicted(&self) -> bool {
1123 let written = self.written.load(std::sync::atomic::Ordering::Relaxed);
1124 let fresh = fs::metadata(self.dir.join(EVICT_MARKER))
1125 .and_then(|m| m.modified())
1126 .ok()
1127 .and_then(|t| SystemTime::now().duration_since(t).ok())
1128 .is_some_and(|age| age < EVICT_INTERVAL);
1129 fresh && written < self.capacity_for(0) / EVICT_WRITE_DIVISOR
1130 }
1131}
1132
1133const EVICT_MARKER: &str = ".last-evict";
1135const EVICT_INTERVAL: Duration = Duration::from_secs(5);
1136const EVICT_WRITE_DIVISOR: u64 = 16;
1137
1138fn eviction_order(items: &[Item], dead: &HashSet<String>) -> Vec<usize> {
1144 let newest = items.iter().map(|i| i.mtime).max().unwrap_or(SystemTime::UNIX_EPOCH);
1145 let age_secs = |i: &Item| newest.duration_since(i.mtime).unwrap_or_default().as_secs_f64() + 1.0;
1146 let score = |i: &Item| -> (u8, f64) {
1147 if dead.contains(&i.key) {
1148 return (0, -(i.bytes as f64));
1149 }
1150 match i.tier {
1151 Tier::Dex => (1, -(age_secs(i) * i.bytes as f64)),
1152 Tier::Rendered => (2, -age_secs(i)),
1153 Tier::Derived => (3, i.cost_us as f64 / i.bytes.max(1) as f64 / (1.0 + age_secs(i) / 3600.0)),
1154 }
1155 };
1156 let mut order: Vec<usize> = (0..items.len()).collect();
1157 order.sort_by(|&a, &b| {
1158 let (sa, sb) = (score(&items[a]), score(&items[b]));
1159 sa.0.cmp(&sb.0).then(sa.1.total_cmp(&sb.1)).then(items[a].mtime.cmp(&items[b].mtime))
1160 });
1161 order
1162}
1163
1164fn remove_item(item: &Item) -> std::io::Result<()> {
1165 match item.tier {
1166 Tier::Dex => fs::remove_dir_all(&item.path),
1167 _ => fs::remove_file(&item.path),
1168 }
1169}
1170
1171const TIER_DIRS: [&str; 3] = [DERIVED_DIR, RENDERED_DIR, SOURCES_DIR];
1173
1174fn safe_name(name: &str) -> String {
1176 name.chars().map(|c| if is_key_char(c) { c } else { '_' }).collect()
1177}
1178
1179pub fn rendered_key(producer: &str, version: &str, query: &str) -> String {
1183 format!("{producer}\0{version}\0{RENDERED_FORMAT}\0{query}")
1184}
1185
1186fn map_file(file: &fs::File) -> Option<memmap2::Mmap> {
1188 unsafe { memmap2::MmapOptions::new().map(file) }.ok()
1191}
1192
1193fn map_prefix(file: &fs::File, len: u64) -> Option<memmap2::Mmap> {
1195 let len = usize::try_from(len).ok()?;
1196 unsafe { memmap2::MmapOptions::new().len(len).map(file) }.ok()
1198}
1199
1200fn predates_boot(mtime: Option<SystemTime>, boot: Option<SystemTime>, now: SystemTime) -> bool {
1206 match (mtime, boot) {
1207 (Some(m), Some(b)) => m < b || m > now,
1208 _ => true,
1209 }
1210}
1211
1212fn boot_time() -> Option<SystemTime> {
1214 static BOOT: OnceLock<Option<SystemTime>> = OnceLock::new();
1215 *BOOT.get_or_init(read_boot_time)
1216}
1217
1218#[cfg(any(target_os = "linux", target_os = "android"))]
1219fn read_boot_time() -> Option<SystemTime> {
1220 let stat = fs::read_to_string("/proc/stat").ok()?;
1221 let secs = stat.lines().find_map(|l| l.strip_prefix("btime "))?.trim().parse().ok()?;
1222 Some(UNIX_EPOCH + Duration::from_secs(secs))
1223}
1224
1225#[cfg(target_vendor = "apple")]
1226fn read_boot_time() -> Option<SystemTime> {
1227 let mut tv = libc::timeval { tv_sec: 0, tv_usec: 0 };
1228 let mut len = std::mem::size_of::<libc::timeval>();
1229 let rc = unsafe { libc::sysctlbyname(c"kern.boottime".as_ptr(), (&raw mut tv).cast(), &mut len, std::ptr::null_mut(), 0) };
1232 if rc != 0 || len != std::mem::size_of::<libc::timeval>() {
1233 return None;
1234 }
1235 Some(UNIX_EPOCH + Duration::from_secs(u64::try_from(tv.tv_sec).ok()?) + Duration::from_micros(u64::try_from(tv.tv_usec).ok()?))
1236}
1237
1238#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
1239fn read_boot_time() -> Option<SystemTime> {
1240 None
1241}
1242
1243fn derived_cost_us(path: &Path) -> u64 {
1245 use std::io::Read;
1246 let mut header = [0u8; 24];
1247 match fs::File::open(path).and_then(|mut f| f.read_exact(&mut header)) {
1248 Ok(()) if &header[..8] == DERIVED_MAGIC => u64::from_le_bytes(header[16..24].try_into().expect("8 bytes")),
1249 _ => 0,
1250 }
1251}
1252
1253fn source_stamp(apk: &Path) -> Option<String> {
1255 let abs = fs::canonicalize(apk).ok()?;
1256 let meta = fs::metadata(&abs).ok()?;
1257 let mtime = meta.modified().ok()?.duration_since(SystemTime::UNIX_EPOCH).ok()?.as_nanos();
1258 let path = abs.to_str()?;
1259 (!path.contains('\n')).then(|| format!("{}\n{mtime}\n{path}", meta.len()))
1260}
1261
1262fn touch_file(file: &fs::File) -> std::io::Result<()> {
1263 file.set_modified(SystemTime::now())
1264}
1265
1266fn touch_dir(dir: &Path) -> std::io::Result<()> {
1267 let now = SystemTime::now();
1268 if fs::File::open(dir).and_then(|f| f.set_modified(now)).is_ok() {
1269 return Ok(());
1270 }
1271 let marker = dir.join(".touch");
1273 fs::File::create(&marker)?;
1274 fs::remove_file(&marker)
1275}
1276
1277struct Slot<T> {
1278 value: Arc<T>,
1279 size: usize,
1280 tick: u64,
1282}
1283
1284pub struct MemoryCache<T> {
1287 cap_bytes: usize,
1288 used: usize,
1289 reserved: usize,
1291 clock: u64,
1292 map: HashMap<ApkKey, HashMap<String, Slot<T>>>,
1293}
1294
1295impl<T> MemoryCache<T> {
1296 pub fn new(cap_bytes: usize) -> Self {
1297 MemoryCache { cap_bytes, used: 0, reserved: 0, clock: 0, map: HashMap::new() }
1298 }
1299
1300 fn tick(&mut self) -> u64 {
1301 self.clock += 1;
1302 self.clock
1303 }
1304
1305 pub fn get(&mut self, key: &ApkKey, entry_name: &str) -> Option<Arc<T>> {
1306 let tick = self.tick();
1307 let slot = self.map.get_mut(key)?.get_mut(entry_name)?;
1308 slot.tick = tick;
1309 Some(Arc::clone(&slot.value))
1310 }
1311
1312 pub fn insert(&mut self, key: &ApkKey, entry_name: &str, value: Arc<T>, size: usize) {
1313 let tick = self.tick();
1314 let per_apk = self.map.entry(key.clone()).or_default();
1315 if let Some(old) = per_apk.insert(entry_name.to_string(), Slot { value, size, tick }) {
1316 self.used -= old.size;
1317 }
1318 self.used += size;
1319 self.evict_over_cap();
1320 }
1321
1322 pub fn resize(&mut self, key: &ApkKey, entry_name: &str, value: &Arc<T>, size: usize) {
1327 let Some(slot) = self.map.get_mut(key).and_then(|m| m.get_mut(entry_name)) else { return };
1328 if !Arc::ptr_eq(&slot.value, value) {
1329 return;
1330 }
1331 let old = std::mem::replace(&mut slot.size, size);
1332 self.used = self.used - old + size;
1333 self.evict_over_cap();
1334 }
1335
1336 pub fn replace(&mut self, key: &ApkKey, entry_name: &str, current: &Arc<T>, new: Arc<T>) -> bool {
1339 match self.map.get_mut(key).and_then(|m| m.get_mut(entry_name)) {
1340 Some(slot) if Arc::ptr_eq(&slot.value, current) => {
1341 slot.value = new;
1342 true
1343 }
1344 _ => false,
1345 }
1346 }
1347
1348 pub fn charged_bytes(&self, key: &ApkKey, entry_name: &str) -> Option<usize> {
1350 self.map.get(key)?.get(entry_name).map(|s| s.size)
1351 }
1352
1353 fn evict_over_cap(&mut self) {
1356 while self.used + self.reserved > self.cap_bytes && self.len() > 1 {
1357 let Some((k, name)) = self.lru() else { break };
1358 if let Some(per_apk) = self.map.get_mut(&k) {
1359 if let Some(old) = per_apk.remove(&name) {
1360 self.used -= old.size;
1361 }
1362 if per_apk.is_empty() {
1363 self.map.remove(&k);
1364 }
1365 }
1366 }
1367 }
1368
1369 fn lru(&self) -> Option<(ApkKey, String)> {
1370 self.map
1371 .iter()
1372 .flat_map(|(k, per)| per.iter().map(move |(n, s)| (s.tick, k, n)))
1373 .min_by_key(|(t, _, _)| *t)
1374 .map(|(_, k, n)| (k.clone(), n.clone()))
1375 }
1376
1377 pub fn used_bytes(&self) -> usize {
1378 self.used
1379 }
1380
1381 pub fn capacity_bytes(&self) -> usize {
1382 self.cap_bytes
1383 }
1384
1385 pub fn reserved_bytes(&self) -> usize {
1388 self.reserved
1389 }
1390
1391 pub fn set_reserved(&mut self, bytes: usize) {
1394 self.reserved = bytes;
1395 self.evict_over_cap();
1396 }
1397
1398 pub fn set_capacity(&mut self, cap_bytes: usize) {
1401 self.cap_bytes = cap_bytes;
1402 self.evict_over_cap();
1403 }
1404
1405 pub fn peek(&self, key: &ApkKey, entry_name: &str) -> Option<Arc<T>> {
1407 self.map.get(key)?.get(entry_name).map(|s| Arc::clone(&s.value))
1408 }
1409
1410 pub fn values(&self) -> impl Iterator<Item = &Arc<T>> {
1412 self.map.values().flat_map(|per| per.values().map(|s| &s.value))
1413 }
1414
1415 pub fn len(&self) -> usize {
1416 self.map.values().map(HashMap::len).sum()
1417 }
1418
1419 pub fn is_empty(&self) -> bool {
1420 self.len() == 0
1421 }
1422
1423 pub fn remove_apk(&mut self, key: &ApkKey) -> usize {
1425 let freed: usize = self.map.remove(key).map(|per| per.values().map(|s| s.size).sum()).unwrap_or(0);
1426 self.used -= freed;
1427 freed
1428 }
1429
1430 pub fn clear(&mut self) {
1431 self.map.clear();
1432 self.used = 0;
1433 }
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438 use super::*;
1439
1440 #[test]
1441 fn regions_of_a_file_map_at_any_offset() {
1442 let dir = tempfile::tempdir().unwrap();
1443 let path = dir.path().join("app.apk");
1444 let bytes: Vec<u8> = (0..20_000u32).map(|i| (i % 251) as u8).collect();
1445 fs::write(&path, &bytes).unwrap();
1446 for (off, len) in [(0, 10), (4097, 3000), (12_345, 7655), (19_999, 1)] {
1447 let d = DexData::map_region(&path, off as u64, len).unwrap();
1448 assert!(matches!(d, DexData::Mapped(_)));
1449 assert_eq!(&*d, &bytes[off..off + len]);
1450 }
1451 assert!(DexData::map_region(&path, 19_999, 2).is_err());
1452 assert!(DexData::map_region(&dir.path().join("missing"), 0, 1).is_err());
1453 }
1454
1455 #[test]
1456 fn memory_remove_apk_releases_only_that_apk() {
1457 let (a, b) = (ApkKey::from_id("a-0123456789abcdef".into()), ApkKey::from_id("b-0123456789abcdef".into()));
1458 let mut mem = MemoryCache::new(1 << 20);
1459 mem.insert(&a, "classes.dex", Arc::new(1), 100);
1460 mem.insert(&a, "classes2.dex", Arc::new(2), 50);
1461 mem.insert(&b, "classes.dex", Arc::new(3), 7);
1462 assert_eq!(mem.remove_apk(&a), 150);
1463 assert_eq!(mem.remove_apk(&a), 0);
1464 assert_eq!((mem.used_bytes(), mem.len()), (7, 1));
1465 assert!(mem.get(&b, "classes.dex").is_some());
1466 }
1467
1468 #[test]
1469 fn disk_roundtrip_and_clear() {
1470 let dir = tempfile::tempdir().unwrap();
1471 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
1472 let key = ApkKey::from_id("app-0123456789abcdef".into());
1473 assert!(cache.get(&key, "classes.dex", 3).is_none());
1474 cache.put(&key, "classes.dex", b"abc").unwrap();
1475 assert_eq!(&*cache.get(&key, "classes.dex", 3).unwrap(), b"abc");
1476 assert!(cache.get(&key, "classes.dex", 4).is_none());
1477 let (dirs, files, bytes) = cache.stats();
1478 assert_eq!((dirs, files, bytes), (1, 1, 3 + L1_TRAILER));
1479 assert_eq!(cache.clear().unwrap(), 3 + L1_TRAILER);
1480 assert!(cache.get(&key, "classes.dex", 3).is_none());
1481 }
1482
1483 #[test]
1484 fn disk_evicts_least_recently_used_apk() {
1485 let dir = tempfile::tempdir().unwrap();
1486 let cache = DiskCache::open(dir.path()).with_capacity(20);
1487 let old = ApkKey::from_id("old-0000000000000001".into());
1488 let new = ApkKey::from_id("new-0000000000000002".into());
1489 cache.put(&old, "classes.dex", &[0; 6]).unwrap();
1490 let later = SystemTime::now() + Duration::from_secs(5);
1492 cache.put(&new, "classes.dex", &[0; 6]).unwrap();
1493 fs::File::open(dir.path().join("new-0000000000000002")).unwrap().set_modified(later).unwrap();
1494 assert_eq!(cache.stats(), (2, 2, 2 * (6 + L1_TRAILER)));
1495 cache.evict();
1496 assert_eq!(cache.stats(), (1, 1, 6 + L1_TRAILER));
1497 assert!(cache.get(&new, "classes.dex", 6).is_some());
1498 assert!(cache.get(&old, "classes.dex", 6).is_none());
1499 }
1500
1501 const GIB: u64 = 1 << 30;
1502 const MIB: u64 = 1 << 20;
1503
1504 #[test]
1505 fn auto_disk_budget_follows_free_space() {
1506 assert_eq!(AutoDisk::default().bytes_for(None, 0), AUTO_DISK_UNKNOWN);
1508 assert_eq!(AutoDisk::default().bytes_for(Some((500 * GIB, 1000 * GIB)), 0), AutoDisk::CEILING);
1510 let tight = AutoDisk { percent: 10, ceiling: 5 * GIB };
1512 assert_eq!(tight.bytes_for(Some((100 * GIB, 500 * GIB)), 0), 5 * GIB);
1513 assert_eq!(tight.bytes_for(Some((20 * GIB, 50 * GIB)), 0), 20 * GIB / 100 * 10);
1514 assert_eq!(tight.bytes_for(None, 0), 4 * GIB);
1515 assert_eq!(AutoDisk::default().bytes_for(Some((100 * GIB, 500 * GIB)), 0), 100 * GIB / 100 * 20);
1517 assert_eq!(AutoDisk::default().bytes_for(Some((10 * GIB, 20 * GIB)), 0), 10 * GIB / 100 * 20);
1519 assert_eq!(AutoDisk::default().bytes_for(Some((9 * GIB, 20 * GIB)), GIB), 10 * GIB / 100 * 20);
1520 assert_eq!(AutoDisk::default().bytes_for(Some((GIB + 100 * MIB, 16 * GIB)), 0), 100 * MIB);
1522 assert_eq!(AutoDisk::default().bytes_for(Some((GIB, 16 * GIB)), 0), 0);
1523 }
1524
1525 #[test]
1526 fn auto_disk_budget_shrinks_below_usage_on_a_nearly_full_disk() {
1527 assert_eq!(AutoDisk::default().bytes_for(Some((GIB / 2, 100 * GIB)), 6 * GIB), (6 * GIB + GIB / 2) / 100 * 20);
1530 assert_eq!(AutoDisk::default().bytes_for(Some((GIB / 2, 100 * GIB)), 5 * GIB + GIB / 2), GIB);
1533 assert_eq!(AutoDisk::default().bytes_for(Some((GIB / 2, 100 * GIB)), 3 * GIB), 0);
1535 assert_eq!(auto_disk_reserve(100 * GIB), 5 * GIB);
1536 assert_eq!(auto_disk_reserve(4 * GIB), GIB);
1537 }
1538
1539 #[test]
1540 fn explicit_disk_capacity_wins() {
1541 let dir = tempfile::tempdir().unwrap();
1542 let cache = DiskCache::open(dir.path()).with_capacity_mode(DiskCapacity::default()).with_capacity(7);
1543 assert_eq!(cache.capacity_mode(), DiskCapacity::Fixed(7));
1544 assert_eq!(cache.capacity_for(1 << 40), 7);
1545 let b = cache.budget();
1546 assert_eq!((b.capacity, b.cap_bytes, b.used), (DiskCapacity::Fixed(7), 7, 0));
1547 assert_eq!(b.space.is_some(), cfg!(unix));
1549 let auto = DiskCache::open(dir.path()).with_capacity_mode(DiskCapacity::default());
1550 let ab = auto.budget();
1553 assert_eq!(ab.cap_bytes, AutoDisk::default().bytes_for(ab.space, ab.used));
1554 assert_eq!(disk_space(&dir.path().join("a/b")).is_some(), cfg!(unix));
1556 }
1557
1558 #[test]
1559 fn eviction_follows_a_lowered_capacity() {
1560 let dir = tempfile::tempdir().unwrap();
1561 let keys: Vec<ApkKey> = (0..3).map(|i| ApkKey::from_id(format!("app-000000000000000{i}"))).collect();
1562 let cache = DiskCache::open(dir.path()).with_capacity(100);
1563 for (i, k) in keys.iter().enumerate() {
1564 cache.put(k, "classes.dex", &[0; 10]).unwrap();
1565 let t = SystemTime::now() + Duration::from_secs(10 * i as u64);
1566 fs::File::open(dir.path().join(k.as_str())).unwrap().set_modified(t).unwrap();
1567 }
1568 cache.evict();
1569 assert_eq!(cache.stats(), (3, 3, 3 * (10 + L1_TRAILER)));
1570 let cache = cache.with_capacity(25);
1573 cache.evict();
1574 assert_eq!(cache.stats(), (1, 1, 10 + L1_TRAILER));
1575 assert!(cache.get(&keys[2], "classes.dex", 10).is_some());
1576 }
1577
1578 #[test]
1579 fn cache_dir_names_are_recognised() {
1580 assert!(is_cache_dir_name("app-0123456789abcdef"));
1581 assert!(is_cache_dir_name("-0123456789abcdef")); assert!(is_cache_dir_name("my.app_v2-0123456789abcdef"));
1583 assert!(!is_cache_dir_name("Documents"));
1584 assert!(!is_cache_dir_name("app-0123456789ABCDEF")); assert!(!is_cache_dir_name("app-0123456789abcde")); assert!(!is_cache_dir_name("app 1-0123456789abcdef")); assert!(!is_cache_dir_name(&format!("{}-0123456789abcdef", "a".repeat(41))));
1588 }
1589
1590 #[test]
1591 fn unrelated_directories_survive_evict_and_clear() {
1592 let dir = tempfile::tempdir().unwrap();
1593 let cache = DiskCache::open(dir.path()).with_capacity(4);
1594 let key = ApkKey::from_id("app-0123456789abcdef".into());
1595 cache.put(&key, "classes.dex", &[0; 8]).unwrap();
1597 assert!(dir.path().join(CACHEDIR_TAG).is_file());
1598 let thesis = dir.path().join("my-thesis");
1600 fs::create_dir(&thesis).unwrap();
1601 fs::write(thesis.join("chapter1.txt"), vec![0u8; 100]).unwrap();
1602 fs::write(dir.path().join("notes.txt"), b"keep").unwrap();
1603 assert_eq!(cache.stats(), (1, 1, 8 + L1_TRAILER));
1605 cache.evict();
1606 assert!(thesis.join("chapter1.txt").is_file());
1607 assert!(cache.get(&key, "classes.dex", 8).is_none());
1608 cache.put(&key, "classes.dex", &[0; 8]).unwrap();
1609 assert_eq!(cache.clear().unwrap(), 8 + L1_TRAILER);
1610 assert!(thesis.join("chapter1.txt").is_file());
1611 assert!(dir.path().join("notes.txt").is_file());
1612 assert!(dir.path().join(CACHEDIR_TAG).is_file());
1613 assert!(dir.path().is_dir());
1614 assert_eq!(cache.stats(), (0, 0, 0));
1615 }
1616
1617 #[test]
1618 fn foreign_directories_are_refused_and_never_tagged() {
1619 let dir = tempfile::tempdir().unwrap();
1620 let cache = DiskCache::open(dir.path()).with_capacity(1);
1621 let lookalike = dir.path().join("app-0123456789abcdef");
1623 fs::create_dir(&lookalike).unwrap();
1624 fs::write(lookalike.join("classes.dex"), [0; 16]).unwrap();
1625 fs::write(dir.path().join("notes.txt"), b"keep").unwrap();
1626 cache.evict();
1627 assert!(lookalike.join("classes.dex").is_file());
1628 assert!(matches!(cache.clear(), Err(CacheError::NotACacheDir { .. })));
1629 assert!(lookalike.join("classes.dex").is_file());
1630 let key = ApkKey::from_id("other-fedcba9876543210".into());
1633 assert!(matches!(cache.put(&key, "classes.dex", &[0; 4]), Err(CacheError::NotACacheDir { .. })));
1634 assert!(!dir.path().join(CACHEDIR_TAG).exists());
1635 assert!(!dir.path().join("other-fedcba9876543210").exists());
1636 let empty = tempfile::tempdir().unwrap();
1638 assert_eq!(DiskCache::open(empty.path()).clear().unwrap(), 0);
1639 assert_eq!(DiskCache::open(empty.path().join("missing")).clear().unwrap(), 0);
1640 }
1641
1642 #[test]
1643 fn a_directory_holding_only_spilled_replies_is_still_a_cache() {
1644 let dir = tempfile::tempdir().unwrap();
1646 fs::create_dir(dir.path().join(SPILL_DIR)).unwrap();
1647 fs::write(dir.path().join(SPILL_DIR).join("ab.txt"), b"reply").unwrap();
1648 let cache = DiskCache::open(dir.path());
1649 assert!(cache.check_usable().is_ok());
1650 cache.put(&ApkKey::from_id("app-0123456789abcdef".into()), "classes.dex", &[0; 4]).unwrap();
1651 cache.clear().unwrap();
1652 assert!(!dir.path().join(SPILL_DIR).exists(), "clear removes spilled replies too");
1653 }
1654
1655 #[test]
1656 fn legacy_cache_directories_are_adopted() {
1657 let dir = tempfile::tempdir().unwrap();
1659 let cache = DiskCache::open(dir.path()).with_capacity(1);
1660 let legacy = dir.path().join("app-0123456789abcdef");
1661 fs::create_dir(&legacy).unwrap();
1662 fs::write(legacy.join("classes.dex"), [0; 16]).unwrap();
1663 assert_eq!(cache.stats(), (1, 1, 16));
1664 cache.evict();
1665 assert!(!legacy.exists(), "over capacity: evicted");
1666 fs::create_dir(&legacy).unwrap();
1667 fs::write(legacy.join("classes.dex"), [0; 16]).unwrap();
1668 assert_eq!(cache.clear().unwrap(), 16);
1669 let key = ApkKey::from_id("app-0123456789abcdef".into());
1671 cache.put(&key, "classes.dex", &[0; 4]).unwrap();
1672 assert!(dir.path().join(CACHEDIR_TAG).is_file());
1673 }
1674
1675 #[test]
1676 fn a_tag_that_is_not_a_file_is_not_a_tag() {
1677 let dir = tempfile::tempdir().unwrap();
1678 let cache = DiskCache::open(dir.path()).with_capacity(1);
1679 fs::create_dir(dir.path().join(CACHEDIR_TAG)).unwrap();
1680 fs::write(dir.path().join("notes.txt"), b"keep").unwrap();
1681 let key = ApkKey::from_id("app-0123456789abcdef".into());
1682 assert!(matches!(cache.put(&key, "classes.dex", &[0; 4]), Err(CacheError::NotACacheDir { .. })));
1683 assert!(matches!(cache.clear(), Err(CacheError::NotACacheDir { .. })));
1684 }
1685
1686 #[test]
1687 fn dot_files_make_a_directory_foreign() {
1688 let dir = tempfile::tempdir().unwrap();
1690 let cache = DiskCache::open(dir.path());
1691 fs::write(dir.path().join(".gitignore"), b"*\n").unwrap();
1692 assert!(matches!(cache.check_usable(), Err(CacheError::NotACacheDir { .. })));
1693 let key = ApkKey::from_id("app-0123456789abcdef".into());
1694 assert!(matches!(cache.put(&key, "classes.dex", &[0; 4]), Err(CacheError::NotACacheDir { .. })));
1695 assert!(!dir.path().join(CACHEDIR_TAG).exists());
1696 let legacy = tempfile::tempdir().unwrap();
1698 fs::create_dir(legacy.path().join("app-0123456789abcdef")).unwrap();
1699 fs::write(legacy.path().join(".DS_Store"), b"").unwrap();
1700 assert!(DiskCache::open(legacy.path()).check_usable().is_ok());
1701 }
1702
1703 #[test]
1704 fn memory_lru_evicts_by_bytes() {
1705 let mut m: MemoryCache<Vec<u8>> = MemoryCache::new(10);
1706 let k = ApkKey::from_id("k".into());
1707 m.insert(&k, "a", Arc::new(vec![0; 4]), 4);
1708 m.insert(&k, "b", Arc::new(vec![0; 4]), 4);
1709 assert!(m.get(&k, "a").is_some()); m.insert(&k, "c", Arc::new(vec![0; 4]), 4); assert!(m.get(&k, "b").is_none());
1712 assert!(m.get(&k, "a").is_some());
1713 assert_eq!(m.used_bytes(), 8);
1714 assert_eq!(m.len(), 2);
1715 m.insert(&k, "a", Arc::new(vec![0; 2]), 2);
1717 assert_eq!(m.used_bytes(), 6);
1718 m.clear();
1719 assert!(m.is_empty());
1720 }
1721
1722 #[test]
1723 fn memory_lru_resize_charges_and_evicts() {
1724 let mut m: MemoryCache<Vec<u8>> = MemoryCache::new(10);
1725 let k = ApkKey::from_id("k".into());
1726 let a = Arc::new(vec![0; 4]);
1727 m.insert(&k, "a", Arc::clone(&a), 4);
1728 let b = Arc::new(vec![0; 4]);
1729 m.insert(&k, "b", Arc::clone(&b), 4);
1730 m.resize(&k, "b", &b, 7);
1732 assert_eq!(m.used_bytes(), 7);
1733 assert!(m.get(&k, "a").is_none());
1734 assert_eq!(m.charged_bytes(&k, "b"), Some(7));
1735 let b2 = Arc::new(vec![1; 4]);
1737 m.insert(&k, "b", Arc::clone(&b2), 4);
1738 m.resize(&k, "b", &b, 9);
1739 assert_eq!(m.charged_bytes(&k, "b"), Some(4));
1740 m.resize(&k, "b", &b2, 50);
1742 assert_eq!((m.len(), m.used_bytes()), (1, 50));
1743 }
1744
1745 #[test]
1746 fn memory_lru_capacity_can_shrink_and_peek_keeps_recency() {
1747 let mut m: MemoryCache<u8> = MemoryCache::new(10);
1748 let k = ApkKey::from_id("k".into());
1749 m.insert(&k, "a", Arc::new(1), 4);
1750 m.insert(&k, "b", Arc::new(2), 4);
1751 assert_eq!(*m.peek(&k, "a").unwrap(), 1); m.set_capacity(5);
1753 assert_eq!((m.capacity_bytes(), m.used_bytes()), (5, 4));
1754 assert!(m.peek(&k, "a").is_none());
1755 assert!(m.peek(&k, "b").is_some());
1756 }
1757
1758 #[test]
1759 fn memory_lru_reserved_bytes_make_room_under_the_cap() {
1760 let mut m: MemoryCache<u8> = MemoryCache::new(10);
1761 let k = ApkKey::from_id("k".into());
1762 m.insert(&k, "a", Arc::new(1), 4);
1763 m.insert(&k, "b", Arc::new(2), 4);
1764 m.set_reserved(3);
1765 assert_eq!((m.used_bytes(), m.reserved_bytes()), (4, 3), "a evicted to fit 3 reserved bytes");
1766 assert!(m.peek(&k, "a").is_none());
1767 m.insert(&k, "c", Arc::new(3), 3);
1768 assert_eq!(m.used_bytes(), 7);
1769 m.set_reserved(0);
1770 m.insert(&k, "d", Arc::new(4), 3);
1771 assert_eq!(m.used_bytes(), 10, "nothing reserved: the whole cap holds entries");
1772 }
1773
1774 #[test]
1775 fn memory_lru_replace_keeps_size_and_recency() {
1776 let mut m: MemoryCache<Vec<u8>> = MemoryCache::new(10);
1777 let k = ApkKey::from_id("k".into());
1778 let a = Arc::new(vec![0; 4]);
1779 m.insert(&k, "a", Arc::clone(&a), 4);
1780 m.insert(&k, "b", Arc::new(vec![0; 4]), 4);
1781 assert!(m.get(&k, "a").is_some()); let a2 = Arc::new(vec![9; 4]);
1783 assert!(m.replace(&k, "a", &a, Arc::clone(&a2)));
1784 assert!(!m.replace(&k, "a", &a, Arc::new(vec![])), "stale handle");
1785 assert!(!m.replace(&k, "missing", &a, Arc::new(vec![])));
1786 assert_eq!(m.used_bytes(), 8);
1787 m.insert(&k, "c", Arc::new(vec![0; 4]), 4); assert!(Arc::ptr_eq(&m.get(&k, "a").unwrap(), &a2));
1789 assert!(m.get(&k, "b").is_none());
1790 }
1791
1792 #[test]
1793 fn fnv_lanes_match_scalar() {
1794 fn scalar(bytes: &[u8]) -> u64 {
1795 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1796 for &b in bytes {
1797 h ^= b as u64;
1798 h = h.wrapping_mul(0x0100_0000_01b3);
1799 }
1800 h
1801 }
1802 let a = fnv64(b"PK\x01\x02classes.dex");
1805 let b = fnv64(b"PK\x01\x02classes2.dex");
1806 assert_ne!(a, b);
1807 assert_ne!(fnv64(b""), fnv64(b"\0"));
1808 assert_eq!(fnv64(b"abc"), scalar(b"abc")); }
1810
1811 fn set_mtime(path: &Path, secs_from_now: i64) {
1812 let t = if secs_from_now >= 0 {
1813 SystemTime::now() + Duration::from_secs(secs_from_now as u64)
1814 } else {
1815 SystemTime::now() - Duration::from_secs((-secs_from_now) as u64)
1816 };
1817 fs::File::open(path).unwrap().set_modified(t).unwrap();
1818 }
1819
1820 #[test]
1821 fn files_are_checked_when_they_predate_the_boot() {
1822 let now = SystemTime::now();
1823 let boot = Some(now - Duration::from_secs(60));
1824 assert!(predates_boot(Some(now - Duration::from_secs(61)), boot, now));
1825 assert!(!predates_boot(Some(now - Duration::from_secs(59)), boot, now));
1826 assert!(predates_boot(Some(now + Duration::from_secs(5)), boot, now));
1828 assert!(predates_boot(Some(now), None, now));
1829 assert!(predates_boot(None, boot, now));
1830 }
1831
1832 fn booted_cache(dir: &Path) -> DiskCache {
1834 let mut cache = DiskCache::open(dir).with_capacity(1 << 20);
1835 cache.boot = Some(SystemTime::now() - Duration::from_secs(60));
1836 cache
1837 }
1838
1839 fn age(path: &Path, damage: Option<(u64, &[u8])>, cut: Option<u64>) {
1842 let f = fs::OpenOptions::new().write(true).open(path).unwrap();
1843 if let Some((at, bytes)) = damage {
1844 (&f).seek(SeekFrom::Start(at)).unwrap();
1845 (&f).write_all(bytes).unwrap();
1846 }
1847 if let Some(len) = cut {
1848 f.set_len(len).unwrap();
1849 }
1850 f.set_modified(SystemTime::now() - Duration::from_secs(120)).unwrap();
1851 }
1852
1853 #[test]
1854 fn l1_files_from_an_earlier_boot_are_checked() {
1855 let dir = tempfile::tempdir().unwrap();
1856 let cache = booted_cache(dir.path());
1857 let key = ApkKey::from_id("app-0123456789abcdef".into());
1858 let data: Vec<u8> = (0..4096u32).map(|i| i as u8).collect();
1859 let path = cache.entry_path(&key, "classes.dex");
1860 let n = data.len() as u64;
1861
1862 cache.put(&key, "classes.dex", &data).unwrap();
1864 age(&path, None, None);
1865 assert_eq!(&*cache.get(&key, "classes.dex", n).unwrap(), &data[..]);
1866 assert!(!cache.predates_boot(&fs::metadata(&path).unwrap()));
1867
1868 let flipped = [!data[7]];
1870 for (at, bytes) in [(1024, &[0u8; 1024][..]), (7, &flipped[..])] {
1871 cache.put(&key, "classes.dex", &data).unwrap();
1872 age(&path, Some((at, bytes)), None);
1873 assert!(cache.get(&key, "classes.dex", n).is_none(), "damage at {at}");
1874 assert!(!path.exists(), "damage at {at}");
1875 }
1876 for cut in [n, n - 100] {
1879 cache.put(&key, "classes.dex", &data).unwrap();
1880 age(&path, None, Some(cut));
1881 assert!(cache.get(&key, "classes.dex", 0).is_none(), "cut to {cut}");
1882 assert!(!path.exists(), "cut to {cut}");
1883 }
1884
1885 cache.put(&key, "classes.dex", &data).unwrap();
1887 let f = fs::OpenOptions::new().write(true).open(&path).unwrap();
1888 (&f).write_all(&[!data[0]]).unwrap();
1889 f.set_modified(SystemTime::now()).unwrap();
1890 assert_eq!(cache.get(&key, "classes.dex", n).unwrap()[0], !data[0]);
1891 }
1892
1893 #[test]
1894 fn l2_and_l3_files_carry_checksums() {
1895 let dir = tempfile::tempdir().unwrap();
1896 let cache = booted_cache(dir.path());
1897 let key = ApkKey::from_id("app-0123456789abcdef".into());
1898 let path = cache.derived_path(&key, "classes.dex", "locators", 1);
1899 let flip = DERIVED_HEADER as u64 + 3;
1900 cache.put_derived(&key, "classes.dex", "locators", 1, Duration::from_millis(7), b"tables").unwrap();
1901 age(&path, None, None);
1902 assert_eq!(&*cache.get_derived(&key, "classes.dex", "locators", 1).unwrap(), b"tables");
1903 age(&path, Some((flip, b"X")), None);
1904 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
1905 assert!(!path.exists());
1906
1907 cache.put_rendered(&key, "q", b"rows").unwrap();
1909 assert_eq!(cache.get_rendered(&key, "q").unwrap(), b"rows");
1910 let path = cache.rendered_path(&key, "q");
1911 let len = fs::metadata(&path).unwrap().len();
1912 let f = fs::OpenOptions::new().write(true).open(&path).unwrap();
1913 (&f).seek(SeekFrom::Start(len - 1)).unwrap();
1914 (&f).write_all(b"X").unwrap();
1915 assert!(cache.get_rendered(&key, "q").is_none());
1916 assert!(!path.exists());
1917 }
1918
1919 #[test]
1920 fn derived_roundtrip_is_versioned_and_aligned() {
1921 let dir = tempfile::tempdir().unwrap();
1922 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
1923 let key = ApkKey::from_id("app-0123456789abcdef".into());
1924 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
1925 cache.put_derived(&key, "classes.dex", "locators", 1, Duration::from_millis(7), b"tables").unwrap();
1926 let d = cache.get_derived(&key, "classes.dex", "locators", 1).unwrap();
1927 assert_eq!(&*d, b"tables");
1928 assert_eq!(d.build_cost, Duration::from_millis(7));
1929 assert_eq!(d.as_ptr() as usize % 64, 0, "payload as aligned as the mapping");
1930 assert!(cache.get_derived(&key, "classes.dex", "locators", 2).is_none());
1932 assert!(cache.get_derived(&key, "classes.dex", "callgraph", 1).is_none());
1933 assert!(cache.get_derived(&key, "classes2.dex", "locators", 1).is_none());
1934 let path = cache.derived_path(&key, "classes.dex", "locators", 1);
1936 let full = fs::read(&path).unwrap();
1937 fs::write(&path, &full[..full.len() - 1]).unwrap();
1938 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
1939 fs::write(&path, &full[..10]).unwrap();
1940 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
1941 let [l1, l2, l3] = cache.tier_stats();
1942 assert_eq!((l1.files, l2.files, l3.files), (0, 1, 0));
1943 }
1944
1945 #[test]
1946 fn rendered_results_are_keyed_by_the_full_query() {
1947 let dir = tempfile::tempdir().unwrap();
1948 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
1949 let key = ApkKey::from_id("app-0123456789abcdef".into());
1950 let q = rendered_key("findrefs", "1.0", "string\0token");
1951 assert!(cache.get_rendered(&key, &q).is_none());
1952 cache.put_rendered(&key, &q, b"rows").unwrap();
1953 assert_eq!(cache.get_rendered(&key, &q).unwrap(), b"rows");
1954 assert!(cache.get_rendered(&key, &rendered_key("findrefs", "1.1", "string\0token")).is_none(), "tool upgrade");
1955 let other = rendered_key("findrefs", "1.0", "string\0other");
1957 fs::rename(cache.rendered_path(&key, &q), cache.rendered_path(&key, &other)).unwrap();
1958 assert!(cache.get_rendered(&key, &other).is_none());
1959 }
1960
1961 #[test]
1962 fn stats_and_clear_cover_every_tier() {
1963 let dir = tempfile::tempdir().unwrap();
1964 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
1965 let (a, b) = (ApkKey::from_id("a-0123456789abcdef".into()), ApkKey::from_id("b-0123456789abcdef".into()));
1966 cache.put(&a, "classes.dex", &[0; 10]).unwrap();
1967 cache.put_derived(&a, "classes.dex", "locators", 1, Duration::ZERO, &[0; 6]).unwrap();
1968 cache.put_rendered(&b, "q", &[0; 3]).unwrap();
1969 let (apks, files, bytes) = cache.stats();
1970 assert_eq!((apks, files), (2, 3));
1971 assert_eq!(bytes, (10 + L1_TRAILER) + (64 + 6) + (16 + 1 + 3));
1972 let [l1, l2, l3] = cache.tier_stats();
1973 assert_eq!((l1.apks, l2.apks, l3.apks), (1, 1, 1));
1974 fs::remove_file(dir.path().join(CACHEDIR_TAG)).unwrap();
1976 assert!(cache.check_usable().is_ok());
1977 assert_eq!(cache.clear().unwrap(), bytes);
1978 assert_eq!(cache.stats(), (0, 0, 0));
1979 assert!(!dir.path().join("derived").exists() && !dir.path().join("rendered").exists());
1980 }
1981
1982 #[test]
1983 fn disabled_tiers_miss_and_skip_writes() {
1984 let dir = tempfile::tempdir().unwrap();
1985 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20).with_tiers(&[Tier::Derived]);
1986 let key = ApkKey::from_id("app-0123456789abcdef".into());
1987 cache.put(&key, "classes.dex", b"abc").unwrap();
1988 cache.put_rendered(&key, "q", b"rows").unwrap();
1989 cache.put_derived(&key, "classes.dex", "k", 1, Duration::ZERO, b"t").unwrap();
1990 assert_eq!(cache.tier_stats().map(|t| t.files), [0, 1, 0]);
1991 let all = DiskCache::open(dir.path()).with_capacity(1 << 20).with_tiers(&Tier::ALL);
1992 all.put(&key, "classes.dex", b"abc").unwrap();
1993 assert!(cache.get(&key, "classes.dex", 3).is_none());
1994 assert!(cache.uses(Tier::Derived) && !cache.uses(Tier::Dex));
1995 }
1996
1997 #[test]
1998 fn lazy_eviction_skips_a_walk_right_after_another() {
1999 let dir = tempfile::tempdir().unwrap();
2000 let cache = DiskCache::open(dir.path()).with_capacity(1 << 30);
2001 let key = ApkKey::from_id("app-0123456789abcdef".into());
2002 cache.put(&key, "classes.dex", &[0; 100]).unwrap();
2003 cache.evict();
2004 assert!(dir.path().join(EVICT_MARKER).is_file());
2005 assert!(cache.check_usable().is_ok(), "the marker is the cache's own");
2006 let cache = cache.with_capacity(50);
2008 cache.evict_lazily();
2009 assert_eq!(cache.stats().1, 1);
2010 cache.put(&key, "classes2.dex", &[0; 100]).unwrap();
2012 cache.evict_lazily();
2013 assert_eq!(cache.stats().1, 0);
2014 }
2015
2016 #[test]
2017 fn small_dex_entries_are_not_admitted_by_default() {
2018 let dir = tempfile::tempdir().unwrap();
2019 let cache = DiskCache::open(dir.path());
2020 assert_eq!(cache.min_dex_bytes(), DEFAULT_MIN_DEX_BYTES);
2021 let cache = cache.with_min_dex_bytes(100);
2022 assert!(!cache.admits_dex(99));
2023 assert!(cache.admits_dex(100));
2024 assert!(cache.with_min_dex_bytes(0).admits_dex(0));
2025 }
2026
2027 #[test]
2028 fn eviction_trims_tiers_then_goes_l1_l3_l2() {
2029 let dir = tempfile::tempdir().unwrap();
2030 let cache = DiskCache::open(dir.path()).with_capacity(1000);
2032 let k = |i: u32| ApkKey::from_id(format!("app-{i:016x}"));
2033 for i in 0..4 {
2035 let q = format!("q{i}");
2036 cache.put_rendered(&k(0), &q, &[0; 64]).unwrap();
2037 set_mtime(&cache.rendered_path(&k(0), &q), -100 + i as i64);
2038 }
2039 cache.evict();
2040 let [_, _, l3] = cache.tier_stats();
2041 assert_eq!(l3.files, 1, "trimmed to its own budget, newest kept");
2042 assert!(cache.get_rendered(&k(0), "q3").is_some());
2043 cache.put_derived(&k(1), "classes.dex", "cheap", 1, Duration::from_micros(10), &[0; 136]).unwrap();
2045 cache.put_derived(&k(1), "classes.dex", "dear", 1, Duration::from_secs(1), &[0; 136]).unwrap();
2046 cache.put(&k(2), "classes.dex", &[0; 300]).unwrap();
2047 cache.put(&k(3), "classes.dex", &[0; 300]).unwrap();
2048 set_mtime(&dir.path().join(k(2).as_str()), -50);
2049 cache.evict();
2051 assert!(cache.get(&k(2), "classes.dex", 300).is_none());
2052 assert!(cache.get(&k(3), "classes.dex", 300).is_some());
2053 assert_eq!(cache.tier_stats().map(|t| t.files), [1, 2, 1]);
2054 let cache = cache.with_capacity(450);
2057 cache.evict();
2058 assert_eq!(cache.tier_stats().map(|t| t.files), [0, 1, 0]);
2059 assert!(cache.get_derived(&k(1), "classes.dex", "dear", 1).is_some());
2060 }
2061
2062 #[test]
2063 fn l1_eviction_weighs_size_with_age() {
2064 let dir = tempfile::tempdir().unwrap();
2065 let cache = DiskCache::open(dir.path()).with_capacity(600);
2066 let (big, small) = (ApkKey::from_id("big-0000000000000001".into()), ApkKey::from_id("small-0000000000000002".into()));
2067 cache.put(&small, "classes.dex", &[0; 100]).unwrap();
2069 cache.put(&big, "classes.dex", &[0; 550]).unwrap();
2070 set_mtime(&dir.path().join(small.as_str()), -3);
2071 cache.evict();
2072 assert!(cache.get(&small, "classes.dex", 100).is_some());
2073 assert!(cache.get(&big, "classes.dex", 550).is_none());
2074 }
2075
2076 #[test]
2077 fn entries_of_deleted_apks_go_first() {
2078 let dir = tempfile::tempdir().unwrap();
2079 let apks = tempfile::tempdir().unwrap();
2080 let cache = DiskCache::open(dir.path()).with_capacity(1000);
2081 let (live, dead) = (ApkKey::from_id("live-0000000000000001".into()), ApkKey::from_id("dead-0000000000000002".into()));
2082 let (live_apk, dead_apk) = (apks.path().join("live.apk"), apks.path().join("dead.apk"));
2083 fs::write(&live_apk, b"PK").unwrap();
2084 fs::write(&dead_apk, b"PK").unwrap();
2085 cache.note_source(&live, &live_apk).unwrap();
2086 cache.note_source(&dead, &dead_apk).unwrap();
2087 cache.put(&live, "classes.dex", &[0; 400]).unwrap();
2088 cache.put_derived(&dead, "classes.dex", "locators", 1, Duration::from_secs(9), &[0; 100]).unwrap();
2090 cache.put(&dead, "classes.dex", &[0; 400]).unwrap();
2091 set_mtime(&dir.path().join(live.as_str()), -100);
2092 fs::remove_file(&dead_apk).unwrap();
2093 cache.evict();
2095 assert_eq!(cache.stats().1, 3);
2096 let cache = cache.with_capacity(500);
2097 cache.evict();
2098 assert!(cache.get(&live, "classes.dex", 400).is_some());
2099 assert!(cache.get(&dead, "classes.dex", 400).is_none());
2100 assert!(cache.get_derived(&dead, "classes.dex", "locators", 1).is_none());
2101 assert!(!dir.path().join("sources").join(dead.as_str()).exists(), "source record dropped with its entries");
2102 assert!(dir.path().join("sources").join(live.as_str()).exists());
2103 }
2104}