1mod lock;
47mod procs;
48mod threads;
49
50pub use lock::LockExt;
51pub use procs::{ProcessRegistry, ProcessUsage};
52pub use threads::{Threads, auto_threads, auto_threads_for, cpus};
53
54use std::collections::{HashMap, HashSet};
55use std::fs;
56use std::io::{Read, Seek, SeekFrom, Write};
57use std::ops::Deref;
58use std::path::{Path, PathBuf};
59use std::sync::{Arc, OnceLock};
60use std::time::{Duration, SystemTime};
61
62pub const MAPPED_CHARGE_DIVISOR: usize = 8;
75
76#[derive(Debug, Clone)]
85pub struct ApkKey {
86 id: String,
87 archives: Arc<[ArchiveKey]>,
89}
90
91#[derive(Debug)]
93struct ArchiveKey {
94 label: String,
96 path: PathBuf,
97 key: ApkKey,
98}
99
100impl PartialEq for ApkKey {
101 fn eq(&self, other: &ApkKey) -> bool {
102 self.id == other.id
103 }
104}
105
106impl Eq for ApkKey {}
107
108impl std::hash::Hash for ApkKey {
109 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
110 self.id.hash(state);
111 }
112}
113
114fn is_key_char(c: char) -> bool {
116 c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'
117}
118const STEM_MAX: usize = 40;
120const HASH_HEX_LEN: usize = 16;
122
123impl ApkKey {
124 pub fn for_apk(apk: &asc_apk::Apk) -> asc_apk::Result<ApkKey> {
134 let ids = apk.directories()?;
135 let mut key = 0u64;
136 let mut archives = Vec::with_capacity(if ids.len() > 1 { ids.len() } else { 0 });
137 for (i, id) in ids.iter().enumerate() {
138 let meta = fs::metadata(id.path).map_err(|source| asc_apk::ApkError::Io { path: id.path.to_path_buf(), source })?;
139 let mtime = meta.modified().ok().and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()).map(|d| d.as_nanos()).unwrap_or(0);
140 let mut h = fnv64(id.directory);
141 h ^= meta.len().wrapping_mul(0x9E37_79B9_7F4A_7C15);
142 h ^= (mtime as u64).rotate_left(17);
143 if ids.len() > 1 {
144 let stem = id.path.file_stem().map(|s| s.to_string_lossy()).unwrap_or_default();
146 let key = ApkKey::lone(&stem, h);
147 archives.push(ArchiveKey { label: id.label.to_string(), path: id.path.to_path_buf(), key });
148 }
149 key = if i == 0 { h } else { key.rotate_left(11) ^ h ^ fnv64(id.path.as_os_str().as_encoded_bytes()).rotate_left(29) };
151 }
152 Ok(ApkKey { archives: archives.into(), ..ApkKey::lone(&apk.stem(), key) })
153 }
154
155 pub fn for_content(label: &str, sha256: &[u8; 32]) -> ApkKey {
166 let name = label.rsplit(['/', '\\']).next().unwrap_or(label);
167 let stem = Path::new(name).file_stem().map_or_else(|| name.into(), |stem| stem.to_string_lossy());
168 let mut prefix = [0u8; 8];
169 prefix.copy_from_slice(&sha256[..8]);
170 ApkKey::lone(&stem, u64::from_be_bytes(prefix))
171 }
172
173 fn lone(stem: &str, hash: u64) -> ApkKey {
175 let stem: String = stem.chars().filter(|c| is_key_char(*c)).take(STEM_MAX).collect();
176 ApkKey::from_id(format!("{stem}-{hash:0width$x}", width = HASH_HEX_LEN))
177 }
178
179 fn from_id(id: String) -> ApkKey {
180 ApkKey { id, archives: Arc::new([]) }
181 }
182
183 pub fn as_str(&self) -> &str {
186 &self.id
187 }
188
189 pub fn locate<'n>(&self, name: &'n str) -> (&ApkKey, &'n str) {
194 let found = asc_apk::archive_of(name).and_then(|label| {
195 let a = self.archives.iter().find(|a| a.label == label)?;
196 Some((&a.key, &name[label.len() + 1..]))
197 });
198 found.unwrap_or((self, name))
199 }
200}
201
202fn fnv64(bytes: &[u8]) -> u64 {
205 const PRIME: u64 = 0x0100_0000_01b3;
206 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
207 let (chunks, rest) = bytes.as_chunks::<8>();
208 for c in chunks {
209 h ^= u64::from_le_bytes(*c);
210 h = h.wrapping_mul(PRIME);
211 }
212 for &b in rest {
213 h ^= b as u64;
214 h = h.wrapping_mul(PRIME);
215 }
216 h
217}
218
219#[non_exhaustive]
221pub enum DexData {
222 Owned(Vec<u8>),
224 Mapped(memmap2::Mmap),
226}
227
228impl Deref for DexData {
229 type Target = [u8];
230 fn deref(&self) -> &[u8] {
231 match self {
232 DexData::Owned(v) => v,
233 DexData::Mapped(m) => m,
234 }
235 }
236}
237
238impl DexData {
239 pub fn map_region(path: &Path, offset: u64, len: usize) -> std::io::Result<DexData> {
250 let file = fs::File::open(path)?;
251 if file.metadata()?.len() < offset.saturating_add(len as u64) {
252 return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "region past the end of the file"));
253 }
254 let map = unsafe { memmap2::MmapOptions::new().offset(offset).len(len).map(&file) }?;
257 Ok(DexData::Mapped(map))
258 }
259}
260
261impl AsRef<[u8]> for DexData {
262 fn as_ref(&self) -> &[u8] {
263 self
264 }
265}
266
267#[derive(Debug, thiserror::Error)]
269#[non_exhaustive]
270pub enum CacheError {
271 #[error("cache io error at {path}: {source}")]
273 Io {
274 path: PathBuf,
276 source: std::io::Error,
278 },
279 #[error("{path} is not a asc cache directory (no {tag} marker); refusing to touch it")]
282 NotACacheDir {
283 path: PathBuf,
285 tag: &'static str,
287 },
288 #[error("{path}: too little free disk space left to cache {bytes} bytes")]
291 LowDiskSpace {
292 path: PathBuf,
294 bytes: u64,
296 },
297}
298
299fn io_err(path: &Path) -> impl Fn(std::io::Error) -> CacheError + '_ {
300 move |source| CacheError::Io { path: path.to_path_buf(), source }
301}
302
303pub struct DiskCache {
305 dir: PathBuf,
306 cap: DiskCapacity,
307 min_dex_bytes: u64,
308 min_derived_cost: Duration,
309 tiers: [bool; 3],
311 written: std::sync::atomic::AtomicU64,
313 boot: Option<SystemTime>,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
320#[non_exhaustive]
321pub enum Tier {
322 Dex,
324 Derived,
326 Rendered,
328}
329
330impl Tier {
331 pub const ALL: [Tier; 3] = [Tier::Dex, Tier::Derived, Tier::Rendered];
333
334 pub fn name(self) -> &'static str {
336 match self {
337 Tier::Dex => "dex",
338 Tier::Derived => "derived",
339 Tier::Rendered => "rendered",
340 }
341 }
342
343 pub fn label(self) -> &'static str {
345 match self {
346 Tier::Dex => "L1 dex",
347 Tier::Derived => "L2 derived",
348 Tier::Rendered => "L3 rendered",
349 }
350 }
351}
352
353const DERIVED_DIR: &str = "derived";
355const RENDERED_DIR: &str = "rendered";
357const SOURCES_DIR: &str = "sources";
359pub const SPILL_DIR: &str = "out";
362pub const DECOMPILERS_DIR: &str = "decompilers";
365
366pub const RENDERED_MAX: u64 = 128 << 20;
369pub const RENDERED_BUDGET_DIVISOR: u64 = 8;
371pub const DERIVED_BUDGET_DIVISOR: u64 = 2;
373
374pub const DEFAULT_MIN_DEX_BYTES: u64 = 4 << 20;
377pub const DEFAULT_MIN_DERIVED_COST: Duration = Duration::from_millis(2);
380
381const DERIVED_MAGIC: &[u8; 8] = b"ASCL2\0\0\0";
384const DERIVED_HEADER: usize = 64;
385const DERIVED_FORMAT: u32 = 2;
387const RENDERED_MAGIC: &[u8; 8] = b"ASCL3\0\0\0";
388const RENDERED_FORMAT: u32 = 2;
390const L1_MAGIC: &[u8; 4] = b"ASC1";
393const L1_TRAILER: u64 = 8;
394
395#[non_exhaustive]
398pub struct Derived {
399 map: memmap2::Mmap,
400 pub build_cost: Duration,
402}
403
404#[derive(Debug)]
406pub struct Rendered {
407 map: memmap2::Mmap,
408 body: usize,
409}
410
411impl AsRef<[u8]> for Rendered {
412 fn as_ref(&self) -> &[u8] {
413 &self.map[self.body..]
414 }
415}
416
417impl Deref for Derived {
418 type Target = [u8];
419 fn deref(&self) -> &[u8] {
420 &self.map[DERIVED_HEADER..]
421 }
422}
423
424impl AsRef<[u8]> for Derived {
425 fn as_ref(&self) -> &[u8] {
426 self
427 }
428}
429
430#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
432#[non_exhaustive]
433pub struct TierStats {
434 pub apks: usize,
436 pub files: usize,
438 pub bytes: u64,
440}
441
442struct Item {
444 tier: Tier,
445 key: String,
446 path: PathBuf,
447 mtime: SystemTime,
448 files: usize,
449 bytes: u64,
450 cost_us: u64,
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub enum DiskCapacity {
457 Auto(AutoDisk),
459 Fixed(u64),
461}
462
463impl Default for DiskCapacity {
464 fn default() -> DiskCapacity {
465 DiskCapacity::Auto(AutoDisk::default())
466 }
467}
468
469pub const AUTO_DISK_UNKNOWN: u64 = 4 << 30;
471pub const AUTO_DISK_RESERVE_MIN: u64 = 1 << 30;
474pub const AUTO_DISK_RESERVE_PERCENT: u64 = 5;
477
478pub fn auto_disk_reserve(total: u64) -> u64 {
481 AUTO_DISK_RESERVE_MIN.max(total / 100 * AUTO_DISK_RESERVE_PERCENT)
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488#[non_exhaustive]
489pub struct AutoDisk {
490 pub percent: u8,
492 pub ceiling: u64,
494}
495
496impl AutoDisk {
497 pub const PERCENT: u8 = 20;
499 pub const CEILING: u64 = 32 << 30;
502
503 pub fn new(percent: u8, ceiling: u64) -> AutoDisk {
505 AutoDisk { percent, ceiling }
506 }
507
508 pub fn bytes_for(&self, space: Option<(u64, u64)>, used: u64) -> u64 {
516 let Some((free, total)) = space else { return AUTO_DISK_UNKNOWN.min(self.ceiling) };
517 let room = used.saturating_add(free);
518 let budget = (room / 100 * u64::from(self.percent)).min(self.ceiling);
519 budget.min(room.saturating_sub(auto_disk_reserve(total)))
520 }
521}
522
523impl Default for AutoDisk {
524 fn default() -> AutoDisk {
525 AutoDisk { percent: AutoDisk::PERCENT, ceiling: AutoDisk::CEILING }
526 }
527}
528
529pub fn disk_space(path: &Path) -> Option<(u64, u64)> {
532 let mut p = path;
533 loop {
534 if p.exists() {
535 return fs_space(p);
536 }
537 p = p.parent()?;
538 }
539}
540
541#[cfg(unix)]
542fn fs_space(path: &Path) -> Option<(u64, u64)> {
543 use std::os::unix::ffi::OsStrExt;
544 let c = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
545 let mut st = std::mem::MaybeUninit::<libc::statvfs>::uninit();
546 if unsafe { libc::statvfs(c.as_ptr(), st.as_mut_ptr()) } != 0 {
548 return None;
549 }
550 let st = unsafe { st.assume_init() };
552 #[allow(clippy::useless_conversion, clippy::unnecessary_cast)]
554 let (frsize, bavail, blocks) = (st.f_frsize as u64, st.f_bavail as u64, st.f_blocks as u64);
555 Some((bavail.saturating_mul(frsize), blocks.saturating_mul(frsize)))
556}
557
558#[cfg(not(unix))]
559fn fs_space(_path: &Path) -> Option<(u64, u64)> {
560 None
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565#[non_exhaustive]
566pub struct DiskBudget {
567 pub capacity: DiskCapacity,
569 pub cap_bytes: u64,
571 pub used: u64,
573 pub space: Option<(u64, u64)>,
575}
576
577const STALE_TMP: Duration = Duration::from_secs(10 * 60);
579
580pub const CACHEDIR_TAG: &str = "CACHEDIR.TAG";
588const CACHEDIR_TAG_CONTENT: &str = "Signature: 8a477f597d28d172789f06886806bc55\n\
589# This file is a cache directory tag created by asc.\n\
590# For information about cache directory tags, see: https://bford.info/cachedir/\n";
591
592fn is_cache_dir_name(name: &str) -> bool {
597 let Some((stem, hash)) = name.rsplit_once('-') else { return false };
598 hash.len() == HASH_HEX_LEN
599 && hash.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
600 && stem.len() <= STEM_MAX
601 && stem.chars().all(is_key_char)
602}
603
604impl DiskCache {
605 pub fn default_dir() -> Option<PathBuf> {
608 let var = |name: &str| std::env::var_os(name).filter(|v| !v.is_empty()).map(PathBuf::from);
609 if let Some(d) = var("XDG_CACHE_HOME") {
610 return Some(d.join("asc"));
611 }
612 if cfg!(windows)
613 && let Some(d) = var("LOCALAPPDATA")
614 {
615 return Some(d.join("asc").join("cache"));
616 }
617 var("HOME").map(|h| h.join(".cache").join("asc"))
618 }
619
620 pub fn open(dir: impl Into<PathBuf>) -> DiskCache {
624 DiskCache {
625 dir: dir.into(),
626 cap: DiskCapacity::default(),
627 min_dex_bytes: DEFAULT_MIN_DEX_BYTES,
628 min_derived_cost: DEFAULT_MIN_DERIVED_COST,
629 tiers: [true; 3],
630 written: Default::default(),
631 boot: boot_time(),
632 }
633 }
634
635 pub fn with_tiers(mut self, tiers: &[Tier]) -> DiskCache {
638 self.tiers = Tier::ALL.map(|t| tiers.contains(&t));
639 self
640 }
641
642 pub fn uses(&self, tier: Tier) -> bool {
644 self.tiers[tier as usize]
645 }
646
647 pub fn with_min_dex_bytes(mut self, bytes: u64) -> DiskCache {
649 self.min_dex_bytes = bytes;
650 self
651 }
652
653 pub fn min_dex_bytes(&self) -> u64 {
657 self.min_dex_bytes
658 }
659
660 pub fn admits_dex(&self, len: u64) -> bool {
663 len >= self.min_dex_bytes
664 }
665
666 pub fn with_min_derived_cost(mut self, cost: Duration) -> DiskCache {
669 self.min_derived_cost = cost;
670 self
671 }
672
673 pub fn admits_derived(&self, cost: Duration) -> bool {
676 cost >= self.min_derived_cost
677 }
678
679 pub fn open_default() -> Option<DiskCache> {
681 Self::default_dir().map(Self::open)
682 }
683
684 pub fn with_capacity(self, cap_bytes: u64) -> DiskCache {
686 self.with_capacity_mode(DiskCapacity::Fixed(cap_bytes))
687 }
688
689 pub fn with_capacity_mode(mut self, cap: DiskCapacity) -> DiskCache {
691 self.cap = cap;
692 self
693 }
694
695 pub fn capacity_mode(&self) -> DiskCapacity {
697 self.cap
698 }
699
700 pub fn dir(&self) -> &Path {
702 &self.dir
703 }
704
705 pub fn capacity_for(&self, used: u64) -> u64 {
708 match self.cap {
709 DiskCapacity::Fixed(b) => b,
710 DiskCapacity::Auto(auto) => auto.bytes_for(disk_space(&self.dir), used),
711 }
712 }
713
714 pub fn budget(&self) -> DiskBudget {
716 let used = self.stats().2;
717 let space = disk_space(&self.dir);
718 let cap_bytes = match self.cap {
719 DiskCapacity::Fixed(b) => b,
720 DiskCapacity::Auto(auto) => auto.bytes_for(space, used),
721 };
722 DiskBudget { capacity: self.cap, cap_bytes, used, space }
723 }
724
725 fn predates_boot(&self, meta: &fs::Metadata) -> bool {
727 predates_boot(meta.modified().ok(), self.boot, SystemTime::now())
728 }
729
730 fn discard<T>(&self, path: &Path) -> Option<T> {
732 let _ = fs::remove_file(path);
735 None
736 }
737
738 fn entry_path(&self, key: &ApkKey, entry_name: &str) -> PathBuf {
739 let (key, entry_name) = key.locate(entry_name);
740 self.dir.join(key.as_str()).join(safe_name(entry_name))
741 }
742
743 fn derived_path(&self, key: &ApkKey, name: &str, kind: &str, version: u32) -> PathBuf {
744 let (key, name) = key.locate(name);
745 let file = format!("{}~{}.v{DERIVED_FORMAT}.{version}", safe_name(name), safe_name(kind));
746 self.dir.join(DERIVED_DIR).join(key.as_str()).join(file)
747 }
748
749 fn rendered_path(&self, key: &ApkKey, query: &str) -> PathBuf {
750 self.dir.join(RENDERED_DIR).join(key.as_str()).join(format!("{:016x}", fnv64(query.as_bytes())))
751 }
752
753 pub fn get(&self, key: &ApkKey, entry_name: &str, expected_len: u64) -> Option<DexData> {
761 if !self.uses(Tier::Dex) {
762 return None;
763 }
764 let path = self.entry_path(key, entry_name);
765 let file = fs::File::open(&path).ok()?;
766 let meta = file.metadata().ok()?;
767 let len = meta.len().checked_sub(L1_TRAILER).filter(|&l| l > 0)?;
768 if expected_len != 0 && len != expected_len {
769 return None;
770 }
771 let mut trailer = [0u8; L1_TRAILER as usize];
772 (&file).seek(SeekFrom::Start(len)).ok()?;
773 (&file).read_exact(&mut trailer).ok()?;
774 let map = map_prefix(&file, len)?;
777 if &trailer[..4] != L1_MAGIC {
778 return self.discard(&path);
779 }
780 if self.predates_boot(&meta) {
781 if crc32fast::hash(&map) != u32::from_le_bytes(trailer[4..].try_into().expect("4 bytes")) {
782 drop(map);
784 return self.discard(&path);
785 }
786 let _ = touch_file(&file, &path);
787 }
788 let _ = touch_dir(path.parent()?);
790 Some(DexData::Mapped(map))
791 }
792
793 pub fn put(&self, key: &ApkKey, entry_name: &str, bytes: &[u8]) -> Result<(), CacheError> {
803 if !self.uses(Tier::Dex) {
804 return Ok(());
805 }
806 let mut trailer = [0u8; L1_TRAILER as usize];
807 trailer[..4].copy_from_slice(L1_MAGIC);
808 trailer[4..].copy_from_slice(&crc32fast::hash(bytes).to_le_bytes());
809 self.write_atomic(&self.entry_path(key, entry_name), &[bytes, &trailer])
810 }
811
812 pub fn get_derived(&self, key: &ApkKey, name: &str, kind: &str, version: u32) -> Option<Derived> {
821 if !self.uses(Tier::Derived) {
822 return None;
823 }
824 let path = self.derived_path(key, name, kind, version);
825 let file = fs::File::open(&path).ok()?;
826 let meta = file.metadata().ok()?;
827 let map = map_file(&file)?;
828 let header = map.get(..DERIVED_HEADER)?;
829 let u32_at = |o: usize| u32::from_le_bytes(header[o..o + 4].try_into().expect("4 bytes"));
830 let u64_at = |o: usize| u64::from_le_bytes(header[o..o + 8].try_into().expect("8 bytes"));
831 if &header[..8] != DERIVED_MAGIC
832 || u32_at(8) != DERIVED_FORMAT
833 || u32_at(12) != version
834 || u64_at(24) != (map.len() - DERIVED_HEADER) as u64
835 {
836 return None;
837 }
838 if self.predates_boot(&meta) && crc32fast::hash(&map[DERIVED_HEADER..]) != u32_at(32) {
839 drop(map);
840 return self.discard(&path);
841 }
842 let _ = touch_file(&file, &path);
843 Some(Derived { build_cost: Duration::from_micros(u64_at(16)), map })
844 }
845
846 pub fn put_derived(
854 &self,
855 key: &ApkKey,
856 name: &str,
857 kind: &str,
858 version: u32,
859 build_cost: Duration,
860 payload: &[u8],
861 ) -> Result<(), CacheError> {
862 if !self.uses(Tier::Derived) {
863 return Ok(());
864 }
865 let mut header = [0u8; DERIVED_HEADER];
866 header[..8].copy_from_slice(DERIVED_MAGIC);
867 header[8..12].copy_from_slice(&DERIVED_FORMAT.to_le_bytes());
868 header[12..16].copy_from_slice(&version.to_le_bytes());
869 header[16..24].copy_from_slice(&(build_cost.as_micros() as u64).to_le_bytes());
870 header[24..32].copy_from_slice(&(payload.len() as u64).to_le_bytes());
871 header[32..36].copy_from_slice(&crc32fast::hash(payload).to_le_bytes());
872 self.write_atomic(&self.derived_path(key, name, kind, version), &[&header, payload])
873 }
874
875 pub fn get_rendered(&self, key: &ApkKey, query: &str) -> Option<Vec<u8>> {
878 self.get_rendered_view(key, query).map(|view| view.as_ref().to_vec())
879 }
880
881 pub fn get_rendered_view(&self, key: &ApkKey, query: &str) -> Option<Rendered> {
885 if !self.uses(Tier::Rendered) {
886 return None;
887 }
888 let path = self.rendered_path(key, query);
889 let file = fs::File::open(&path).ok()?;
890 let data = map_file(&file)?;
891 let key_len = u32::from_le_bytes(data.get(8..12)?.try_into().ok()?) as usize;
892 let crc = u32::from_le_bytes(data.get(12..16)?.try_into().ok()?);
893 let body = 16usize.checked_add(key_len)?;
894 if data.get(..8)? != RENDERED_MAGIC || crc32fast::hash(&data[16..]) != crc {
895 drop(data);
896 return self.discard(&path);
897 }
898 if data.get(16..body)? != query.as_bytes() {
900 return None;
901 }
902 let _ = touch_file(&file, &path);
903 Some(Rendered { map: data, body })
904 }
905
906 pub fn put_rendered(&self, key: &ApkKey, query: &str, payload: &[u8]) -> Result<(), CacheError> {
912 if !self.uses(Tier::Rendered) {
913 return Ok(());
914 }
915 let mut crc = crc32fast::Hasher::new();
916 crc.update(query.as_bytes());
917 crc.update(payload);
918 let mut header = Vec::with_capacity(16 + query.len());
919 header.extend_from_slice(RENDERED_MAGIC);
920 header.extend_from_slice(&(query.len() as u32).to_le_bytes());
921 header.extend_from_slice(&crc.finalize().to_le_bytes());
922 header.extend_from_slice(query.as_bytes());
923 self.write_atomic(&self.rendered_path(key, query), &[&header, payload])
924 }
925
926 pub fn note_source(&self, key: &ApkKey, apk_path: &Path) -> Result<(), CacheError> {
935 if !key.archives.is_empty() {
936 return key.archives.iter().try_for_each(|a| self.note_source(&a.key, &a.path));
937 }
938 let path = self.dir.join(SOURCES_DIR).join(key.as_str());
939 let Some(stamp) = source_stamp(apk_path) else { return Ok(()) };
940 if fs::read_to_string(&path).is_ok_and(|s| s == stamp) {
941 return Ok(());
942 }
943 self.write_atomic(&path, &[stamp.as_bytes()])
944 }
945
946 fn write_atomic(&self, path: &Path, parts: &[&[u8]]) -> Result<(), CacheError> {
951 if !self.is_cache_dir() {
954 return Err(self.not_a_cache_dir());
955 }
956 let len: u64 = parts.iter().map(|p| p.len() as u64).sum();
957 if matches!(self.cap, DiskCapacity::Auto(_))
961 && let Some((free, total)) = disk_space(&self.dir)
962 && free.saturating_sub(len) < auto_disk_reserve(total)
963 {
964 return Err(CacheError::LowDiskSpace { path: self.dir.clone(), bytes: len });
965 }
966 let parent = path.parent().expect("cache path has a parent");
967 fs::create_dir_all(parent).map_err(io_err(parent))?;
968 let _ = self.ensure_tag();
971 let tmp = parent.join(format!(".{}.{}.tmp", path.file_name().unwrap_or_default().to_string_lossy(), std::process::id()));
972 {
973 let mut f = fs::File::create(&tmp).map_err(io_err(&tmp))?;
974 for p in parts {
975 f.write_all(p).map_err(io_err(&tmp))?;
976 }
977 }
978 fs::rename(&tmp, path).map_err(io_err(path))?;
979 self.written.fetch_add(len, std::sync::atomic::Ordering::Relaxed);
980 Ok(())
981 }
982
983 pub fn stats(&self) -> (usize, usize, u64) {
986 let items = self.items();
987 let keys: HashSet<&str> = items.iter().map(|i| i.key.as_str()).collect();
988 (keys.len(), items.iter().map(|i| i.files).sum(), items.iter().map(|i| i.bytes).sum())
989 }
990
991 pub fn tier_stats(&self) -> [TierStats; 3] {
993 let items = self.items();
994 Tier::ALL.map(|tier| {
995 let of_tier = || items.iter().filter(move |i| i.tier == tier);
996 let keys: HashSet<&str> = of_tier().map(|i| i.key.as_str()).collect();
997 TierStats { apks: keys.len(), files: of_tier().map(|i| i.files).sum(), bytes: of_tier().map(|i| i.bytes).sum() }
998 })
999 }
1000
1001 pub fn clear(&self) -> Result<u64, CacheError> {
1010 if !self.dir.exists() {
1011 return Ok(0);
1012 }
1013 if !self.is_cache_dir() {
1014 return Err(self.not_a_cache_dir());
1015 }
1016 let mut bytes = 0;
1017 for item in self.items() {
1018 remove_item(&item).map_err(io_err(&item.path))?;
1019 bytes += item.bytes;
1020 }
1021 for sub in [DERIVED_DIR, RENDERED_DIR, SOURCES_DIR, SPILL_DIR] {
1022 let path = self.dir.join(sub);
1023 if path.is_dir() {
1024 fs::remove_dir_all(&path).map_err(io_err(&path))?;
1025 }
1026 }
1027 Ok(bytes)
1028 }
1029
1030 pub fn check_usable(&self) -> Result<(), CacheError> {
1040 if self.is_cache_dir() { Ok(()) } else { Err(self.not_a_cache_dir()) }
1041 }
1042
1043 fn not_a_cache_dir(&self) -> CacheError {
1044 CacheError::NotACacheDir { path: self.dir.clone(), tag: CACHEDIR_TAG }
1045 }
1046
1047 fn ensure_tag(&self) -> Result<(), CacheError> {
1049 if self.has_tag() {
1050 return Ok(());
1051 }
1052 let tag = self.dir.join(CACHEDIR_TAG);
1053 match fs::OpenOptions::new().write(true).create_new(true).open(&tag) {
1055 Ok(mut f) => f.write_all(CACHEDIR_TAG_CONTENT.as_bytes()).map_err(|e| {
1058 drop(f);
1059 let _ = fs::remove_file(&tag);
1060 io_err(&tag)(e)
1061 }),
1062 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
1063 Err(e) => Err(io_err(&tag)(e)),
1064 }
1065 }
1066
1067 fn has_tag(&self) -> bool {
1068 self.dir.join(CACHEDIR_TAG).is_file()
1069 }
1070
1071 fn is_cache_dir(&self) -> bool {
1077 if self.has_tag() {
1078 return true;
1079 }
1080 match fs::read_dir(&self.dir) {
1081 Ok(rd) => rd.flatten().all(|d| {
1082 let name = d.file_name();
1083 let name = name.to_string_lossy();
1084 match d.file_type() {
1085 Ok(t) if t.is_dir() => {
1086 is_cache_dir_name(&name) || TIER_DIRS.contains(&&*name) || name == SPILL_DIR || name == DECOMPILERS_DIR
1087 }
1088 Ok(t) if t.is_file() => name == ".DS_Store" || name == EVICT_MARKER,
1089 _ => false,
1090 }
1091 }),
1092 Err(e) => e.kind() == std::io::ErrorKind::NotFound,
1093 }
1094 }
1095
1096 fn apk_dirs(&self) -> Vec<(SystemTime, PathBuf, usize, u64)> {
1101 apk_dirs_in(&self.dir)
1102 }
1103
1104 fn items(&self) -> Vec<Item> {
1106 let mut out: Vec<Item> = self
1107 .apk_dirs()
1108 .into_iter()
1109 .map(|(mtime, path, files, bytes)| Item {
1110 tier: Tier::Dex,
1111 key: path.file_name().unwrap_or_default().to_string_lossy().into_owned(),
1112 path,
1113 mtime,
1114 files,
1115 bytes,
1116 cost_us: 0,
1117 })
1118 .collect();
1119 for (tier, sub) in [(Tier::Derived, DERIVED_DIR), (Tier::Rendered, RENDERED_DIR)] {
1120 for (_, dir, _, _) in apk_dirs_in(&self.dir.join(sub)) {
1121 let key = dir.file_name().unwrap_or_default().to_string_lossy().into_owned();
1122 let Ok(rd) = fs::read_dir(&dir) else { continue };
1123 for f in rd.flatten() {
1124 let Ok(m) = f.metadata() else { continue };
1125 if !m.is_file() || f.file_name().to_string_lossy().ends_with(".tmp") {
1126 continue;
1127 }
1128 let cost_us = if tier == Tier::Derived { derived_cost_us(&f.path()) } else { 0 };
1129 let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
1130 out.push(Item { tier, key: key.clone(), path: f.path(), mtime, files: 1, bytes: m.len(), cost_us });
1131 }
1132 }
1133 }
1134 out
1135 }
1136
1137 fn dead_keys(&self) -> HashSet<String> {
1140 let mut out = HashSet::new();
1141 let Ok(rd) = fs::read_dir(self.dir.join(SOURCES_DIR)) else { return out };
1142 for f in rd.flatten() {
1143 let Ok(stamp) = fs::read_to_string(f.path()) else { continue };
1144 let Some(path) = stamp.lines().nth(2) else { continue };
1145 if source_stamp(Path::new(path)).as_deref() != Some(stamp.as_str()) {
1146 out.insert(f.file_name().to_string_lossy().into_owned());
1147 }
1148 }
1149 out
1150 }
1151}
1152
1153fn apk_dirs_in(root: &Path) -> Vec<(SystemTime, PathBuf, usize, u64)> {
1158 let mut out = Vec::new();
1159 let Ok(rd) = fs::read_dir(root) else { return out };
1160 let now = SystemTime::now();
1161 for d in rd.flatten() {
1162 let path = d.path();
1163 if !is_cache_dir_name(&d.file_name().to_string_lossy()) || !path.is_dir() {
1164 continue;
1165 }
1166 let mtime = d.metadata().and_then(|m| m.modified()).unwrap_or(SystemTime::UNIX_EPOCH);
1167 let (mut files, mut size) = (0usize, 0u64);
1168 if let Ok(fd) = fs::read_dir(&path) {
1169 for f in fd.flatten() {
1170 let Ok(m) = f.metadata() else { continue };
1171 let name = f.file_name();
1172 let is_tmp = name.to_string_lossy().ends_with(".tmp");
1173 if is_tmp && m.modified().ok().and_then(|t| now.duration_since(t).ok()).is_some_and(|age| age > STALE_TMP) {
1174 let _ = fs::remove_file(f.path());
1175 continue;
1176 }
1177 files += 1;
1178 size += m.len();
1179 }
1180 }
1181 out.push((mtime, path, files, size));
1182 }
1183 out
1184}
1185
1186impl DiskCache {
1187 pub fn evict(&self) {
1199 if !self.is_cache_dir() {
1200 return;
1201 }
1202 self.written.store(0, std::sync::atomic::Ordering::Relaxed);
1203 let _ = fs::File::create(self.dir.join(EVICT_MARKER));
1204 let items = self.items();
1205 let mut total: u64 = items.iter().map(|i| i.bytes).sum();
1206 let cap = self.capacity_for(total);
1207 let caps = [cap, cap / DERIVED_BUDGET_DIVISOR, (cap / RENDERED_BUDGET_DIVISOR).min(RENDERED_MAX)];
1208 let mut tier_used = [0u64; 3];
1209 for i in &items {
1210 tier_used[i.tier as usize] += i.bytes;
1211 }
1212 if total > cap || tier_used[1] > caps[1] || tier_used[2] > caps[2] {
1213 let order = eviction_order(&items, &self.dead_keys());
1214 let mut gone = vec![false; items.len()];
1215 let mut remove = |idx: usize, total: &mut u64, tier_used: &mut [u64; 3]| {
1216 if !gone[idx] && remove_item(&items[idx]).is_ok() {
1217 gone[idx] = true;
1218 *total = total.saturating_sub(items[idx].bytes);
1219 let t = items[idx].tier as usize;
1220 tier_used[t] = tier_used[t].saturating_sub(items[idx].bytes);
1221 }
1222 };
1223 for tier in [Tier::Rendered, Tier::Derived] {
1225 for &idx in order.iter().filter(|&&i| items[i].tier == tier) {
1226 if tier_used[tier as usize] <= caps[tier as usize] {
1227 break;
1228 }
1229 remove(idx, &mut total, &mut tier_used);
1230 }
1231 }
1232 for &idx in &order {
1233 if total <= cap {
1234 break;
1235 }
1236 remove(idx, &mut total, &mut tier_used);
1237 }
1238 }
1239 if let Ok(rd) = fs::read_dir(self.dir.join(SOURCES_DIR)) {
1241 let live: HashSet<String> = self.items().into_iter().map(|i| i.key).collect();
1242 for f in rd.flatten() {
1243 if !live.contains(&*f.file_name().to_string_lossy()) {
1244 let _ = fs::remove_file(f.path());
1245 }
1246 }
1247 }
1248 }
1249}
1250
1251impl DiskCache {
1252 pub fn evict_lazily(&self) {
1258 if !self.recently_evicted() {
1259 self.evict();
1260 }
1261 }
1262
1263 fn recently_evicted(&self) -> bool {
1271 let written = self.written.load(std::sync::atomic::Ordering::Relaxed);
1272 let fresh = fs::metadata(self.dir.join(EVICT_MARKER))
1273 .and_then(|m| m.modified())
1274 .ok()
1275 .and_then(|t| SystemTime::now().duration_since(t).ok())
1276 .is_some_and(|age| age < EVICT_INTERVAL);
1277 fresh && written < self.capacity_for(0) / EVICT_WRITE_DIVISOR
1278 }
1279}
1280
1281const EVICT_MARKER: &str = ".last-evict";
1283const EVICT_INTERVAL: Duration = Duration::from_secs(5);
1284const EVICT_WRITE_DIVISOR: u64 = 16;
1285
1286fn eviction_order(items: &[Item], dead: &HashSet<String>) -> Vec<usize> {
1292 let newest = items.iter().map(|i| i.mtime).max().unwrap_or(SystemTime::UNIX_EPOCH);
1293 let age_secs = |i: &Item| newest.duration_since(i.mtime).unwrap_or_default().as_secs_f64() + 1.0;
1294 let score = |i: &Item| -> (u8, f64) {
1295 if dead.contains(&i.key) {
1296 return (0, -(i.bytes as f64));
1297 }
1298 match i.tier {
1299 Tier::Dex => (1, -(age_secs(i) * i.bytes as f64)),
1300 Tier::Rendered => (2, -age_secs(i)),
1301 Tier::Derived => (3, i.cost_us as f64 / i.bytes.max(1) as f64 / (1.0 + age_secs(i) / 3600.0)),
1302 }
1303 };
1304 let mut order: Vec<usize> = (0..items.len()).collect();
1305 order.sort_by(|&a, &b| {
1306 let (sa, sb) = (score(&items[a]), score(&items[b]));
1307 sa.0.cmp(&sb.0).then(sa.1.total_cmp(&sb.1)).then(items[a].mtime.cmp(&items[b].mtime))
1308 });
1309 order
1310}
1311
1312fn remove_item(item: &Item) -> std::io::Result<()> {
1313 match item.tier {
1314 Tier::Dex => fs::remove_dir_all(&item.path),
1315 _ => fs::remove_file(&item.path),
1316 }
1317}
1318
1319const TIER_DIRS: [&str; 3] = [DERIVED_DIR, RENDERED_DIR, SOURCES_DIR];
1321
1322fn safe_name(name: &str) -> String {
1324 name.chars().map(|c| if is_key_char(c) { c } else { '_' }).collect()
1325}
1326
1327pub fn rendered_key(producer: &str, version: &str, query: &str) -> String {
1331 format!("{producer}\0{version}\0{RENDERED_FORMAT}\0{query}")
1332}
1333
1334fn map_file(file: &fs::File) -> Option<memmap2::Mmap> {
1336 unsafe { memmap2::MmapOptions::new().map(file) }.ok()
1339}
1340
1341fn map_prefix(file: &fs::File, len: u64) -> Option<memmap2::Mmap> {
1343 let len = usize::try_from(len).ok()?;
1344 unsafe { memmap2::MmapOptions::new().len(len).map(file) }.ok()
1346}
1347
1348fn predates_boot(mtime: Option<SystemTime>, boot: Option<SystemTime>, now: SystemTime) -> bool {
1354 match (mtime, boot) {
1355 (Some(m), Some(b)) => m < b || m > now,
1356 _ => true,
1357 }
1358}
1359
1360fn boot_time() -> Option<SystemTime> {
1362 static BOOT: OnceLock<Option<SystemTime>> = OnceLock::new();
1363 *BOOT.get_or_init(read_boot_time)
1364}
1365
1366#[cfg(any(target_os = "linux", target_os = "android"))]
1367fn read_boot_time() -> Option<SystemTime> {
1368 let stat = fs::read_to_string("/proc/stat").ok()?;
1369 let secs = stat.lines().find_map(|l| l.strip_prefix("btime "))?.trim().parse().ok()?;
1370 Some(SystemTime::UNIX_EPOCH + Duration::from_secs(secs))
1371}
1372
1373#[cfg(target_vendor = "apple")]
1374fn read_boot_time() -> Option<SystemTime> {
1375 let mut tv = libc::timeval { tv_sec: 0, tv_usec: 0 };
1376 let mut len = std::mem::size_of::<libc::timeval>();
1377 let rc = unsafe { libc::sysctlbyname(c"kern.boottime".as_ptr(), (&raw mut tv).cast(), &mut len, std::ptr::null_mut(), 0) };
1380 if rc != 0 || len != std::mem::size_of::<libc::timeval>() {
1381 return None;
1382 }
1383 Some(
1384 SystemTime::UNIX_EPOCH
1385 + Duration::from_secs(u64::try_from(tv.tv_sec).ok()?)
1386 + Duration::from_micros(u64::try_from(tv.tv_usec).ok()?),
1387 )
1388}
1389
1390#[cfg(windows)]
1391fn read_boot_time() -> Option<SystemTime> {
1392 let uptime = Duration::from_millis(unsafe { windows_sys::Win32::System::SystemInformation::GetTickCount64() });
1394 SystemTime::now().checked_sub(uptime)
1395}
1396
1397#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple", windows)))]
1398fn read_boot_time() -> Option<SystemTime> {
1399 None
1400}
1401
1402fn derived_cost_us(path: &Path) -> u64 {
1404 use std::io::Read;
1405 let mut header = [0u8; 24];
1406 match fs::File::open(path).and_then(|mut f| f.read_exact(&mut header)) {
1407 Ok(()) if &header[..8] == DERIVED_MAGIC => u64::from_le_bytes(header[16..24].try_into().expect("8 bytes")),
1408 _ => 0,
1409 }
1410}
1411
1412fn source_stamp(apk: &Path) -> Option<String> {
1414 let abs = fs::canonicalize(apk).ok()?;
1415 let meta = fs::metadata(&abs).ok()?;
1416 let mtime = meta.modified().ok()?.duration_since(SystemTime::UNIX_EPOCH).ok()?.as_nanos();
1417 let path = abs.to_str()?;
1418 (!path.contains('\n')).then(|| format!("{}\n{mtime}\n{path}", meta.len()))
1419}
1420
1421fn open_for_times(path: &Path) -> std::io::Result<fs::File> {
1426 #[cfg(windows)]
1427 {
1428 use std::os::windows::fs::OpenOptionsExt;
1429 const FILE_WRITE_ATTRIBUTES: u32 = 0x0100;
1430 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
1431 fs::OpenOptions::new().access_mode(FILE_WRITE_ATTRIBUTES).custom_flags(FILE_FLAG_BACKUP_SEMANTICS).open(path)
1432 }
1433 #[cfg(not(windows))]
1434 {
1435 fs::File::open(path)
1436 }
1437}
1438
1439fn touch_file(file: &fs::File, path: &Path) -> std::io::Result<()> {
1441 if cfg!(windows) { open_for_times(path)?.set_modified(SystemTime::now()) } else { file.set_modified(SystemTime::now()) }
1442}
1443
1444fn touch_dir(dir: &Path) -> std::io::Result<()> {
1445 let now = SystemTime::now();
1446 if open_for_times(dir).and_then(|f| f.set_modified(now)).is_ok() {
1447 return Ok(());
1448 }
1449 let marker = dir.join(".touch");
1451 fs::File::create(&marker)?;
1452 fs::remove_file(&marker)
1453}
1454
1455struct Slot<T> {
1456 value: Arc<T>,
1457 size: usize,
1458 tick: u64,
1460}
1461
1462pub struct MemoryCache<T> {
1465 cap_bytes: usize,
1466 used: usize,
1467 reserved: usize,
1469 clock: u64,
1470 map: HashMap<ApkKey, HashMap<String, Slot<T>>>,
1471}
1472
1473impl<T> MemoryCache<T> {
1474 pub fn new(cap_bytes: usize) -> Self {
1476 MemoryCache { cap_bytes, used: 0, reserved: 0, clock: 0, map: HashMap::new() }
1477 }
1478
1479 fn tick(&mut self) -> u64 {
1480 self.clock += 1;
1481 self.clock
1482 }
1483
1484 pub fn get(&mut self, key: &ApkKey, entry_name: &str) -> Option<Arc<T>> {
1486 let tick = self.tick();
1487 let slot = self.map.get_mut(key)?.get_mut(entry_name)?;
1488 slot.tick = tick;
1489 Some(Arc::clone(&slot.value))
1490 }
1491
1492 pub fn insert(&mut self, key: &ApkKey, entry_name: &str, value: Arc<T>, size: usize) {
1496 let tick = self.tick();
1497 let per_apk = self.map.entry(key.clone()).or_default();
1498 if let Some(old) = per_apk.insert(entry_name.to_string(), Slot { value, size, tick }) {
1499 self.used -= old.size;
1500 }
1501 self.used += size;
1502 self.evict_over_cap();
1503 }
1504
1505 pub fn recharge(&mut self, key: &ApkKey, entry_name: &str, size_of: impl FnOnce(&T) -> usize) -> bool {
1512 let Some(slot) = self.map.get_mut(key).and_then(|m| m.get_mut(entry_name)) else { return false };
1513 let size = size_of(&slot.value);
1514 let old = std::mem::replace(&mut slot.size, size);
1515 if old == size {
1516 return false;
1517 }
1518 self.used = self.used - old + size;
1519 self.evict_over_cap();
1520 true
1521 }
1522
1523 pub fn replace(&mut self, key: &ApkKey, entry_name: &str, current: &Arc<T>, new: Arc<T>) -> bool {
1526 match self.map.get_mut(key).and_then(|m| m.get_mut(entry_name)) {
1527 Some(slot) if Arc::ptr_eq(&slot.value, current) => {
1528 slot.value = new;
1529 true
1530 }
1531 _ => false,
1532 }
1533 }
1534
1535 pub fn charged_bytes(&self, key: &ApkKey, entry_name: &str) -> Option<usize> {
1537 self.map.get(key)?.get(entry_name).map(|s| s.size)
1538 }
1539
1540 fn evict_over_cap(&mut self) {
1543 while self.used + self.reserved > self.cap_bytes && self.len() > 1 {
1544 let Some((k, name)) = self.lru() else { break };
1545 if let Some(per_apk) = self.map.get_mut(&k) {
1546 if let Some(old) = per_apk.remove(&name) {
1547 self.used -= old.size;
1548 }
1549 if per_apk.is_empty() {
1550 self.map.remove(&k);
1551 }
1552 }
1553 }
1554 }
1555
1556 fn lru(&self) -> Option<(ApkKey, String)> {
1557 self.map
1558 .iter()
1559 .flat_map(|(k, per)| per.iter().map(move |(n, s)| (s.tick, k, n)))
1560 .min_by_key(|(t, _, _)| *t)
1561 .map(|(_, k, n)| (k.clone(), n.clone()))
1562 }
1563
1564 pub fn used_bytes(&self) -> usize {
1566 self.used
1567 }
1568
1569 pub fn capacity_bytes(&self) -> usize {
1571 self.cap_bytes
1572 }
1573
1574 pub fn reserved_bytes(&self) -> usize {
1577 self.reserved
1578 }
1579
1580 pub fn set_reserved(&mut self, bytes: usize) {
1583 self.reserved = bytes;
1584 self.evict_over_cap();
1585 }
1586
1587 pub fn set_capacity(&mut self, cap_bytes: usize) {
1590 self.cap_bytes = cap_bytes;
1591 self.evict_over_cap();
1592 }
1593
1594 pub fn peek(&self, key: &ApkKey, entry_name: &str) -> Option<Arc<T>> {
1596 self.map.get(key)?.get(entry_name).map(|s| Arc::clone(&s.value))
1597 }
1598
1599 pub fn values(&self) -> impl Iterator<Item = &Arc<T>> {
1601 self.map.values().flat_map(|per| per.values().map(|s| &s.value))
1602 }
1603
1604 pub fn len(&self) -> usize {
1606 self.map.values().map(HashMap::len).sum()
1607 }
1608
1609 pub fn is_empty(&self) -> bool {
1611 self.len() == 0
1612 }
1613
1614 pub fn remove_apk(&mut self, key: &ApkKey) -> usize {
1616 let freed: usize = self.map.remove(key).map(|per| per.values().map(|s| s.size).sum()).unwrap_or(0);
1617 self.used -= freed;
1618 freed
1619 }
1620
1621 pub fn clear(&mut self) {
1623 self.map.clear();
1624 self.used = 0;
1625 }
1626}
1627
1628#[cfg(test)]
1629mod tests {
1630 use super::*;
1631
1632 #[test]
1633 fn content_keys_are_stable_and_distinct() {
1634 let mut sha = [0u8; 32];
1635 sha[0] = 0x3a;
1636 sha[7] = 0x7f;
1637 let key = ApkKey::for_content("entry.hap/ets/modules.abc", &sha);
1638 assert_eq!(key.as_str(), "modules-3a0000000000007f");
1639 assert!(is_cache_dir_name(key.as_str()), "evicted and cleared like an APK's directory");
1640 assert_eq!(ApkKey::for_content("other/dir/modules.abc", &sha), key, "the place does not count");
1641 let mut other = sha;
1642 other[1] = 1;
1643 assert_ne!(ApkKey::for_content("entry.hap/ets/modules.abc", &other), key);
1644 assert_eq!(ApkKey::for_content("a b/w*idgets.abc", &sha).as_str(), "widgets-3a0000000000007f");
1645 assert_eq!(key.locate("ets/modules.abc"), (&key, "ets/modules.abc"));
1646 }
1647
1648 #[test]
1649 fn regions_of_a_file_map_at_any_offset() {
1650 let dir = tempfile::tempdir().unwrap();
1651 let path = dir.path().join("app.apk");
1652 let bytes: Vec<u8> = (0..20_000u32).map(|i| (i % 251) as u8).collect();
1653 fs::write(&path, &bytes).unwrap();
1654 for (off, len) in [(0, 10), (4097, 3000), (12_345, 7655), (19_999, 1)] {
1655 let d = DexData::map_region(&path, off as u64, len).unwrap();
1656 assert!(matches!(d, DexData::Mapped(_)));
1657 assert_eq!(&*d, &bytes[off..off + len]);
1658 }
1659 assert!(DexData::map_region(&path, 19_999, 2).is_err());
1660 assert!(DexData::map_region(&dir.path().join("missing"), 0, 1).is_err());
1661 }
1662
1663 #[test]
1664 fn memory_remove_apk_releases_only_that_apk() {
1665 let (a, b) = (ApkKey::from_id("a-0123456789abcdef".into()), ApkKey::from_id("b-0123456789abcdef".into()));
1666 let mut mem = MemoryCache::new(1 << 20);
1667 mem.insert(&a, "classes.dex", Arc::new(1), 100);
1668 mem.insert(&a, "classes2.dex", Arc::new(2), 50);
1669 mem.insert(&b, "classes.dex", Arc::new(3), 7);
1670 assert_eq!(mem.remove_apk(&a), 150);
1671 assert_eq!(mem.remove_apk(&a), 0);
1672 assert_eq!((mem.used_bytes(), mem.len()), (7, 1));
1673 assert!(mem.get(&b, "classes.dex").is_some());
1674 }
1675
1676 #[test]
1677 fn disk_roundtrip_and_clear() {
1678 let dir = tempfile::tempdir().unwrap();
1679 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
1680 let key = ApkKey::from_id("app-0123456789abcdef".into());
1681 assert!(cache.get(&key, "classes.dex", 3).is_none());
1682 cache.put(&key, "classes.dex", b"abc").unwrap();
1683 assert_eq!(&*cache.get(&key, "classes.dex", 3).unwrap(), b"abc");
1684 assert!(cache.get(&key, "classes.dex", 4).is_none());
1685 let (dirs, files, bytes) = cache.stats();
1686 assert_eq!((dirs, files, bytes), (1, 1, 3 + L1_TRAILER));
1687 assert_eq!(cache.clear().unwrap(), 3 + L1_TRAILER);
1688 assert!(cache.get(&key, "classes.dex", 3).is_none());
1689 }
1690
1691 const GIB: u64 = 1 << 30;
1692 const MIB: u64 = 1 << 20;
1693
1694 #[test]
1695 fn auto_disk_budget_follows_free_space() {
1696 assert_eq!(AutoDisk::default().bytes_for(None, 0), AUTO_DISK_UNKNOWN);
1698 assert_eq!(AutoDisk::default().bytes_for(Some((500 * GIB, 1000 * GIB)), 0), AutoDisk::CEILING);
1700 let tight = AutoDisk { percent: 10, ceiling: 5 * GIB };
1702 assert_eq!(tight.bytes_for(Some((100 * GIB, 500 * GIB)), 0), 5 * GIB);
1703 assert_eq!(tight.bytes_for(Some((20 * GIB, 50 * GIB)), 0), 20 * GIB / 100 * 10);
1704 assert_eq!(tight.bytes_for(None, 0), 4 * GIB);
1705 assert_eq!(AutoDisk::default().bytes_for(Some((100 * GIB, 500 * GIB)), 0), 100 * GIB / 100 * 20);
1707 assert_eq!(AutoDisk::default().bytes_for(Some((10 * GIB, 20 * GIB)), 0), 10 * GIB / 100 * 20);
1709 assert_eq!(AutoDisk::default().bytes_for(Some((9 * GIB, 20 * GIB)), GIB), 10 * GIB / 100 * 20);
1710 assert_eq!(AutoDisk::default().bytes_for(Some((GIB + 100 * MIB, 16 * GIB)), 0), 100 * MIB);
1712 assert_eq!(AutoDisk::default().bytes_for(Some((GIB, 16 * GIB)), 0), 0);
1713 }
1714
1715 #[test]
1716 fn auto_disk_budget_shrinks_below_usage_on_a_nearly_full_disk() {
1717 assert_eq!(AutoDisk::default().bytes_for(Some((GIB / 2, 100 * GIB)), 6 * GIB), (6 * GIB + GIB / 2) / 100 * 20);
1720 assert_eq!(AutoDisk::default().bytes_for(Some((GIB / 2, 100 * GIB)), 5 * GIB + GIB / 2), GIB);
1723 assert_eq!(AutoDisk::default().bytes_for(Some((GIB / 2, 100 * GIB)), 3 * GIB), 0);
1725 assert_eq!(auto_disk_reserve(100 * GIB), 5 * GIB);
1726 assert_eq!(auto_disk_reserve(4 * GIB), GIB);
1727 }
1728
1729 #[test]
1730 fn explicit_disk_capacity_wins() {
1731 let dir = tempfile::tempdir().unwrap();
1732 let cache = DiskCache::open(dir.path()).with_capacity_mode(DiskCapacity::default()).with_capacity(7);
1733 assert_eq!(cache.capacity_mode(), DiskCapacity::Fixed(7));
1734 assert_eq!(cache.capacity_for(1 << 40), 7);
1735 let b = cache.budget();
1736 assert_eq!((b.capacity, b.cap_bytes, b.used), (DiskCapacity::Fixed(7), 7, 0));
1737 assert_eq!(b.space.is_some(), cfg!(unix));
1739 let auto = DiskCache::open(dir.path()).with_capacity_mode(DiskCapacity::default());
1740 let ab = auto.budget();
1743 assert_eq!(ab.cap_bytes, AutoDisk::default().bytes_for(ab.space, ab.used));
1744 assert_eq!(disk_space(&dir.path().join("a/b")).is_some(), cfg!(unix));
1746 }
1747
1748 #[test]
1749 fn eviction_follows_a_lowered_capacity() {
1750 let dir = tempfile::tempdir().unwrap();
1751 let keys: Vec<ApkKey> = (0..3).map(|i| ApkKey::from_id(format!("app-000000000000000{i}"))).collect();
1752 let cache = DiskCache::open(dir.path()).with_capacity(100);
1753 for (i, k) in keys.iter().enumerate() {
1754 cache.put(k, "classes.dex", &[0; 10]).unwrap();
1755 let t = SystemTime::now() + Duration::from_secs(10 * i as u64);
1756 open_for_times(&dir.path().join(k.as_str())).unwrap().set_modified(t).unwrap();
1757 }
1758 cache.evict();
1759 assert_eq!(cache.stats(), (3, 3, 3 * (10 + L1_TRAILER)));
1760 let cache = cache.with_capacity(25);
1763 cache.evict();
1764 assert_eq!(cache.stats(), (1, 1, 10 + L1_TRAILER));
1765 assert!(cache.get(&keys[2], "classes.dex", 10).is_some());
1766 }
1767
1768 #[test]
1769 fn cache_dir_names_are_recognised() {
1770 assert!(is_cache_dir_name("app-0123456789abcdef"));
1771 assert!(is_cache_dir_name("-0123456789abcdef")); assert!(is_cache_dir_name("my.app_v2-0123456789abcdef"));
1773 assert!(!is_cache_dir_name("Documents"));
1774 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))));
1778 }
1779
1780 #[test]
1781 fn unrelated_directories_survive_evict_and_clear() {
1782 let dir = tempfile::tempdir().unwrap();
1783 let cache = DiskCache::open(dir.path()).with_capacity(4);
1784 let key = ApkKey::from_id("app-0123456789abcdef".into());
1785 cache.put(&key, "classes.dex", &[0; 8]).unwrap();
1787 assert!(dir.path().join(CACHEDIR_TAG).is_file());
1788 let thesis = dir.path().join("my-thesis");
1790 fs::create_dir(&thesis).unwrap();
1791 fs::write(thesis.join("chapter1.txt"), vec![0u8; 100]).unwrap();
1792 fs::write(dir.path().join("notes.txt"), b"keep").unwrap();
1793 assert_eq!(cache.stats(), (1, 1, 8 + L1_TRAILER));
1795 cache.evict();
1796 assert!(thesis.join("chapter1.txt").is_file());
1797 assert!(cache.get(&key, "classes.dex", 8).is_none());
1798 cache.put(&key, "classes.dex", &[0; 8]).unwrap();
1799 assert_eq!(cache.clear().unwrap(), 8 + L1_TRAILER);
1800 assert!(thesis.join("chapter1.txt").is_file());
1801 assert!(dir.path().join("notes.txt").is_file());
1802 assert!(dir.path().join(CACHEDIR_TAG).is_file());
1803 assert!(dir.path().is_dir());
1804 assert_eq!(cache.stats(), (0, 0, 0));
1805 }
1806
1807 #[test]
1808 fn foreign_directories_are_refused_and_never_tagged() {
1809 let dir = tempfile::tempdir().unwrap();
1810 let cache = DiskCache::open(dir.path()).with_capacity(1);
1811 let lookalike = dir.path().join("app-0123456789abcdef");
1813 fs::create_dir(&lookalike).unwrap();
1814 fs::write(lookalike.join("classes.dex"), [0; 16]).unwrap();
1815 fs::write(dir.path().join("notes.txt"), b"keep").unwrap();
1816 cache.evict();
1817 assert!(lookalike.join("classes.dex").is_file());
1818 assert!(matches!(cache.clear(), Err(CacheError::NotACacheDir { .. })));
1819 assert!(lookalike.join("classes.dex").is_file());
1820 let key = ApkKey::from_id("other-fedcba9876543210".into());
1823 assert!(matches!(cache.put(&key, "classes.dex", &[0; 4]), Err(CacheError::NotACacheDir { .. })));
1824 assert!(!dir.path().join(CACHEDIR_TAG).exists());
1825 assert!(!dir.path().join("other-fedcba9876543210").exists());
1826 let empty = tempfile::tempdir().unwrap();
1828 assert_eq!(DiskCache::open(empty.path()).clear().unwrap(), 0);
1829 assert_eq!(DiskCache::open(empty.path().join("missing")).clear().unwrap(), 0);
1830 }
1831
1832 #[test]
1833 fn a_directory_holding_only_spilled_replies_is_still_a_cache() {
1834 let dir = tempfile::tempdir().unwrap();
1836 fs::create_dir(dir.path().join(SPILL_DIR)).unwrap();
1837 fs::write(dir.path().join(SPILL_DIR).join("ab.txt"), b"reply").unwrap();
1838 let cache = DiskCache::open(dir.path());
1839 assert!(cache.check_usable().is_ok());
1840 cache.put(&ApkKey::from_id("app-0123456789abcdef".into()), "classes.dex", &[0; 4]).unwrap();
1841 cache.clear().unwrap();
1842 assert!(!dir.path().join(SPILL_DIR).exists(), "clear removes spilled replies too");
1843 }
1844
1845 #[test]
1846 fn decompiler_working_directories_keep_a_cache_usable() {
1847 let dir = tempfile::tempdir().unwrap();
1849 fs::create_dir_all(dir.path().join(DECOMPILERS_DIR).join("jeb")).unwrap();
1850 let cache = DiskCache::open(dir.path());
1851 assert!(cache.check_usable().is_ok());
1852 cache.put(&ApkKey::from_id("app-0123456789abcdef".into()), "classes.dex", &[0; 4]).unwrap();
1853 cache.clear().unwrap();
1854 assert!(dir.path().join(DECOMPILERS_DIR).join("jeb").exists(), "a backend's working directory is not the cache's to clear");
1855 }
1856
1857 #[test]
1858 fn legacy_cache_directories_are_adopted() {
1859 let dir = tempfile::tempdir().unwrap();
1861 let cache = DiskCache::open(dir.path()).with_capacity(1);
1862 let legacy = dir.path().join("app-0123456789abcdef");
1863 fs::create_dir(&legacy).unwrap();
1864 fs::write(legacy.join("classes.dex"), [0; 16]).unwrap();
1865 assert_eq!(cache.stats(), (1, 1, 16));
1866 cache.evict();
1867 assert!(!legacy.exists(), "over capacity: evicted");
1868 fs::create_dir(&legacy).unwrap();
1869 fs::write(legacy.join("classes.dex"), [0; 16]).unwrap();
1870 assert_eq!(cache.clear().unwrap(), 16);
1871 let key = ApkKey::from_id("app-0123456789abcdef".into());
1873 cache.put(&key, "classes.dex", &[0; 4]).unwrap();
1874 assert!(dir.path().join(CACHEDIR_TAG).is_file());
1875 }
1876
1877 #[test]
1878 fn a_tag_that_is_not_a_file_is_not_a_tag() {
1879 let dir = tempfile::tempdir().unwrap();
1880 let cache = DiskCache::open(dir.path()).with_capacity(1);
1881 fs::create_dir(dir.path().join(CACHEDIR_TAG)).unwrap();
1882 fs::write(dir.path().join("notes.txt"), b"keep").unwrap();
1883 let key = ApkKey::from_id("app-0123456789abcdef".into());
1884 assert!(matches!(cache.put(&key, "classes.dex", &[0; 4]), Err(CacheError::NotACacheDir { .. })));
1885 assert!(matches!(cache.clear(), Err(CacheError::NotACacheDir { .. })));
1886 }
1887
1888 #[test]
1889 fn dot_files_make_a_directory_foreign() {
1890 let dir = tempfile::tempdir().unwrap();
1892 let cache = DiskCache::open(dir.path());
1893 fs::write(dir.path().join(".gitignore"), b"*\n").unwrap();
1894 assert!(matches!(cache.check_usable(), Err(CacheError::NotACacheDir { .. })));
1895 let key = ApkKey::from_id("app-0123456789abcdef".into());
1896 assert!(matches!(cache.put(&key, "classes.dex", &[0; 4]), Err(CacheError::NotACacheDir { .. })));
1897 assert!(!dir.path().join(CACHEDIR_TAG).exists());
1898 let legacy = tempfile::tempdir().unwrap();
1900 fs::create_dir(legacy.path().join("app-0123456789abcdef")).unwrap();
1901 fs::write(legacy.path().join(".DS_Store"), b"").unwrap();
1902 assert!(DiskCache::open(legacy.path()).check_usable().is_ok());
1903 }
1904
1905 #[test]
1906 fn memory_lru_evicts_by_bytes() {
1907 let mut m: MemoryCache<Vec<u8>> = MemoryCache::new(10);
1908 let k = ApkKey::from_id("k".into());
1909 m.insert(&k, "a", Arc::new(vec![0; 4]), 4);
1910 m.insert(&k, "b", Arc::new(vec![0; 4]), 4);
1911 assert!(m.get(&k, "a").is_some()); m.insert(&k, "c", Arc::new(vec![0; 4]), 4); assert!(m.get(&k, "b").is_none());
1914 assert!(m.get(&k, "a").is_some());
1915 assert_eq!(m.used_bytes(), 8);
1916 assert_eq!(m.len(), 2);
1917 m.insert(&k, "a", Arc::new(vec![0; 2]), 2);
1919 assert_eq!(m.used_bytes(), 6);
1920 m.clear();
1921 assert!(m.is_empty());
1922 }
1923
1924 #[test]
1925 fn memory_lru_recharge_measures_the_current_value_and_evicts() {
1926 let mut m: MemoryCache<Vec<u8>> = MemoryCache::new(10);
1927 let k = ApkKey::from_id("k".into());
1928 m.insert(&k, "a", Arc::new(vec![0; 4]), 4);
1929 m.insert(&k, "b", Arc::new(vec![0; 7]), 4);
1930 assert!(m.recharge(&k, "b", Vec::len));
1932 assert_eq!(m.used_bytes(), 7);
1933 assert!(m.get(&k, "a").is_none());
1934 assert_eq!(m.charged_bytes(&k, "b"), Some(7));
1935 assert!(!m.recharge(&k, "b", Vec::len));
1936 m.insert(&k, "b", Arc::new(vec![1; 9]), 4);
1938 assert!(m.recharge(&k, "b", Vec::len));
1939 assert_eq!(m.charged_bytes(&k, "b"), Some(9));
1940 assert!(!m.recharge(&k, "missing", Vec::len));
1941 assert!(m.recharge(&k, "b", |_| 50));
1943 assert_eq!((m.len(), m.used_bytes()), (1, 50));
1944 }
1945
1946 #[test]
1947 fn memory_lru_capacity_can_shrink_and_peek_keeps_recency() {
1948 let mut m: MemoryCache<u8> = MemoryCache::new(10);
1949 let k = ApkKey::from_id("k".into());
1950 m.insert(&k, "a", Arc::new(1), 4);
1951 m.insert(&k, "b", Arc::new(2), 4);
1952 assert_eq!(*m.peek(&k, "a").unwrap(), 1); m.set_capacity(5);
1954 assert_eq!((m.capacity_bytes(), m.used_bytes()), (5, 4));
1955 assert!(m.peek(&k, "a").is_none());
1956 assert!(m.peek(&k, "b").is_some());
1957 }
1958
1959 #[test]
1960 fn memory_lru_reserved_bytes_make_room_under_the_cap() {
1961 let mut m: MemoryCache<u8> = MemoryCache::new(10);
1962 let k = ApkKey::from_id("k".into());
1963 m.insert(&k, "a", Arc::new(1), 4);
1964 m.insert(&k, "b", Arc::new(2), 4);
1965 m.set_reserved(3);
1966 assert_eq!((m.used_bytes(), m.reserved_bytes()), (4, 3), "a evicted to fit 3 reserved bytes");
1967 assert!(m.peek(&k, "a").is_none());
1968 m.insert(&k, "c", Arc::new(3), 3);
1969 assert_eq!(m.used_bytes(), 7);
1970 m.set_reserved(0);
1971 m.insert(&k, "d", Arc::new(4), 3);
1972 assert_eq!(m.used_bytes(), 10, "nothing reserved: the whole cap holds entries");
1973 }
1974
1975 #[test]
1976 fn memory_lru_replace_keeps_size_and_recency() {
1977 let mut m: MemoryCache<Vec<u8>> = MemoryCache::new(10);
1978 let k = ApkKey::from_id("k".into());
1979 let a = Arc::new(vec![0; 4]);
1980 m.insert(&k, "a", Arc::clone(&a), 4);
1981 m.insert(&k, "b", Arc::new(vec![0; 4]), 4);
1982 assert!(m.get(&k, "a").is_some()); let a2 = Arc::new(vec![9; 4]);
1984 assert!(m.replace(&k, "a", &a, Arc::clone(&a2)));
1985 assert!(!m.replace(&k, "a", &a, Arc::new(vec![])), "stale handle");
1986 assert!(!m.replace(&k, "missing", &a, Arc::new(vec![])));
1987 assert_eq!(m.used_bytes(), 8);
1988 m.insert(&k, "c", Arc::new(vec![0; 4]), 4); assert!(Arc::ptr_eq(&m.get(&k, "a").unwrap(), &a2));
1990 assert!(m.get(&k, "b").is_none());
1991 }
1992
1993 #[test]
1994 fn fnv_lanes_match_scalar() {
1995 fn scalar(bytes: &[u8]) -> u64 {
1996 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1997 for &b in bytes {
1998 h ^= b as u64;
1999 h = h.wrapping_mul(0x0100_0000_01b3);
2000 }
2001 h
2002 }
2003 let a = fnv64(b"PK\x01\x02classes.dex");
2006 let b = fnv64(b"PK\x01\x02classes2.dex");
2007 assert_ne!(a, b);
2008 assert_ne!(fnv64(b""), fnv64(b"\0"));
2009 assert_eq!(fnv64(b"abc"), scalar(b"abc")); }
2011
2012 fn set_mtime(path: &Path, secs_from_now: i64) {
2013 let t = if secs_from_now >= 0 {
2014 SystemTime::now() + Duration::from_secs(secs_from_now as u64)
2015 } else {
2016 SystemTime::now() - Duration::from_secs((-secs_from_now) as u64)
2017 };
2018 open_for_times(path).unwrap().set_modified(t).unwrap();
2019 }
2020
2021 #[test]
2022 fn files_are_checked_when_they_predate_the_boot() {
2023 let now = SystemTime::now();
2024 let boot = Some(now - Duration::from_secs(60));
2025 assert!(predates_boot(Some(now - Duration::from_secs(61)), boot, now));
2026 assert!(!predates_boot(Some(now - Duration::from_secs(59)), boot, now));
2027 assert!(predates_boot(Some(now + Duration::from_secs(5)), boot, now));
2029 assert!(predates_boot(Some(now), None, now));
2030 assert!(predates_boot(None, boot, now));
2031 }
2032
2033 fn booted_cache(dir: &Path) -> DiskCache {
2035 let mut cache = DiskCache::open(dir).with_capacity(1 << 20);
2036 cache.boot = Some(SystemTime::now() - Duration::from_secs(60));
2037 cache
2038 }
2039
2040 fn age(path: &Path, damage: Option<(u64, &[u8])>, cut: Option<u64>) {
2043 let f = fs::OpenOptions::new().write(true).open(path).unwrap();
2044 if let Some((at, bytes)) = damage {
2045 (&f).seek(SeekFrom::Start(at)).unwrap();
2046 (&f).write_all(bytes).unwrap();
2047 }
2048 if let Some(len) = cut {
2049 f.set_len(len).unwrap();
2050 }
2051 f.set_modified(SystemTime::now() - Duration::from_secs(120)).unwrap();
2052 }
2053
2054 #[test]
2055 fn l1_files_from_an_earlier_boot_are_checked() {
2056 let dir = tempfile::tempdir().unwrap();
2057 let cache = booted_cache(dir.path());
2058 let key = ApkKey::from_id("app-0123456789abcdef".into());
2059 let data: Vec<u8> = (0..4096u32).map(|i| i as u8).collect();
2060 let path = cache.entry_path(&key, "classes.dex");
2061 let n = data.len() as u64;
2062
2063 cache.put(&key, "classes.dex", &data).unwrap();
2065 age(&path, None, None);
2066 assert_eq!(&*cache.get(&key, "classes.dex", n).unwrap(), &data[..]);
2067 assert!(!cache.predates_boot(&fs::metadata(&path).unwrap()));
2068
2069 let flipped = [!data[7]];
2071 for (at, bytes) in [(1024, &[0u8; 1024][..]), (7, &flipped[..])] {
2072 cache.put(&key, "classes.dex", &data).unwrap();
2073 age(&path, Some((at, bytes)), None);
2074 assert!(cache.get(&key, "classes.dex", n).is_none(), "damage at {at}");
2075 assert!(!path.exists(), "damage at {at}");
2076 }
2077 for cut in [n, n - 100] {
2080 cache.put(&key, "classes.dex", &data).unwrap();
2081 age(&path, None, Some(cut));
2082 assert!(cache.get(&key, "classes.dex", 0).is_none(), "cut to {cut}");
2083 assert!(!path.exists(), "cut to {cut}");
2084 }
2085
2086 cache.put(&key, "classes.dex", &data).unwrap();
2088 let f = fs::OpenOptions::new().write(true).open(&path).unwrap();
2089 (&f).write_all(&[!data[0]]).unwrap();
2090 f.set_modified(SystemTime::now()).unwrap();
2091 assert_eq!(cache.get(&key, "classes.dex", n).unwrap()[0], !data[0]);
2092 }
2093
2094 #[test]
2095 fn l2_and_l3_files_carry_checksums() {
2096 let dir = tempfile::tempdir().unwrap();
2097 let cache = booted_cache(dir.path());
2098 let key = ApkKey::from_id("app-0123456789abcdef".into());
2099 let path = cache.derived_path(&key, "classes.dex", "locators", 1);
2100 let flip = DERIVED_HEADER as u64 + 3;
2101 cache.put_derived(&key, "classes.dex", "locators", 1, Duration::from_millis(7), b"tables").unwrap();
2102 age(&path, None, None);
2103 assert_eq!(&*cache.get_derived(&key, "classes.dex", "locators", 1).unwrap(), b"tables");
2104 age(&path, Some((flip, b"X")), None);
2105 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
2106 assert!(!path.exists());
2107
2108 cache.put_rendered(&key, "q", b"rows").unwrap();
2110 assert_eq!(cache.get_rendered(&key, "q").unwrap(), b"rows");
2111 let path = cache.rendered_path(&key, "q");
2112 let len = fs::metadata(&path).unwrap().len();
2113 let f = fs::OpenOptions::new().write(true).open(&path).unwrap();
2114 (&f).seek(SeekFrom::Start(len - 1)).unwrap();
2115 (&f).write_all(b"X").unwrap();
2116 assert!(cache.get_rendered(&key, "q").is_none());
2117 assert!(!path.exists());
2118 }
2119
2120 #[test]
2121 fn derived_roundtrip_is_versioned_and_aligned() {
2122 let dir = tempfile::tempdir().unwrap();
2123 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
2124 let key = ApkKey::from_id("app-0123456789abcdef".into());
2125 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
2126 cache.put_derived(&key, "classes.dex", "locators", 1, Duration::from_millis(7), b"tables").unwrap();
2127 let d = cache.get_derived(&key, "classes.dex", "locators", 1).unwrap();
2128 assert_eq!(&*d, b"tables");
2129 assert_eq!(d.build_cost, Duration::from_millis(7));
2130 assert_eq!(d.as_ptr() as usize % 64, 0, "payload as aligned as the mapping");
2131 drop(d);
2132 assert!(cache.get_derived(&key, "classes.dex", "locators", 2).is_none());
2134 assert!(cache.get_derived(&key, "classes.dex", "callgraph", 1).is_none());
2135 assert!(cache.get_derived(&key, "classes2.dex", "locators", 1).is_none());
2136 let path = cache.derived_path(&key, "classes.dex", "locators", 1);
2138 let full = fs::read(&path).unwrap();
2139 fs::write(&path, &full[..full.len() - 1]).unwrap();
2140 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
2141 fs::write(&path, &full[..10]).unwrap();
2142 assert!(cache.get_derived(&key, "classes.dex", "locators", 1).is_none());
2143 let [l1, l2, l3] = cache.tier_stats();
2144 assert_eq!((l1.files, l2.files, l3.files), (0, 1, 0));
2145 }
2146
2147 #[test]
2148 fn rendered_results_are_keyed_by_the_full_query() {
2149 let dir = tempfile::tempdir().unwrap();
2150 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
2151 let key = ApkKey::from_id("app-0123456789abcdef".into());
2152 let q = rendered_key("findrefs", "1.0", "string\0token");
2153 assert!(cache.get_rendered(&key, &q).is_none());
2154 cache.put_rendered(&key, &q, b"rows").unwrap();
2155 assert_eq!(cache.get_rendered(&key, &q).unwrap(), b"rows");
2156 assert!(cache.get_rendered(&key, &rendered_key("findrefs", "1.1", "string\0token")).is_none(), "tool upgrade");
2157 let other = rendered_key("findrefs", "1.0", "string\0other");
2159 fs::rename(cache.rendered_path(&key, &q), cache.rendered_path(&key, &other)).unwrap();
2160 assert!(cache.get_rendered(&key, &other).is_none());
2161 }
2162
2163 #[test]
2164 fn stats_and_clear_cover_every_tier() {
2165 let dir = tempfile::tempdir().unwrap();
2166 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20);
2167 let (a, b) = (ApkKey::from_id("a-0123456789abcdef".into()), ApkKey::from_id("b-0123456789abcdef".into()));
2168 cache.put(&a, "classes.dex", &[0; 10]).unwrap();
2169 cache.put_derived(&a, "classes.dex", "locators", 1, Duration::ZERO, &[0; 6]).unwrap();
2170 cache.put_rendered(&b, "q", &[0; 3]).unwrap();
2171 let (apks, files, bytes) = cache.stats();
2172 assert_eq!((apks, files), (2, 3));
2173 assert_eq!(bytes, (10 + L1_TRAILER) + (64 + 6) + (16 + 1 + 3));
2174 let [l1, l2, l3] = cache.tier_stats();
2175 assert_eq!((l1.apks, l2.apks, l3.apks), (1, 1, 1));
2176 fs::remove_file(dir.path().join(CACHEDIR_TAG)).unwrap();
2178 assert!(cache.check_usable().is_ok());
2179 assert_eq!(cache.clear().unwrap(), bytes);
2180 assert_eq!(cache.stats(), (0, 0, 0));
2181 assert!(!dir.path().join("derived").exists() && !dir.path().join("rendered").exists());
2182 }
2183
2184 #[test]
2185 fn disabled_tiers_miss_and_skip_writes() {
2186 let dir = tempfile::tempdir().unwrap();
2187 let cache = DiskCache::open(dir.path()).with_capacity(1 << 20).with_tiers(&[Tier::Derived]);
2188 let key = ApkKey::from_id("app-0123456789abcdef".into());
2189 cache.put(&key, "classes.dex", b"abc").unwrap();
2190 cache.put_rendered(&key, "q", b"rows").unwrap();
2191 cache.put_derived(&key, "classes.dex", "k", 1, Duration::ZERO, b"t").unwrap();
2192 assert_eq!(cache.tier_stats().map(|t| t.files), [0, 1, 0]);
2193 let all = DiskCache::open(dir.path()).with_capacity(1 << 20).with_tiers(&Tier::ALL);
2194 all.put(&key, "classes.dex", b"abc").unwrap();
2195 assert!(cache.get(&key, "classes.dex", 3).is_none());
2196 assert!(cache.uses(Tier::Derived) && !cache.uses(Tier::Dex));
2197 }
2198
2199 #[test]
2200 fn lazy_eviction_skips_a_walk_right_after_another() {
2201 let dir = tempfile::tempdir().unwrap();
2202 let cache = DiskCache::open(dir.path()).with_capacity(1 << 30);
2203 let key = ApkKey::from_id("app-0123456789abcdef".into());
2204 cache.put(&key, "classes.dex", &[0; 100]).unwrap();
2205 cache.evict();
2206 assert!(dir.path().join(EVICT_MARKER).is_file());
2207 assert!(cache.check_usable().is_ok(), "the marker is the cache's own");
2208 let cache = cache.with_capacity(50);
2210 cache.evict_lazily();
2211 assert_eq!(cache.stats().1, 1);
2212 cache.put(&key, "classes2.dex", &[0; 100]).unwrap();
2214 cache.evict_lazily();
2215 assert_eq!(cache.stats().1, 0);
2216 }
2217
2218 #[test]
2219 fn small_dex_entries_are_not_admitted_by_default() {
2220 let dir = tempfile::tempdir().unwrap();
2221 let cache = DiskCache::open(dir.path());
2222 assert_eq!(cache.min_dex_bytes(), DEFAULT_MIN_DEX_BYTES);
2223 let cache = cache.with_min_dex_bytes(100);
2224 assert!(!cache.admits_dex(99));
2225 assert!(cache.admits_dex(100));
2226 assert!(cache.with_min_dex_bytes(0).admits_dex(0));
2227 }
2228
2229 #[test]
2230 fn eviction_trims_tiers_then_goes_l1_l3_l2() {
2231 let dir = tempfile::tempdir().unwrap();
2232 let cache = DiskCache::open(dir.path()).with_capacity(1000);
2234 let k = |i: u32| ApkKey::from_id(format!("app-{i:016x}"));
2235 for i in 0..4 {
2237 let q = format!("q{i}");
2238 cache.put_rendered(&k(0), &q, &[0; 64]).unwrap();
2239 set_mtime(&cache.rendered_path(&k(0), &q), -100 + i as i64);
2240 }
2241 cache.evict();
2242 let [_, _, l3] = cache.tier_stats();
2243 assert_eq!(l3.files, 1, "trimmed to its own budget, newest kept");
2244 assert!(cache.get_rendered(&k(0), "q3").is_some());
2245 cache.put_derived(&k(1), "classes.dex", "cheap", 1, Duration::from_micros(10), &[0; 136]).unwrap();
2247 cache.put_derived(&k(1), "classes.dex", "dear", 1, Duration::from_secs(1), &[0; 136]).unwrap();
2248 cache.put(&k(2), "classes.dex", &[0; 300]).unwrap();
2249 cache.put(&k(3), "classes.dex", &[0; 300]).unwrap();
2250 set_mtime(&dir.path().join(k(2).as_str()), -50);
2251 cache.evict();
2253 assert!(cache.get(&k(2), "classes.dex", 300).is_none());
2254 assert!(cache.get(&k(3), "classes.dex", 300).is_some());
2255 assert_eq!(cache.tier_stats().map(|t| t.files), [1, 2, 1]);
2256 let cache = cache.with_capacity(450);
2259 cache.evict();
2260 assert_eq!(cache.tier_stats().map(|t| t.files), [0, 1, 0]);
2261 assert!(cache.get_derived(&k(1), "classes.dex", "dear", 1).is_some());
2262 }
2263
2264 #[test]
2265 fn l1_eviction_weighs_size_with_age() {
2266 let dir = tempfile::tempdir().unwrap();
2267 let cache = DiskCache::open(dir.path()).with_capacity(600);
2268 let (big, small) = (ApkKey::from_id("big-0000000000000001".into()), ApkKey::from_id("small-0000000000000002".into()));
2269 cache.put(&small, "classes.dex", &[0; 100]).unwrap();
2271 cache.put(&big, "classes.dex", &[0; 550]).unwrap();
2272 set_mtime(&dir.path().join(small.as_str()), -3);
2273 cache.evict();
2274 assert!(cache.get(&small, "classes.dex", 100).is_some());
2275 assert!(cache.get(&big, "classes.dex", 550).is_none());
2276 }
2277
2278 #[test]
2279 fn entries_of_deleted_apks_go_first() {
2280 let dir = tempfile::tempdir().unwrap();
2281 let apks = tempfile::tempdir().unwrap();
2282 let cache = DiskCache::open(dir.path()).with_capacity(1000);
2283 let (live, dead) = (ApkKey::from_id("live-0000000000000001".into()), ApkKey::from_id("dead-0000000000000002".into()));
2284 let (live_apk, dead_apk) = (apks.path().join("live.apk"), apks.path().join("dead.apk"));
2285 fs::write(&live_apk, b"PK").unwrap();
2286 fs::write(&dead_apk, b"PK").unwrap();
2287 cache.note_source(&live, &live_apk).unwrap();
2288 cache.note_source(&dead, &dead_apk).unwrap();
2289 cache.put(&live, "classes.dex", &[0; 400]).unwrap();
2290 cache.put_derived(&dead, "classes.dex", "locators", 1, Duration::from_secs(9), &[0; 100]).unwrap();
2292 cache.put(&dead, "classes.dex", &[0; 400]).unwrap();
2293 set_mtime(&dir.path().join(live.as_str()), -100);
2294 fs::remove_file(&dead_apk).unwrap();
2295 cache.evict();
2297 assert_eq!(cache.stats().1, 3);
2298 let cache = cache.with_capacity(500);
2299 cache.evict();
2300 assert!(cache.get(&live, "classes.dex", 400).is_some());
2301 assert!(cache.get(&dead, "classes.dex", 400).is_none());
2302 assert!(cache.get_derived(&dead, "classes.dex", "locators", 1).is_none());
2303 assert!(!dir.path().join("sources").join(dead.as_str()).exists(), "source record dropped with its entries");
2304 assert!(dir.path().join("sources").join(live.as_str()).exists());
2305 }
2306}