1mod budget;
23mod callgraph;
24mod classes;
25mod compact;
26mod compose;
27pub mod condition;
28pub mod config;
29mod defs;
30mod hierarchy;
31pub mod hints;
32mod manifest;
33pub mod names;
34mod prebuild;
35mod resident;
36mod resources;
37mod rules;
38mod skip;
39mod sweep;
40pub mod text;
41mod tiers;
42
43use asc_apk::{Apk, Entry, Limits};
44use asc_cache::{ApkKey, DexData, DiskCache, MemoryCache, Tier};
45use asc_dex::Dex;
46use asc_dex::container::{dex041_logical_offsets, logical_dex_buffers, logical_dex_iter};
47pub use asc_findrefs::CompiledQuery;
48use asc_findrefs::{FindRefs, Hit, Query, RefKind, SearchState};
49use asc_rebuild::Rebuilt;
50use std::borrow::Cow;
51use std::collections::{BTreeMap, HashSet};
52use std::path::{Path, PathBuf};
53use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
54use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak, mpsc};
55use std::time::{Duration, Instant, SystemTime};
56
57pub use asc_apk::Limits as InflateLimits;
58pub use asc_apk::archive_of;
59pub use asc_arsc;
60pub use asc_cache;
61pub use asc_findrefs::{ClassQuery, ClassRelation, FieldAccess, MemberQuery};
62pub use asc_smali::Options as SmaliOptions;
63pub use budget::{Device, MemoryPolicy, available_memory, total_memory};
64pub use callgraph::{CallQuery, CallRoots, CallRow, CallTree, Direction};
65pub use classes::{ClassListing, DexClasses, EntryClasses, dex_classes};
66pub use compact::TextRows;
67pub use compose::{DEFAULT_FAN_OUT, MatchingMethods};
68pub use hierarchy::{HierarchyQuery, HierarchyRow, HierarchyTree, Lineage};
69pub use manifest::{Application, Component, DataSpec, IntentFilter, ManifestSummary, Permission, protection_level_name};
70pub use prebuild::PrebuildReport;
71pub use resources::{ResourceField, ResourceRow, parse_id};
72pub use rules::compile_rules;
73pub use sweep::{Claim, DexDedup};
74pub use tiers::{Busy, auto_threads, auto_threads_for};
75
76#[derive(Debug, thiserror::Error)]
77pub enum EngineError {
78 #[error(transparent)]
79 Apk(#[from] asc_apk::ApkError),
80 #[error(transparent)]
81 Dex(#[from] asc_dex::DexError),
82 #[error(transparent)]
83 Find(#[from] asc_findrefs::FindError),
84 #[error(transparent)]
85 Rebuild(#[from] asc_rebuild::RebuildError),
86 #[error(transparent)]
87 Smali(#[from] asc_smali::SmaliError),
88 #[error(transparent)]
89 Axml(#[from] asc_axml::AxmlError),
90 #[error(transparent)]
91 Arsc(#[from] asc_arsc::ArscError),
92 #[error("class {class} not found in APK{hint}")]
94 ClassNotFound { class: String, hint: String },
95 #[error("{0}")]
96 Query(String),
97 #[error("{dex}: {reason}")]
98 BadDex { dex: String, reason: String },
99 #[error("worker panicked while processing {0}")]
100 WorkerPanic(String),
101}
102
103pub type Result<T> = std::result::Result<T, EngineError>;
104
105pub type DexBytes = Arc<DexData>;
107
108#[derive(Clone)]
110pub struct ClassHit {
111 pub dex_name: String,
112 pub dex: DexBytes,
113 pub also_defined_in: Vec<String>,
116}
117
118#[derive(Clone)]
120pub struct ClassSource {
121 pub dex_name: String,
122 pub source: String,
123 pub rebuilt: Option<Rebuilt>,
127 pub warning: Option<String>,
129 pub scan_us: u128,
131 pub also_defined_in: Vec<String>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct RefResult {
142 pub dex_name: String,
143 pub method_idx: u32,
144 pub method: String,
146 pub signature: String,
148 pub matched: Vec<Matched>,
150 pub relation: Option<ClassRelation>,
152 pub sites: Vec<RefSite>,
154 pub context: Option<String>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct RefSite {
161 pub offset: u32,
163 pub matched: usize,
165 pub field_access: Option<FieldAccess>,
167 pub pc: Option<u32>,
169 pub line: Option<u32>,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub enum RefContext {
176 #[default]
178 None,
179 Lines(usize),
181 Method,
183}
184
185#[derive(Debug, Clone, Copy)]
187pub struct FindOptions {
188 pub aggregate: bool,
190 pub context: RefContext,
191}
192
193impl Default for FindOptions {
194 fn default() -> Self {
195 FindOptions { aggregate: true, context: RefContext::None }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum Matched {
202 String(String),
204 Symbol(String),
206 Field { symbol: String, access: FieldAccess },
208}
209
210impl Matched {
211 pub fn as_str(&self) -> &str {
213 match self {
214 Matched::String(s) | Matched::Symbol(s) | Matched::Field { symbol: s, .. } => s,
215 }
216 }
217
218 pub fn field_access(&self) -> Option<FieldAccess> {
220 match self {
221 Matched::Field { access, .. } => Some(*access),
222 _ => None,
223 }
224 }
225}
226
227impl std::fmt::Display for Matched {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 match self {
232 Matched::String(s) => f.write_str(&text::quoted(s)),
233 Matched::Symbol(s) => f.write_str(s),
234 Matched::Field { symbol, access } => write!(f, "{symbol} [{}]", access.as_str()),
235 }
236 }
237}
238
239impl RefResult {
240 pub fn line(&self) -> String {
243 let matched = self.matched.iter().map(Matched::to_string).collect::<Vec<_>>().join(", ");
244 match self.relation {
245 Some(rel) => format!("{} | {} | {} {matched}", self.dex_name, self.signature, rel.verb()),
246 None => format!("{} | {} | {matched}", self.dex_name, self.signature),
247 }
248 }
249
250 pub fn text(&self) -> String {
252 let mut out = self.line();
253 if let Some(c) = &self.context {
254 out.push('\n');
255 out.push_str(c.trim_end_matches('\n'));
256 }
257 out
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct RefBatch {
264 pub entry_name: String,
265 pub results: Vec<RefResult>,
266 pub text: Option<TextRows>,
269 pub inflate_us: u128,
270 pub search_us: u128,
271 pub source: &'static str,
273 pub error: Option<String>,
274}
275
276impl RefBatch {
277 pub fn row_count(&self) -> usize {
279 self.results.len() + self.text.as_ref().map_or(0, TextRows::len)
280 }
281
282 fn note_error(&mut self, name: &str, e: &str) {
287 let msg = if name == self.entry_name { e.to_string() } else { format!("{name}: {e}") };
288 self.error = Some(match self.error.take() {
289 Some(prev) => format!("{prev}; {msg}"),
290 None => msg,
291 });
292 }
293}
294
295#[derive(Debug, Clone, Default)]
297pub struct RefSummary {
298 pub entries: usize,
299 pub total_results: usize,
300 pub total_us: u128,
301 pub notes: Vec<String>,
304}
305
306pub struct CacheConfig {
308 pub memory_bytes: usize,
310 pub disk: Option<DiskCache>,
312 pub memory_auto: Option<MemoryPolicy>,
316 pub write_queue_bytes: usize,
319 pub writers: Option<usize>,
321}
322
323impl CacheConfig {
324 pub const WRITE_QUEUE: usize = 512 << 20;
329 pub const MAX_AUTO_WRITERS: usize = 4;
332
333 pub fn disk_default() -> CacheConfig {
335 CacheConfig { disk: DiskCache::open_default(), ..CacheConfig::default() }
336 }
337
338 pub fn resident(memory_bytes: usize) -> CacheConfig {
340 CacheConfig { memory_bytes, disk: DiskCache::open_default(), ..CacheConfig::default() }
341 }
342
343 pub fn auto_writers(cpus: usize) -> usize {
345 (cpus / 8).clamp(1, CacheConfig::MAX_AUTO_WRITERS)
346 }
347}
348
349impl Default for CacheConfig {
350 fn default() -> CacheConfig {
351 CacheConfig { memory_bytes: 0, disk: None, memory_auto: None, write_queue_bytes: CacheConfig::WRITE_QUEUE, writers: None }
352 }
353}
354
355pub struct SharedCache(Option<Arc<EngineCache>>);
361
362impl SharedCache {
363 pub fn new(config: CacheConfig, logger: impl Fn(&str) + Send + Sync + 'static) -> SharedCache {
366 let mut holder = Engine::new("").with_logger(logger).with_cache(config);
367 SharedCache(holder.cache.take())
368 }
369
370 pub fn memory_bytes(&self) -> usize {
372 self.0.as_ref().and_then(|c| c.memory.as_ref()).map(|m| m.lock().unwrap().used_bytes()).unwrap_or(0)
373 }
374
375 pub fn memory_capacity(&self) -> usize {
377 self.0.as_ref().and_then(|c| c.memory.as_ref()).map(|m| m.lock().unwrap().capacity_bytes()).unwrap_or(0)
378 }
379
380 pub fn memory_reserved(&self) -> usize {
383 self.0.as_ref().and_then(|c| c.memory.as_ref()).map(|m| m.lock().unwrap().reserved_bytes()).unwrap_or(0)
384 }
385
386 pub fn set_reserved(&self, bytes: usize) {
390 if let Some(m) = self.0.as_ref().and_then(|c| c.memory.as_ref()) {
391 m.lock().unwrap().set_reserved(bytes);
392 }
393 }
394
395 pub fn flush_writes(&self) {
397 if let Some(c) = &self.0 {
398 c.flush(false);
399 }
400 }
401}
402
403impl Drop for SharedCache {
404 fn drop(&mut self) {
405 if let Some(c) = &self.0 {
406 c.flush(true);
407 }
408 }
409}
410
411pub struct LogicalDex {
415 pub name: String,
416 pub data: DexBytes,
417 pub state: Arc<SearchState>,
418 persisted: Arc<tiers::Persisted>,
419}
420
421pub const MAPPED_CHARGE_DIVISOR: usize = 8;
434
435pub struct CachedEntry {
437 pub name: String,
438 pub data: DexBytes,
439 logical: OnceLock<Vec<LogicalDex>>,
440}
441
442impl CachedEntry {
443 fn new(name: &str, data: DexBytes) -> CachedEntry {
444 CachedEntry { name: name.to_string(), data, logical: OnceLock::new() }
445 }
446
447 pub fn logical(&self) -> &[LogicalDex] {
449 self.logical.get_or_init(|| {
450 let bufs = logical_dex_buffers(&self.name, &self.data);
451 bufs.into_iter()
452 .map(|(name, buf)| {
453 let data = match buf {
454 Cow::Borrowed(_) => Arc::clone(&self.data),
455 Cow::Owned(v) => Arc::new(DexData::Owned(v)),
456 };
457 LogicalDex { name, data, state: Arc::new(SearchState::new()), persisted: Arc::default() }
458 })
459 .collect()
460 })
461 }
462
463 pub fn resident_bytes(&self) -> usize {
468 self.data.len() * dex041_logical_offsets(&self.data).len()
469 }
470
471 pub fn state_bytes(&self) -> usize {
474 self.logical.get().map_or(0, |l| l.iter().map(|d| d.state.heap_bytes() + d.state.mapped_bytes()).sum())
475 }
476
477 pub fn charged_bytes(&self) -> usize {
482 let copies = dex041_logical_offsets(&self.data).len();
483 let len = self.data.len();
484 let own =
485 if self.is_mapped() { (len / MAPPED_CHARGE_DIVISOR) * copies.min(1) + len * copies.saturating_sub(1) } else { len * copies };
486 let tables = self
487 .logical
488 .get()
489 .map_or(0, |l| l.iter().map(|d| d.state.heap_bytes() + d.state.mapped_bytes() / MAPPED_CHARGE_DIVISOR).sum::<usize>());
490 own + tables
491 }
492
493 fn with_data(&self, data: DexBytes) -> CachedEntry {
497 let logical = OnceLock::new();
498 if let Some(old) = self.logical.get() {
499 let moved = old
500 .iter()
501 .map(|l| LogicalDex {
502 name: l.name.clone(),
503 data: if Arc::ptr_eq(&l.data, &self.data) { Arc::clone(&data) } else { Arc::clone(&l.data) },
504 state: Arc::clone(&l.state),
505 persisted: Arc::clone(&l.persisted),
506 })
507 .collect();
508 let _ = logical.set(moved);
509 }
510 CachedEntry { name: self.name.clone(), data, logical }
511 }
512
513 fn with_state(&self, idx: usize, state: Arc<SearchState>) -> CachedEntry {
516 let logical = OnceLock::new();
517 if let Some(old) = self.logical.get() {
518 let moved = old
519 .iter()
520 .enumerate()
521 .map(|(i, l)| LogicalDex {
522 name: l.name.clone(),
523 data: Arc::clone(&l.data),
524 state: if i == idx { Arc::clone(&state) } else { Arc::clone(&l.state) },
525 persisted: Arc::clone(&l.persisted),
526 })
527 .collect();
528 let _ = logical.set(moved);
529 }
530 CachedEntry { name: self.name.clone(), data: Arc::clone(&self.data), logical }
531 }
532
533 pub fn is_mapped(&self) -> bool {
535 matches!(*self.data, DexData::Mapped(_))
536 }
537}
538
539struct WriteJob {
541 key: ApkKey,
542 apk: Arc<Path>,
543 kind: Job,
544}
545
546const CANCEL_POLL: Duration = Duration::from_millis(20);
548
549pub(crate) static NOT_CANCELLED: AtomicBool = AtomicBool::new(false);
551
552pub(crate) fn cancelled() -> EngineError {
554 asc_apk::ApkError::Cancelled.into()
555}
556
557impl WriteJob {
558 fn held_bytes(&self) -> usize {
561 match &self.kind {
562 Job::Dex { data, .. } => data.len(),
563 Job::Derived(d) => d.held_bytes(),
564 Job::Graph { graph, .. } => graph.heap_bytes(),
565 Job::Rendered { .. } | Job::Reinflate { .. } => 0,
566 }
567 }
568
569 fn defer(self) -> Option<WriteJob> {
574 let WriteJob { key, apk, kind } = self;
575 let kind = match kind {
576 Job::Dex { entry, limits, remap, .. } => Job::Reinflate { entry, limits, remap },
577 Job::Derived(d) => {
578 d.abandon();
579 return None;
580 }
581 Job::Graph { .. } => return None,
582 kind @ (Job::Rendered { .. } | Job::Reinflate { .. }) => kind,
583 };
584 Some(WriteJob { key, apk, kind })
585 }
586
587 fn abandon(self) {
590 if let Job::Derived(d) = self.kind {
591 d.abandon();
592 }
593 }
594}
595
596enum Job {
597 Dex {
600 entry: Entry,
601 limits: Limits,
603 data: DexBytes,
604 remap: Option<Weak<CachedEntry>>,
607 },
608 Reinflate { entry: Entry, limits: Limits, remap: Option<Weak<CachedEntry>> },
611 Derived(tiers::DerivedJob),
613 Rendered { query: String, payload: Vec<u8> },
615 Graph { graph: Arc<hierarchy::ClassGraph>, cost: Duration },
618}
619
620struct Writer {
621 tx: mpsc::Sender<(WriteJob, usize)>,
623 handles: Vec<std::thread::JoinHandle<()>>,
624}
625
626type SharedMemory = Arc<Mutex<MemoryCache<CachedEntry>>>;
627
628struct EngineCache {
629 memory: Option<SharedMemory>,
630 budget: Option<tiers::MemoryBudget>,
632 disk: Option<Arc<DiskCache>>,
633 writer: Mutex<Option<Writer>>,
634 closed: AtomicBool,
637 inflight: Mutex<HashSet<String>>,
640 inflight_done: Condvar,
641 derived_pending: Arc<AtomicUsize>,
644 pending_bytes: Arc<AtomicUsize>,
646 queue_cap: usize,
648 writers: Option<usize>,
650}
651
652impl EngineCache {
653 fn queue_cap(&self) -> usize {
659 let budget = self.memory.as_ref().map_or(usize::MAX, |m| m.lock().unwrap().capacity_bytes());
660 self.queue_cap.min(budget)
661 }
662
663 fn put_async(&self, job: WriteJob) {
669 let Some(disk) = &self.disk else { return };
670 let cap = self.queue_cap();
672 let mut writer = self.writer.lock().unwrap();
673 if self.closed.load(Ordering::Relaxed) {
674 return;
675 }
676 let pending = self.pending_bytes.load(Ordering::Relaxed);
677 let job = if pending > 0 && pending + job.held_bytes() > cap {
678 match job.defer() {
679 Some(job) => job,
680 None => return,
681 }
682 } else {
683 job
684 };
685 let held = job.held_bytes();
686 if writer.is_none() {
687 match self.spawn_writers(disk) {
688 Some(w) => *writer = Some(w),
689 None => {
690 run_job(disk, self.memory.as_ref(), job, &mut WriterState::default());
691 disk.evict_lazily();
692 return;
693 }
694 }
695 }
696 if let Some(w) = writer.as_ref() {
697 let derived = matches!(job.kind, Job::Derived(_));
698 if derived {
699 self.derived_pending.fetch_add(1, Ordering::Relaxed);
700 }
701 self.pending_bytes.fetch_add(held, Ordering::Relaxed);
702 if let Err(mpsc::SendError((job, _))) = w.tx.send((job, held)) {
703 if derived {
704 self.derived_pending.fetch_sub(1, Ordering::Relaxed);
705 }
706 self.pending_bytes.fetch_sub(held, Ordering::Relaxed);
707 job.abandon();
708 *writer = None;
709 }
710 }
711 }
712
713 fn flush(&self, close: bool) {
716 let taken = {
717 let mut writer = self.writer.lock().unwrap();
718 if close {
721 self.closed.store(true, Ordering::Relaxed);
722 }
723 writer.take()
724 };
725 if let Some(Writer { tx, handles }) = taken {
726 drop(tx);
727 for handle in handles {
728 let _ = handle.join();
729 }
730 }
731 }
732
733 fn spawn_writers(&self, disk: &Arc<DiskCache>) -> Option<Writer> {
736 let (tx, rx) = mpsc::channel::<(WriteJob, usize)>();
737 let rx = Arc::new(Mutex::new(rx));
738 let count = self.writers.unwrap_or_else(|| CacheConfig::auto_writers(tiers::cpus())).max(1);
739 let handles: Vec<_> = (0..count)
740 .map_while(|_| {
741 let (rx, disk, memory) = (Arc::clone(&rx), Arc::clone(disk), self.memory.clone());
742 let (derived_pending, pending_bytes) = (Arc::clone(&self.derived_pending), Arc::clone(&self.pending_bytes));
743 std::thread::Builder::new()
744 .name("asc-cache-writer".into())
745 .spawn(move || {
746 prebuild::lower_priority();
748 let mut state = WriterState::default();
749 loop {
750 let Ok((job, held)) = rx.lock().expect("writer queue mutex poisoned").recv() else { break };
752 let derived = matches!(job.kind, Job::Derived(_));
753 run_job(&disk, memory.as_ref(), job, &mut state);
754 pending_bytes.fetch_sub(held, Ordering::Relaxed);
755 if derived {
756 derived_pending.fetch_sub(1, Ordering::Relaxed);
757 }
758 disk.evict_lazily();
761 }
762 })
763 .ok()
764 })
765 .collect();
766 (!handles.is_empty()).then_some(Writer { tx, handles })
767 }
768
769 fn charged(&self) {
771 if let (Some(budget), Some(memory)) = (&self.budget, &self.memory) {
772 budget.maybe_rebalance(memory);
773 }
774 }
775
776 fn begin_inflate(&self, id: String) -> InflightGuard<'_> {
777 let mut set = self.inflight.lock().unwrap();
778 while set.contains(&id) {
779 set = self.inflight_done.wait(set).unwrap();
780 }
781 set.insert(id.clone());
782 InflightGuard { cache: self, id }
783 }
784}
785
786#[derive(Default)]
788struct WriterState {
789 noted: HashSet<ApkKey>,
791 opened: Option<(Arc<Path>, ApkKey, Apk)>,
794}
795
796impl WriterState {
797 fn apk(&mut self, path: &Arc<Path>, key: &ApkKey, limits: Limits) -> Option<&Apk> {
799 if !matches!(&self.opened, Some((p, k, _)) if p == path && k == key) {
800 self.opened = None;
801 let apk = Apk::open(path).ok()?.with_limits(limits);
802 if ApkKey::for_apk(&apk).ok()? != *key {
804 return None;
805 }
806 self.opened = Some((Arc::clone(path), key.clone(), apk));
807 }
808 self.opened.as_ref().map(|(_, _, apk)| apk)
809 }
810}
811
812fn run_job(disk: &DiskCache, memory: Option<&SharedMemory>, job: WriteJob, state: &mut WriterState) {
815 let WriteJob { key, apk, kind } = job;
816 if state.noted.insert(key.clone()) {
817 let _ = disk.note_source(&key, &apk);
818 }
819 match kind {
820 Job::Dex { entry, data, remap, .. } => {
821 if disk.put(&key, &entry.name, &data).is_ok() {
822 remap_entry(disk, memory, key, entry.name, data, remap);
823 }
824 }
825 Job::Reinflate { entry, limits, remap } => {
826 if disk.get(&key, &entry.name, entry.uncomp_size).is_some() {
828 return;
829 }
830 let Some(archive) = state.apk(&apk, &key, limits) else { return };
831 let Ok(Cow::Owned(bytes)) = archive.read_entry(&entry) else { return };
832 let data: DexBytes = Arc::new(DexData::Owned(bytes));
833 if disk.put(&key, &entry.name, &data).is_ok() {
834 remap_entry(disk, memory, key, entry.name, data, remap);
835 }
836 }
837 Job::Derived(d) => tiers::write_derived(disk, memory, &key, d),
838 Job::Rendered { query, payload } => {
839 let _ = disk.put_rendered(&key, &query, &payload);
840 }
841 Job::Graph { graph, cost } => {
842 let _ = disk.put_derived(&key, hierarchy::GRAPH_NAME, hierarchy::GRAPH_KIND, hierarchy::GRAPH_FORMAT, cost, graph.bytes());
843 }
844 }
845}
846
847fn remap_entry(
853 disk: &DiskCache,
854 memory: Option<&SharedMemory>,
855 key: ApkKey,
856 name: String,
857 data: DexBytes,
858 remap: Option<Weak<CachedEntry>>,
859) {
860 drop(data);
863 let (Some(memory), Some(entry)) = (memory, remap.and_then(|w| w.upgrade())) else { return };
864 if entry.is_mapped() {
865 return;
866 }
867 let Some(mapped) = disk.get(&key, &name, entry.data.len() as u64) else { return };
868 if *mapped != **entry.data {
869 return;
870 }
871 let new = Arc::new(entry.with_data(Arc::new(mapped)));
872 let size = new.charged_bytes();
874 let mut memory = memory.lock().unwrap();
875 if memory.replace(&key, &name, &entry, Arc::clone(&new)) {
876 memory.resize(&key, &name, &new, size);
877 }
878}
879
880fn tune_allocator() {
888 #[cfg(all(target_os = "linux", target_env = "gnu"))]
889 {
890 static ONCE: std::sync::Once = std::sync::Once::new();
891 ONCE.call_once(|| {
892 unsafe extern "C" {
893 fn mallopt(param: std::os::raw::c_int, value: std::os::raw::c_int) -> std::os::raw::c_int;
894 }
895 const M_MMAP_THRESHOLD: std::os::raw::c_int = -3;
896 unsafe {
898 mallopt(M_MMAP_THRESHOLD, 1 << 20);
899 }
900 });
901 }
902}
903
904struct InflightGuard<'c> {
905 cache: &'c EngineCache,
906 id: String,
907}
908
909impl Drop for InflightGuard<'_> {
910 fn drop(&mut self) {
911 self.cache.inflight.lock().unwrap().remove(&self.id);
912 self.cache.inflight_done.notify_all();
913 }
914}
915
916#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
918pub struct CacheStats {
919 pub memory_hits: u64,
920 pub disk_hits: u64,
921 pub derived_hits: u64,
924 pub inflates: u64,
925 pub skipped: u64,
928}
929
930type Logger = Arc<dyn Fn(&str) + Send + Sync>;
931
932#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934enum Source {
935 Memory,
936 Disk,
937 Stored,
938 Inflate,
939 Skipped,
941}
942
943impl Source {
944 fn as_str(self) -> &'static str {
945 match self {
946 Source::Memory => "memory",
947 Source::Disk => "disk",
948 Source::Stored => "stored",
949 Source::Inflate => "inflate",
950 Source::Skipped => "skipped",
951 }
952 }
953}
954
955enum Loaded<'a> {
956 Borrowed(Cow<'a, [u8]>),
957 Cached(Arc<CachedEntry>),
958}
959
960impl<'a> Loaded<'a> {
961 fn bytes(&self) -> &[u8] {
962 match self {
963 Loaded::Borrowed(c) => c,
964 Loaded::Cached(e) => &e.data,
965 }
966 }
967
968 fn into_logical(self, entry_name: &str) -> Box<dyn Iterator<Item = (String, DexBytes)> + 'a> {
972 match self {
973 Loaded::Cached(e) => {
974 let logical: Vec<_> = e.logical().iter().map(|l| (l.name.clone(), Arc::clone(&l.data))).collect();
975 Box::new(logical.into_iter())
976 }
977 Loaded::Borrowed(c) => {
978 let offsets = dex041_logical_offsets(&c);
979 if offsets.len() <= 1 {
980 return Box::new(std::iter::once((entry_name.to_string(), Arc::new(DexData::Owned(c.into_owned())))));
981 }
982 let name = entry_name.to_string();
983 Box::new(offsets.into_iter().enumerate().map(move |(i, off)| {
984 let data = asc_dex::container::normalize_dex041_logical(&c, off).into_owned();
985 (format!("{name}!classes{}.dex", i + 1), Arc::new(DexData::Owned(data)))
986 }))
987 }
988 }
989 }
990}
991
992pub struct Engine {
994 apk_path: PathBuf,
995 threads: Threads,
996 debug: bool,
997 logger: Logger,
998 cache: Option<Arc<EngineCache>>,
999 shared_cache: bool,
1001 stats: Arc<Stats>,
1002 key_cache: Mutex<Option<(u64, Option<SystemTime>, ApkKey)>>,
1004 opened: Mutex<Option<Apk>>,
1007 limits: Limits,
1008 resources: Mutex<Option<resources::CachedTable>>,
1010 prebuild: prebuild::Prebuild,
1013 class_graph: Mutex<Option<hierarchy::CachedGraph>>,
1015}
1016
1017pub struct SearchTarget {
1020 apk: Apk,
1021 key: Option<ApkKey>,
1022 pub entries: Vec<Entry>,
1024}
1025
1026#[derive(Default)]
1027struct Stats {
1028 memory_hits: AtomicUsize,
1029 disk_hits: AtomicUsize,
1030 derived_hits: AtomicUsize,
1031 inflates: AtomicUsize,
1032 skipped: AtomicUsize,
1033}
1034
1035#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1037pub enum Threads {
1038 #[default]
1040 Auto,
1041 Fixed(usize),
1042}
1043
1044const TURN_POLL: Duration = Duration::from_millis(10);
1046
1047impl Threads {
1048 pub fn count(self) -> usize {
1050 match self {
1051 Threads::Auto => auto_threads(),
1052 Threads::Fixed(n) => n.max(1),
1053 }
1054 }
1055
1056 pub fn most(self) -> usize {
1058 match self {
1059 Threads::Auto => tiers::cpus(),
1060 Threads::Fixed(n) => n.max(1),
1061 }
1062 }
1063
1064 pub fn wait_turn(self, k: usize, stop: impl Fn() -> bool) {
1071 if self == Threads::Auto {
1072 while k >= auto_threads() && !stop() {
1073 std::thread::sleep(TURN_POLL);
1074 }
1075 }
1076 }
1077}
1078
1079impl From<usize> for Threads {
1080 fn from(n: usize) -> Threads {
1081 Threads::Fixed(n)
1082 }
1083}
1084
1085impl From<Option<usize>> for Threads {
1087 fn from(n: Option<usize>) -> Threads {
1088 n.map_or(Threads::Auto, Threads::Fixed)
1089 }
1090}
1091
1092impl Engine {
1093 pub fn new(apk_path: impl AsRef<Path>) -> Engine {
1097 Engine {
1098 apk_path: apk_path.as_ref().to_path_buf(),
1099 threads: Threads::Auto,
1100 debug: false,
1101 logger: Arc::new(|m| println!("{m}")),
1102 cache: None,
1103 shared_cache: false,
1104 stats: Arc::new(Stats::default()),
1105 key_cache: Mutex::new(None),
1106 opened: Mutex::new(None),
1107 limits: Limits::default(),
1108 resources: Mutex::new(None),
1109 prebuild: prebuild::Prebuild::default(),
1110 class_graph: Mutex::new(None),
1111 }
1112 }
1113
1114 pub fn with_limits(mut self, limits: Limits) -> Engine {
1116 self.limits = limits;
1117 self
1118 }
1119
1120 pub fn with_threads(mut self, threads: impl Into<Threads>) -> Engine {
1121 self.threads = threads.into();
1122 self
1123 }
1124
1125 pub fn with_debug(mut self, debug: bool) -> Engine {
1126 self.debug = debug;
1127 self
1128 }
1129
1130 pub fn with_logger(mut self, logger: impl Fn(&str) + Send + Sync + 'static) -> Engine {
1132 self.logger = Arc::new(logger);
1133 self
1134 }
1135
1136 pub fn with_cache(mut self, mut config: CacheConfig) -> Engine {
1140 if let Some(Err(e)) = config.disk.as_ref().map(DiskCache::check_usable) {
1141 (self.logger)(&format!("Warning: {e}; disk cache disabled"));
1142 config.disk = None;
1143 }
1144 if config.memory_bytes > 0 {
1145 tune_allocator();
1146 }
1147 if config.memory_bytes == 0 && config.disk.is_none() {
1148 self.cache = None;
1149 } else {
1150 let cache = EngineCache {
1151 memory: (config.memory_bytes > 0).then(|| Arc::new(Mutex::new(MemoryCache::new(config.memory_bytes)))),
1152 budget: (config.memory_bytes > 0).then(|| tiers::MemoryBudget::new(config.memory_auto)),
1153 disk: config.disk.map(Arc::new),
1154 writer: Mutex::new(None),
1155 closed: AtomicBool::new(false),
1156 inflight: Mutex::new(HashSet::new()),
1157 inflight_done: Condvar::new(),
1158 derived_pending: Arc::new(AtomicUsize::new(0)),
1159 pending_bytes: Arc::new(AtomicUsize::new(0)),
1160 queue_cap: config.write_queue_bytes,
1161 writers: config.writers,
1162 };
1163 cache.charged();
1165 self.cache = Some(Arc::new(cache));
1166 }
1167 self
1168 }
1169
1170 pub fn with_shared_cache(mut self, cache: &SharedCache) -> Engine {
1172 self.cache = cache.0.clone();
1173 self.shared_cache = true;
1174 self
1175 }
1176
1177 pub fn apk_path(&self) -> &Path {
1178 &self.apk_path
1179 }
1180
1181 pub fn threads(&self) -> usize {
1183 self.threads.count()
1184 }
1185
1186 pub fn cache_stats(&self) -> CacheStats {
1187 CacheStats {
1188 memory_hits: self.stats.memory_hits.load(Ordering::Relaxed) as u64,
1189 disk_hits: self.stats.disk_hits.load(Ordering::Relaxed) as u64,
1190 derived_hits: self.stats.derived_hits.load(Ordering::Relaxed) as u64,
1191 inflates: self.stats.inflates.load(Ordering::Relaxed) as u64,
1192 skipped: self.stats.skipped.load(Ordering::Relaxed) as u64,
1193 }
1194 }
1195
1196 pub fn resident_bytes(&self) -> usize {
1201 let graph = self.class_graph.lock().unwrap().as_ref().map_or(0, |(_, g)| g.heap_bytes());
1202 let table = self.resources.lock().unwrap().as_ref().map_or(0, resources::CachedTable::heap_bytes);
1203 graph + table
1204 }
1205
1206 pub fn memory_cache_bytes(&self) -> usize {
1209 self.cache.as_ref().and_then(|c| c.memory.as_ref()).map(|m| m.lock().unwrap().used_bytes()).unwrap_or(0)
1210 }
1211
1212 pub fn release_memory_cache(&self) -> usize {
1216 self.release_resources();
1217 self.class_graph.lock().unwrap().take();
1218 let Some(memory) = self.cache.as_ref().and_then(|c| c.memory.as_ref()) else { return 0 };
1219 let key = self.key_cache.lock().unwrap().as_ref().map(|(_, _, key)| key.clone());
1220 let Some(key) = key.or_else(|| self.open().ok().and_then(|apk| self.apk_key(&apk))) else { return 0 };
1221 memory.lock().unwrap().remove_apk(&key)
1222 }
1223
1224 pub fn memory_cache_mapped_bytes(&self) -> usize {
1227 let Some(m) = self.cache.as_ref().and_then(|c| c.memory.as_ref()) else { return 0 };
1228 m.lock().unwrap().values().filter(|e| e.is_mapped()).map(|e| e.data.len()).sum()
1229 }
1230
1231 pub fn memory_cache_mapped_table_bytes(&self) -> usize {
1235 let Some(m) = self.cache.as_ref().and_then(|c| c.memory.as_ref()) else { return 0 };
1236 let m = m.lock().unwrap();
1237 m.values().filter_map(|e| e.logical.get()).flatten().map(|l| l.state.mapped_bytes()).sum()
1238 }
1239
1240 pub fn flush_cache_writes(&self) {
1242 if let Some(c) = &self.cache {
1243 c.flush(false);
1244 }
1245 }
1246
1247 pub fn close_cache_writes(&self) {
1251 if let Some(c) = &self.cache {
1252 c.flush(true);
1253 }
1254 }
1255
1256 fn log(&self, msg: impl AsRef<str>) {
1257 if self.debug {
1258 (self.logger)(msg.as_ref());
1259 }
1260 }
1261
1262 pub fn open(&self) -> Result<Apk> {
1265 let mut opened = self.opened.lock().unwrap();
1266 if let Some(apk) = opened.as_ref()
1269 && *apk.version() == Apk::version_of(&self.apk_path)?
1270 {
1271 return Ok(apk.clone().with_limits(self.limits));
1272 }
1273 let apk = Apk::open(&self.apk_path)?.with_limits(self.limits);
1274 *opened = Some(apk.clone());
1275 Ok(apk)
1276 }
1277
1278 pub fn resolve_path(apk_path: &Path, base: &Path) -> PathBuf {
1281 Apk::resolve(apk_path, base)
1282 }
1283
1284 pub fn can_open(apk_path: &Path) -> bool {
1286 Apk::open(apk_path).is_ok()
1287 }
1288
1289 pub fn program_notes(&self) -> Vec<String> {
1292 if self.apk_path.is_file() {
1293 return Vec::new();
1294 }
1295 self.open().map(|apk| apk.notes().to_vec()).unwrap_or_default()
1296 }
1297
1298 pub fn search_target(&self) -> Result<SearchTarget> {
1300 let apk = self.open()?;
1301 let entries = apk.dex_entries()?;
1302 let key = self.apk_key(&apk);
1303 Ok(SearchTarget { apk, key, entries })
1304 }
1305
1306 pub fn search_target_entry(
1309 &self,
1310 target: &SearchTarget,
1311 entry: &Entry,
1312 query: &CompiledQuery,
1313 aggregate: bool,
1314 cancel: &AtomicBool,
1315 ) -> RefBatch {
1316 search_entry(
1317 &target.apk,
1318 target.key.as_ref(),
1319 entry,
1320 query,
1321 FindOptions { aggregate, context: RefContext::None },
1322 Shape::Results,
1323 None,
1324 cancel,
1325 self.cache.as_deref(),
1326 &self.stats,
1327 )
1328 }
1329
1330 pub fn dex_entries(&self) -> Result<Vec<Entry>> {
1332 Ok(self.open()?.dex_entries()?)
1333 }
1334
1335 pub fn apk_key(&self, apk: &Apk) -> Option<ApkKey> {
1338 self.cache.as_ref()?;
1339 let ident = apk.stamp().ok()?;
1340 let mut cached = self.key_cache.lock().unwrap();
1341 if let Some((len, mtime, key)) = cached.as_ref()
1342 && (*len, *mtime) == ident
1343 {
1344 return Some(key.clone());
1345 }
1346 let key = ApkKey::for_apk(apk).ok()?;
1347 *cached = Some((ident.0, ident.1, key.clone()));
1348 Some(key)
1349 }
1350
1351 pub fn entry_bytes(&self, apk: &Apk, key: Option<&ApkKey>, entry: &Entry) -> Result<DexBytes> {
1354 Ok(match load_entry(apk, key, entry, self.cache.as_deref(), &self.stats, None)?.0 {
1355 Loaded::Borrowed(c) => Arc::new(DexData::Owned(c.into_owned())),
1356 Loaded::Cached(e) => Arc::clone(&e.data),
1357 })
1358 }
1359
1360 pub fn get_class_dex(&self, descriptor: &str) -> Result<Option<ClassHit>> {
1373 self.get_class_dex_cancellable(descriptor, &NOT_CANCELLED)
1374 }
1375
1376 pub fn get_class_dex_cancellable(&self, descriptor: &str, cancel: &AtomicBool) -> Result<Option<ClassHit>> {
1379 if cancel.load(Ordering::Relaxed) {
1380 return Err(cancelled());
1381 }
1382 let t0 = Instant::now();
1383 let apk = Arc::new(self.open()?);
1384 let entries = Arc::new(apk.dex_entries()?);
1385 let key = Arc::new(self.apk_key(&apk));
1386 self.log(format!("[APK] scan setup={} us entries={}", t0.elapsed().as_micros(), entries.len()));
1387 if entries.is_empty() {
1388 return Ok(None);
1389 }
1390 let target: Arc<Vec<u8>> = Arc::new(asc_dex::mutf8::encode(descriptor));
1393 let next = Arc::new(AtomicUsize::new(0));
1394 let stop = Arc::new(AtomicBool::new(false));
1395 let best = Arc::new(AtomicUsize::new(usize::MAX));
1397 let ranks: Arc<Vec<usize>> = Arc::new(load_ranks(&entries));
1398 let exhaustive = apk.archive_count() > 1;
1399 let workers = self.threads().min(entries.len()).max(1);
1400 let (tx, rx) = mpsc::channel::<(usize, std::result::Result<Option<ClassHit>, EngineError>)>();
1401 let debug = self.debug;
1402 for w in 0..workers {
1403 let (apk, entries, key, target, next, stop, tx, logger, cache, stats, best, ranks) = (
1404 Arc::clone(&apk),
1405 Arc::clone(&entries),
1406 Arc::clone(&key),
1407 Arc::clone(&target),
1408 Arc::clone(&next),
1409 Arc::clone(&stop),
1410 tx.clone(),
1411 Arc::clone(&self.logger),
1412 self.cache.clone(),
1413 Arc::clone(&self.stats),
1414 Arc::clone(&best),
1415 Arc::clone(&ranks),
1416 );
1417 std::thread::spawn(move || {
1418 while !stop.load(Ordering::Relaxed) {
1419 let i = next.fetch_add(1, Ordering::Relaxed);
1420 let Some(entry) = entries.get(i) else { break };
1421 let rank = ranks[i];
1422 if !exhaustive && rank > best.load(Ordering::Relaxed) {
1424 let _ = tx.send((i, Ok(None)));
1425 continue;
1426 }
1427 let t0 = Instant::now();
1428 let cached = cache.as_deref().and_then(|c| lookup_cached(c, key.as_ref().as_ref(), entry, &stats));
1431 let (loaded, source) = match cached {
1432 Some((e, source)) => (Loaded::Cached(e), source),
1433 None => {
1434 match probe_entry(&apk, entry, &target) {
1435 Ok(Some(false)) => {
1436 if debug {
1437 logger(&format!(
1438 "[APK] [T{w:04x}] '{}' prefix-probe={} us hit=false",
1439 entry.name,
1440 t0.elapsed().as_micros()
1441 ));
1442 }
1443 let _ = tx.send((i, Ok(None)));
1444 continue;
1445 }
1446 Ok(Some(true)) | Ok(None) => {}
1447 Err(e) => {
1448 let _ = tx.send((i, Err(e)));
1449 continue;
1450 }
1451 }
1452 match load_entry(&apk, key.as_ref().as_ref(), entry, cache.as_deref(), &stats, Some(&stop)) {
1453 Ok(l) => l,
1454 Err(EngineError::Apk(asc_apk::ApkError::Cancelled)) => break,
1455 Err(e) => {
1456 let _ = tx.send((i, Err(e)));
1457 continue;
1458 }
1459 }
1460 }
1461 };
1462 let t1 = Instant::now();
1463 let mut found = None;
1464 let mut entry_err: Option<EngineError> = None;
1467 for (name, data) in loaded.into_logical(&entry.name) {
1468 if stop.load(Ordering::Relaxed) {
1469 return;
1470 }
1471 match asc_dex::quick::dex_defines_class(&data, &target) {
1472 Ok(true) => {
1473 found = Some(ClassHit { dex_name: name, dex: data, also_defined_in: Vec::new() });
1474 break;
1475 }
1476 Ok(false) => {}
1477 Err(e) => {
1478 entry_err.get_or_insert(e.into());
1479 }
1480 }
1481 }
1482 let is_hit = found.is_some();
1483 if debug {
1484 logger(&format!(
1485 "[APK] [T{w:04x}] '{}' {}={} us lookup={} us hit={}",
1486 entry.name,
1487 source.as_str(),
1488 (t1 - t0).as_micros(),
1489 t1.elapsed().as_micros(),
1490 is_hit
1491 ));
1492 }
1493 if let Some(hit) = found {
1494 best.fetch_min(rank, Ordering::Relaxed);
1495 let _ = tx.send((i, Ok(Some(hit))));
1496 continue;
1497 }
1498 let _ = tx.send((i, entry_err.map_or(Ok(None), Err)));
1499 }
1500 });
1501 }
1502 drop(tx);
1503 let mut hits: Vec<(usize, ClassHit)> = Vec::new();
1504 let mut last_err = None;
1505 let mut answered = vec![false; entries.len()];
1506 let mut seen = 0usize;
1507 loop {
1508 let (i, msg) = match rx.recv_timeout(CANCEL_POLL) {
1509 Ok(msg) => msg,
1510 Err(mpsc::RecvTimeoutError::Timeout) if cancel.load(Ordering::Relaxed) => {
1511 stop.store(true, Ordering::Relaxed);
1512 return Err(cancelled());
1513 }
1514 Err(mpsc::RecvTimeoutError::Timeout) => continue,
1515 Err(mpsc::RecvTimeoutError::Disconnected) => break,
1516 };
1517 seen += 1;
1518 answered[i] = true;
1519 match msg {
1520 Ok(Some(h)) => hits.push((ranks[i], h)),
1521 Ok(None) => {}
1522 Err(e) => last_err = Some(e),
1523 }
1524 if seen == entries.len() {
1525 break;
1526 }
1527 let best = hits.iter().map(|(rank, _)| *rank).min();
1529 if let Some(best) = best.filter(|_| !exhaustive)
1530 && ranks.iter().zip(&answered).all(|(&rank, &done)| done || rank >= best)
1531 {
1532 break;
1533 }
1534 }
1535 stop.store(true, Ordering::Relaxed);
1536 self.log(format!("[APK] getclass total={} us", t0.elapsed().as_micros()));
1537 hits.sort_by_key(|(rank, _)| *rank);
1538 let mut hits = hits.into_iter().map(|(_, h)| h);
1539 match (hits.next(), last_err) {
1540 (Some(mut h), _) => {
1541 let archive = archive_of(&h.dex_name).map(str::to_string);
1542 h.also_defined_in = hits.map(|o| o.dex_name).filter(|n| archive_of(n).map(str::to_string) != archive).collect();
1543 Ok(Some(h))
1544 }
1545 (None, Some(e)) => Err(e),
1546 (None, None) => Ok(None),
1547 }
1548 }
1549
1550 pub fn get_class(&self, descriptor: &str) -> Result<ClassSource> {
1552 self.get_class_with(descriptor, SmaliOptions::default())
1553 }
1554
1555 pub fn get_class_with(&self, descriptor: &str, opts: SmaliOptions) -> Result<ClassSource> {
1556 self.get_class_method(descriptor, opts, None)
1557 }
1558
1559 pub fn get_class_method(&self, descriptor: &str, opts: SmaliOptions, method: Option<&str>) -> Result<ClassSource> {
1564 self.get_class_method_cancellable(descriptor, opts, method, &NOT_CANCELLED)
1565 }
1566
1567 pub fn get_class_method_cancellable(
1570 &self,
1571 descriptor: &str,
1572 opts: SmaliOptions,
1573 method: Option<&str>,
1574 cancel: &AtomicBool,
1575 ) -> Result<ClassSource> {
1576 if let Some(m) = method {
1577 asc_smali::normalize_method_signature(m)?;
1578 }
1579 let t0 = Instant::now();
1580 let hit = self
1581 .get_class_dex_cancellable(descriptor, cancel)?
1582 .ok_or_else(|| EngineError::ClassNotFound { class: descriptor.to_string(), hint: self.class_not_found_hint(descriptor) })?;
1583 let scan_us = t0.elapsed().as_micros();
1584 let (rebuilt, source, warning) = match decompile_dex_class_method(&hit.dex, descriptor, opts, method) {
1585 Ok((r, s)) => (Some(r), s, None),
1586 Err(EngineError::Rebuild(e)) if e.is_input_limitation() => {
1591 let dex = Dex::with_name(&hit.dex, &hit.dex_name)?;
1592 let source = asc_smali::disassemble_class_method(&dex, descriptor, opts, method)?;
1593 let warning = format!("class not rebuilt ({e}); disassembled from the original DEX instead");
1594 self.log(format!("[DEBUG] {warning}"));
1595 (None, source, Some(warning))
1596 }
1597 Err(e) => return Err(e),
1598 };
1599 if self.debug
1600 && let Some(r) = &rebuilt
1601 {
1602 let t = r.timings;
1603 self.log(format!(
1604 "[DEBUG] Hollower Time: {} us\n[DEBUG] Collect Time: {} us\n[DEBUG] Remapper Time: {} us\n[DEBUG] Builder Time: {} us\n[DEBUG] Total Extraction Time: {} us",
1605 t.hollow_us,
1606 t.collect_us,
1607 t.map_us,
1608 t.build_us,
1609 t.total_us()
1610 ));
1611 }
1612 Ok(ClassSource { dex_name: hit.dex_name, source, rebuilt, warning, scan_us, also_defined_in: hit.also_defined_in })
1613 }
1614
1615 pub fn find_refs(&self, query: &Query, mut sink: impl FnMut(RefBatch)) -> Result<RefSummary> {
1618 self.find_refs_with(query, true, &AtomicBool::new(false), &mut sink)
1619 }
1620
1621 pub fn find_refs_with(
1624 &self,
1625 query: &Query,
1626 aggregate: bool,
1627 cancel: &AtomicBool,
1628 sink: &mut dyn FnMut(RefBatch),
1629 ) -> Result<RefSummary> {
1630 self.find_refs_opts(query, FindOptions { aggregate, context: RefContext::None }, cancel, sink)
1631 }
1632
1633 pub fn find_refs_opts(
1635 &self,
1636 query: &Query,
1637 opts: FindOptions,
1638 cancel: &AtomicBool,
1639 sink: &mut dyn FnMut(RefBatch),
1640 ) -> Result<RefSummary> {
1641 self.find_refs_shaped(query, opts, Shape::Results, cancel, sink)
1642 }
1643
1644 pub fn find_refs_text(&self, query: &Query, cancel: &AtomicBool, sink: &mut dyn FnMut(RefBatch)) -> Result<RefSummary> {
1649 self.find_refs_shaped(query, FindOptions::default(), Shape::Text, cancel, sink)
1650 }
1651
1652 fn find_refs_shaped(
1653 &self,
1654 query: &Query,
1655 opts: FindOptions,
1656 shape: Shape,
1657 cancel: &AtomicBool,
1658 sink: &mut dyn FnMut(RefBatch),
1659 ) -> Result<RefSummary> {
1660 let t0 = Instant::now();
1661 compose::check_patterns(query)?;
1662 if let Some(engine) = self.transient(compose::needs_program(query))? {
1663 return engine.find_refs_shaped(query, opts, shape, cancel, sink);
1664 }
1665 let apk = self.open()?;
1666 let mut entries = apk.dex_entries()?;
1667 entries.reverse();
1670 let key = self.apk_key(&apk);
1671 let mut summary = RefSummary { entries: entries.len(), ..Default::default() };
1672 if entries.is_empty() {
1673 return Ok(summary);
1674 }
1675 let disk = self.cache.as_ref().and_then(|c| c.disk.as_ref()).filter(|d| d.uses(Tier::Rendered));
1677 let rendered = disk.zip(key.as_ref()).map(|(d, k)| (d, k, tiers::findrefs_key(query, opts, shape)));
1678 if let Some(recorded) = rendered.as_ref().and_then(|(d, k, q)| d.get_rendered_view(k, q)).and_then(tiers::decode_batches) {
1679 for batch in recorded.batches {
1680 summary.total_results += batch.row_count();
1681 sink(batch);
1682 }
1683 summary.notes = recorded.notes;
1684 summary.total_us = t0.elapsed().as_micros();
1685 self.log(format!("[APK] for_each_findrefs total={} us count={} source=result", summary.total_us, entries.len()));
1686 return Ok(summary);
1687 }
1688 let resolved = self.resolve(std::slice::from_ref(query), cancel)?;
1691 summary.notes = resolved.notes.into_iter().next().unwrap_or_default();
1692 let _query = self.prebuild.query();
1693 let compiled = CompiledQuery::new(resolved.queries.as_ref().map_or(query, |r| &r[0]))?;
1694 let mut recorder = rendered.as_ref().map(|(d, _, _)| tiers::ResultRecorder::new(tiers::result_limit(d, shape)));
1695 let next = AtomicUsize::new(0);
1696 let (tx, rx) = mpsc::channel::<RefBatch>();
1697 let workers = self.threads().min(entries.len()).max(1);
1698 let cache = self.cache.as_deref();
1699 let stats = &self.stats;
1700 let probe = skip::Probe::new(vec![&compiled]);
1701 let (compiled, probe) = (&compiled, &probe);
1702 std::thread::scope(|s| {
1703 for _ in 0..workers {
1704 let tx = tx.clone();
1705 let (apk, entries, next, key) = (&apk, &entries, &next, key.as_ref());
1706 s.spawn(move || {
1707 loop {
1708 if cancel.load(Ordering::Relaxed) {
1709 break;
1710 }
1711 let i = next.fetch_add(1, Ordering::Relaxed);
1712 let Some(entry) = entries.get(i) else { break };
1713 let batch = search_entry(apk, key, entry, compiled, opts, shape, Some(probe), cancel, cache, stats);
1714 if tx.send(batch).is_err() {
1715 break;
1716 }
1717 }
1718 });
1719 }
1720 drop(tx);
1721 for batch in rx {
1722 if self.debug {
1723 self.log(format!(
1724 "[APK] '{}' {}={} us process={} us results={}{}",
1725 batch.entry_name,
1726 batch.source,
1727 batch.inflate_us,
1728 batch.search_us,
1729 batch.row_count(),
1730 batch.error.as_ref().map(|e| format!(" error={e}")).unwrap_or_default()
1731 ));
1732 }
1733 summary.total_results += batch.row_count();
1734 if let Some(r) = recorder.as_mut() {
1735 r.record(&batch);
1736 }
1737 sink(batch);
1738 }
1739 });
1740 if let (Some((_, k, q)), Some(payload)) = (rendered, recorder.and_then(|r| r.finish(&summary.notes))) {
1741 if !cancel.load(Ordering::Relaxed) {
1743 let job = WriteJob { key: k.clone(), apk: Arc::from(apk.path()), kind: Job::Rendered { query: q, payload } };
1744 self.cache.as_ref().expect("rendered implies a cache").put_async(job);
1745 }
1746 }
1747 summary.total_us = t0.elapsed().as_micros();
1748 self.log(format!("[APK] for_each_findrefs total={} us count={} workers={}", summary.total_us, entries.len(), workers));
1749 Ok(summary)
1750 }
1751
1752 pub fn fill_context(&self, rows: &mut [RefResult], ctx: RefContext) -> Result<Vec<String>> {
1756 if ctx == RefContext::None || rows.is_empty() {
1757 return Ok(Vec::new());
1758 }
1759 let _query = self.prebuild.query();
1760 let wanted: HashSet<String> = rows.iter().map(|r| entry_of(&r.dex_name).to_string()).collect();
1761 let (views, errors) = self.load_views(Some(&wanted), &NOT_CANCELLED)?;
1762 let mut errors = errors;
1763 for v in &views {
1764 let r = Dex::with_name(&v.data, &v.name).map_err(EngineError::from).and_then(|dex| {
1765 attach_context(&FindRefs::with_state(&dex, &v.state), rows.iter_mut().filter(|r| r.dex_name == v.name), ctx)
1766 });
1767 if let Err(e) = r {
1768 errors.push(format!("{}: {e}", v.name));
1769 }
1770 }
1771 self.recharge_views(&views);
1772 Ok(errors)
1773 }
1774
1775 fn load_views(&self, only: Option<&HashSet<String>>, cancel: &AtomicBool) -> Result<(Vec<DexView>, Vec<String>)> {
1781 let apk = self.open()?;
1782 let mut entries = apk.dex_entries()?;
1783 if let Some(only) = only {
1784 entries.retain(|e| only.contains(&e.name));
1785 }
1786 let key = self.apk_key(&apk);
1787 let next = AtomicUsize::new(0);
1788 let workers = self.threads().min(entries.len()).max(1);
1789 let cache = self.cache.as_deref();
1790 let l2 = cache.and_then(|c| c.disk.as_deref()).filter(|d| d.uses(Tier::Derived)).zip(key.as_ref());
1791 let mut slots: Vec<Option<std::result::Result<Vec<DexView>, String>>> = (0..entries.len()).map(|_| None).collect();
1792 let done = Mutex::new(&mut slots);
1793 std::thread::scope(|s| {
1794 for _ in 0..workers {
1795 s.spawn(|| {
1796 loop {
1797 if cancel.load(Ordering::Relaxed) {
1798 break;
1799 }
1800 let i = next.fetch_add(1, Ordering::Relaxed);
1801 let Some(entry) = entries.get(i) else { break };
1802 let r = load_entry(&apk, key.as_ref(), entry, cache, &self.stats, Some(cancel))
1803 .map(|(loaded, _)| match loaded {
1804 Loaded::Cached(e) => e
1805 .logical()
1806 .iter()
1807 .enumerate()
1808 .map(|(idx, l)| {
1809 if let (Some((disk, k)), Ok(dex)) = (l2, Dex::with_name(&l.data, &l.name))
1810 && tiers::load_derived(disk, k, l, &dex)
1811 {
1812 self.stats.derived_hits.fetch_add(1, Ordering::Relaxed);
1813 }
1814 DexView {
1815 name: l.name.clone(),
1816 data: Arc::clone(&l.data),
1817 state: Arc::clone(&l.state),
1818 entry: Some(Arc::clone(&e)),
1819 idx,
1820 mask: l.state.table_mask(),
1821 loaded: Instant::now(),
1822 }
1823 })
1824 .collect(),
1825 borrowed => borrowed
1826 .into_logical(&entry.name)
1827 .map(|(name, data)| DexView {
1828 name,
1829 data,
1830 state: Arc::new(SearchState::new()),
1831 entry: None,
1832 idx: 0,
1833 mask: 0,
1834 loaded: Instant::now(),
1835 })
1836 .collect(),
1837 })
1838 .map_err(|e| format!("{}: {e}", entry.name));
1839 done.lock().unwrap()[i] = Some(r);
1840 }
1841 });
1842 }
1843 });
1844 if cancel.load(Ordering::Relaxed) {
1845 return Err(cancelled());
1846 }
1847 let mut views = Vec::new();
1848 let mut errors = Vec::new();
1849 for slot in slots.into_iter().flatten() {
1850 match slot {
1851 Ok(mut v) => views.append(&mut v),
1852 Err(e) => errors.push(e),
1853 }
1854 }
1855 Ok((views, errors))
1856 }
1857
1858 fn recharge_views(&self, views: &[DexView]) {
1861 let Some(cache) = self.cache.as_deref() else { return };
1862 let Some(key) = self.key_cache.lock().unwrap().as_ref().map(|k| k.2.clone()) else { return };
1863 let disk = cache.disk.as_deref().filter(|d| d.uses(Tier::Derived));
1864 let mut seen = HashSet::new();
1865 for v in views {
1866 let Some(e) = &v.entry else { continue };
1867 if let Some(job) = disk.and_then(|d| tiers::derived_job(d, e, v.idx, v.mask, v.loaded.elapsed())) {
1868 cache.put_async(WriteJob { key: key.clone(), apk: Arc::from(self.apk_path.as_path()), kind: Job::Derived(job) });
1869 }
1870 if seen.insert(Arc::as_ptr(e)) {
1871 recharge(Some(cache), Some(&key), e);
1872 }
1873 }
1874 }
1875
1876 pub fn manifest_xml(&self) -> Result<String> {
1878 let apk = self.open()?;
1879 let data = apk.read("AndroidManifest.xml")?;
1880 Ok(asc_axml::decode(&data)?)
1881 }
1882}
1883
1884impl Drop for Engine {
1885 fn drop(&mut self) {
1886 self.prebuild.cancel();
1887 if !self.shared_cache {
1889 self.close_cache_writes();
1890 }
1891 }
1892}
1893
1894struct DexView {
1896 name: String,
1897 data: DexBytes,
1898 state: Arc<SearchState>,
1899 entry: Option<Arc<CachedEntry>>,
1901 idx: usize,
1903 mask: u32,
1906 loaded: Instant,
1907}
1908
1909fn entry_of(dex_name: &str) -> &str {
1912 dex_name.split('!').next().unwrap_or(dex_name)
1913}
1914
1915fn load_ranks(entries: &[Entry]) -> Vec<usize> {
1920 let key = |e: &Entry| {
1921 let inner = e.name.rsplit('/').next().unwrap_or(&e.name);
1922 let number = match inner.strip_prefix("classes").and_then(|rest| rest.strip_suffix(".dex")) {
1923 Some("") => 1,
1924 Some(n) if !n.starts_with('0') => n.parse().unwrap_or(u64::MAX),
1925 _ => u64::MAX,
1926 };
1927 (e.archive, number, inner.to_string())
1928 };
1929 let mut order: Vec<usize> = (0..entries.len()).collect();
1930 order.sort_by_cached_key(|&i| key(&entries[i]));
1931 let mut ranks = vec![0; entries.len()];
1932 for (rank, i) in order.into_iter().enumerate() {
1933 ranks[i] = rank;
1934 }
1935 ranks
1936}
1937
1938fn lookup_cached(cache: &EngineCache, key: Option<&ApkKey>, entry: &Entry, stats: &Stats) -> Option<(Arc<CachedEntry>, Source)> {
1939 let key = key?;
1940 if let Some(mem) = &cache.memory
1941 && let Some(e) = mem.lock().unwrap().get(key, &entry.name)
1942 {
1943 stats.memory_hits.fetch_add(1, Ordering::Relaxed);
1944 return Some((e, Source::Memory));
1945 }
1946 if entry.method == asc_apk::METHOD_DEFLATE
1947 && let Some(disk) = &cache.disk
1948 && let Some(data) = disk.get(key, &entry.name, entry.uncomp_size)
1949 {
1950 stats.disk_hits.fetch_add(1, Ordering::Relaxed);
1951 let e = Arc::new(CachedEntry::new(&entry.name, Arc::new(data)));
1952 if let Some(mem) = &cache.memory {
1953 let size = e.charged_bytes();
1956 mem.lock().unwrap().insert(key, &entry.name, Arc::clone(&e), size);
1957 cache.charged();
1958 }
1959 return Some((e, Source::Disk));
1960 }
1961 None
1962}
1963
1964fn load_entry<'a>(
1966 apk: &'a Apk,
1967 key: Option<&ApkKey>,
1968 entry: &Entry,
1969 cache: Option<&EngineCache>,
1970 stats: &Stats,
1971 cancel: Option<&AtomicBool>,
1972) -> Result<(Loaded<'a>, Source)> {
1973 if let (Some(c), Some(k)) = (cache, key) {
1974 if let Some((e, source)) = lookup_cached(c, Some(k), entry, stats) {
1975 return Ok((Loaded::Cached(e), source));
1976 }
1977 let _guard = c.begin_inflate(format!("{}/{}", k.as_str(), entry.name));
1979 if let Some((e, source)) = lookup_cached(c, Some(k), entry, stats) {
1980 return Ok((Loaded::Cached(e), source));
1981 }
1982 let bytes = apk.read_entry_cancellable(entry, cancel)?;
1983 let (data, keep_in_memory, source) = match bytes {
1984 Cow::Borrowed(b) => {
1989 if c.memory.is_none() && !c.disk.as_ref().is_some_and(|d| d.uses(Tier::Derived)) {
1990 return Ok((Loaded::Borrowed(Cow::Borrowed(b)), Source::Stored));
1991 }
1992 let offset = b.as_ptr() as usize - apk.archive_data(entry).as_ptr() as usize;
1993 let data =
1994 DexData::map_region(apk.archive_path(entry), offset as u64, b.len()).unwrap_or_else(|_| DexData::Owned(b.to_vec()));
1995 (data, c.memory.is_some(), Source::Stored)
1996 }
1997 Cow::Owned(v) => {
1998 stats.inflates.fetch_add(1, Ordering::Relaxed);
1999 (DexData::Owned(v), c.memory.is_some(), Source::Inflate)
2000 }
2001 };
2002 let e = Arc::new(CachedEntry::new(&entry.name, Arc::new(data)));
2003 let mut remap = None;
2004 if keep_in_memory && let Some(mem) = &c.memory {
2005 let size = e.charged_bytes();
2007 mem.lock().unwrap().insert(k, &entry.name, Arc::clone(&e), size);
2008 c.charged();
2009 remap = Some(Arc::downgrade(&e));
2010 }
2011 if source == Source::Inflate && c.disk.as_ref().is_some_and(|d| d.admits_dex(e.data.len() as u64)) {
2013 let job = WriteJob {
2014 key: k.clone(),
2015 apk: Arc::from(apk.path()),
2016 kind: Job::Dex { entry: entry.clone(), limits: apk.limits(), data: Arc::clone(&e.data), remap },
2017 };
2018 c.put_async(job);
2019 }
2020 return Ok((Loaded::Cached(e), source));
2021 }
2022 let bytes = apk.read_entry_cancellable(entry, cancel)?;
2023 let source = if matches!(bytes, Cow::Owned(_)) {
2024 stats.inflates.fetch_add(1, Ordering::Relaxed);
2025 Source::Inflate
2026 } else {
2027 Source::Stored
2028 };
2029 Ok((Loaded::Borrowed(bytes), source))
2030}
2031
2032fn probe_entry(apk: &Apk, entry: &Entry, target: &[u8]) -> Result<Option<bool>> {
2035 const MIN_GAIN: u64 = 256 * 1024;
2037 const SLACK: usize = 64 * 1024;
2039 const WORTH_BELOW: f64 = 0.70;
2041 if entry.method != asc_apk::METHOD_DEFLATE || entry.uncomp_size <= MIN_GAIN {
2042 return Ok(None);
2043 }
2044 let size = entry.uncomp_size as usize;
2045 let prefix = apk.inflate_until(entry, |out| {
2046 if out.len() >= 8 && &out[..8] == asc_dex::container::DEX041_MAGIC {
2047 return asc_apk::PrefixStep::Abort;
2048 }
2049 match asc_dex::quick::probe_needed_len(out) {
2050 Some(needed) if (needed as f64) < size as f64 * WORTH_BELOW => asc_apk::PrefixStep::Continue(needed + SLACK),
2051 Some(_) => asc_apk::PrefixStep::Abort,
2052 None => asc_apk::PrefixStep::Continue(0),
2053 }
2054 })?;
2055 let bytes = match prefix {
2056 asc_apk::Prefix::Complete(data) | asc_apk::Prefix::Partial(data) => data,
2057 asc_apk::Prefix::Aborted(_) => return Ok(None),
2058 };
2059 Ok(asc_dex::quick::dex_defines_class(&bytes, target).ok())
2061}
2062
2063#[allow(clippy::too_many_arguments)]
2064fn search_entry(
2065 apk: &Apk,
2066 key: Option<&ApkKey>,
2067 entry: &Entry,
2068 query: &CompiledQuery,
2069 opts: FindOptions,
2070 shape: Shape,
2071 probe: Option<&skip::Probe<'_>>,
2072 cancel: &AtomicBool,
2073 cache: Option<&EngineCache>,
2074 stats: &Stats,
2075) -> RefBatch {
2076 let run = |fr: &FindRefs<'_, '_, '_>| {
2077 Ok(vec![match shape {
2078 Shape::Results => Rows::Results(rows_compiled(fr, query, opts)?),
2079 Shape::Text => Rows::Text(compact::text_rows(fr, query)?),
2080 }])
2081 };
2082 search_entry_with(apk, key, entry, 1, &run, probe, cancel, cache, stats).pop().expect("one batch per query")
2083}
2084
2085pub(crate) type RunRows<'q> = dyn Fn(&FindRefs<'_, '_, '_>) -> Result<Vec<Rows>> + Sync + 'q;
2087
2088pub(crate) enum Rows {
2090 Results(Vec<RefResult>),
2091 Text(TextRows),
2092}
2093
2094#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2097pub(crate) enum Shape {
2098 Results,
2099 Text,
2100}
2101
2102#[allow(clippy::too_many_arguments)]
2107pub(crate) fn search_entry_with(
2108 apk: &Apk,
2109 key: Option<&ApkKey>,
2110 entry: &Entry,
2111 n: usize,
2112 run: &RunRows<'_>,
2113 probe: Option<&skip::Probe<'_>>,
2114 cancel: &AtomicBool,
2115 cache: Option<&EngineCache>,
2116 stats: &Stats,
2117) -> Vec<RefBatch> {
2118 let t0 = Instant::now();
2119 let empty = RefBatch {
2120 entry_name: entry.name.clone(),
2121 results: Vec::new(),
2122 text: None,
2123 inflate_us: 0,
2124 search_us: 0,
2125 source: "inflate",
2126 error: None,
2127 };
2128 let mut batches = vec![empty; n];
2129 if probe.is_some_and(|p| p.skips(apk, entry, cache, stats)) {
2130 let inflate_us = t0.elapsed().as_micros();
2131 for b in &mut batches {
2132 (b.source, b.inflate_us) = (Source::Skipped.as_str(), inflate_us);
2133 }
2134 return batches;
2135 }
2136 let (loaded, source) = match load_entry(apk, key, entry, cache, stats, Some(cancel)) {
2137 Ok(l) => l,
2138 Err(e) => {
2139 let msg = e.to_string();
2140 for b in &mut batches {
2141 b.error = Some(msg.clone());
2142 }
2143 return batches;
2144 }
2145 };
2146 let t1 = Instant::now();
2147 let mut add = |name: &str, r: Result<Vec<Rows>>| match r {
2148 Ok(rows) => {
2149 for (b, rows) in batches.iter_mut().zip(rows) {
2150 match rows {
2151 Rows::Results(mut rows) => b.results.append(&mut rows),
2152 Rows::Text(t) => match &mut b.text {
2153 Some(text) => text.append(&t),
2154 None => b.text = Some(t),
2155 },
2156 }
2157 }
2158 }
2159 Err(e) => {
2160 let msg = e.to_string();
2161 for b in batches.iter_mut() {
2162 b.note_error(name, &msg);
2163 }
2164 }
2165 };
2166 match &loaded {
2167 Loaded::Cached(e) => {
2168 let l2 = cache.and_then(|c| c.disk.as_deref().filter(|d| d.uses(Tier::Derived)).map(|d| (c, d))).zip(key);
2169 for (i, l) in e.logical().iter().enumerate() {
2170 let r = Dex::with_name(&l.data, &l.name).map_err(EngineError::from).and_then(|dex| {
2171 if let Some(((_, disk), k)) = l2
2172 && tiers::load_derived(disk, k, l, &dex)
2173 {
2174 stats.derived_hits.fetch_add(1, Ordering::Relaxed);
2175 }
2176 let (mask, t) = (l.state.table_mask(), Instant::now());
2177 let rows = run(&FindRefs::with_state(&dex, &l.state));
2178 if let Some(((c, disk), k)) = l2
2179 && let Some(job) = tiers::derived_job(disk, e, i, mask, t.elapsed())
2180 {
2181 c.put_async(WriteJob { key: k.clone(), apk: Arc::from(apk.path()), kind: Job::Derived(job) });
2182 }
2183 rows
2184 });
2185 add(&l.name, r);
2186 }
2187 recharge(cache, key, e);
2188 }
2189 Loaded::Borrowed(_) => {
2190 for (name, buf) in logical_dex_iter(&entry.name, loaded.bytes()) {
2191 let r = Dex::with_name(&buf, &name).map_err(EngineError::from).and_then(|dex| run(&FindRefs::new(&dex)));
2192 add(&name, r);
2193 }
2194 }
2195 }
2196 let (inflate_us, search_us) = ((t1 - t0).as_micros(), t1.elapsed().as_micros());
2197 for b in &mut batches {
2198 (b.source, b.inflate_us, b.search_us) = (source.as_str(), inflate_us, search_us);
2199 }
2200 batches
2201}
2202
2203fn recharge(cache: Option<&EngineCache>, key: Option<&ApkKey>, e: &Arc<CachedEntry>) {
2206 let (Some(mem), Some(key)) = (cache.and_then(|c| c.memory.as_ref()), key) else { return };
2207 let size = e.charged_bytes();
2208 {
2209 let mut mem = mem.lock().unwrap();
2210 if !mem.charged_bytes(key, &e.name).is_some_and(|old| old != size) {
2211 return;
2212 }
2213 mem.resize(key, &e.name, e, size);
2214 }
2215 if let Some(c) = cache {
2216 c.charged();
2217 }
2218}
2219
2220fn hit_matched(dex: &Dex<'_>, kind: RefKind, idx: u32, note: Option<&str>, access: Option<FieldAccess>) -> Result<Matched> {
2223 let m = matched_name(dex, kind, idx)?;
2224 if let (RefKind::Field, Some(access)) = (kind, access) {
2225 return Ok(Matched::Field { symbol: m.as_str().to_string(), access });
2226 }
2227 Ok(match note {
2228 Some(note) => Matched::Symbol(format!("{} {note}", m.as_str())),
2229 None => m,
2230 })
2231}
2232
2233fn matched_name(dex: &Dex<'_>, kind: RefKind, idx: u32) -> Result<Matched> {
2234 Ok(match kind {
2235 RefKind::String => Matched::String(dex.string(idx)?),
2236 RefKind::Type => Matched::Symbol(dex.type_descriptor(idx)?),
2237 RefKind::Method => Matched::Symbol(asc_smali::method_signature(dex, idx)?),
2238 RefKind::Field => Matched::Symbol(asc_smali::field_signature(dex, idx)?),
2239 })
2240}
2241
2242impl RefResult {
2243 fn method_row(dex: &Dex<'_>, m: u32, matched: Vec<Matched>, sites: Vec<RefSite>) -> Result<RefResult> {
2245 Ok(RefResult {
2246 dex_name: dex.name().to_string(),
2247 method_idx: m,
2248 method: dex.method_display(m)?,
2249 signature: asc_smali::method_signature(dex, m)?,
2250 matched,
2251 relation: None,
2252 sites,
2253 context: None,
2254 })
2255 }
2256}
2257
2258pub fn find_refs_in_dex(dex_name: &str, buf: &[u8], query: &Query, aggregate: bool) -> Result<Vec<RefResult>> {
2260 find_refs_in_dex_opts(dex_name, buf, query, FindOptions { aggregate, context: RefContext::None })
2261}
2262
2263pub fn find_refs_in_dex_opts(dex_name: &str, buf: &[u8], query: &Query, opts: FindOptions) -> Result<Vec<RefResult>> {
2265 find_refs_in_dex_compiled(dex_name, buf, &CompiledQuery::new(query)?, opts)
2266}
2267
2268fn find_refs_in_dex_compiled(dex_name: &str, buf: &[u8], query: &CompiledQuery, opts: FindOptions) -> Result<Vec<RefResult>> {
2269 let dex = Dex::with_name(buf, dex_name)?;
2270 rows_compiled(&FindRefs::new(&dex), query, opts)
2271}
2272
2273fn rows_compiled(fr: &FindRefs<'_, '_, '_>, query: &CompiledQuery, opts: FindOptions) -> Result<Vec<RefResult>> {
2276 let dex = fr.dex();
2277 if let Some(c) = query.compound() {
2278 return rules::compound_rows(fr, c, opts);
2279 }
2280 if let Some(d) = query.defined() {
2281 let mut rows = defined_rows(dex, d)?;
2282 if opts.context != RefContext::None {
2283 attach_context(fr, &mut rows, opts.context)?;
2284 }
2285 return Ok(rows);
2286 }
2287 let Some(classes) = fr.class_relation(query)? else {
2288 let hits = fr.find_ref_compiled(query)?;
2289 let mut rows = results_from_hits(dex, query.kind(), &hits, opts.aggregate)?;
2290 if opts.context != RefContext::None {
2291 attach_context(fr, &mut rows, opts.context)?;
2292 }
2293 return Ok(rows);
2294 };
2295 let target = match query.query() {
2296 Query::Extends(t) | Query::Implements(t) => t.as_str(),
2297 _ => "",
2298 };
2299 let dex_name = dex.name().to_string();
2300 classes
2301 .into_iter()
2302 .map(|c| {
2303 let class = dex.type_descriptor(c)?;
2304 Ok(RefResult {
2305 dex_name: dex_name.clone(),
2306 method_idx: c,
2307 method: class.clone(),
2308 signature: class,
2309 matched: vec![Matched::Symbol(target.to_string())],
2310 relation: query.relation(),
2311 sites: Vec::new(),
2312 context: None,
2313 })
2314 })
2315 .collect()
2316}
2317
2318pub(crate) fn defined_rows(dex: &Dex<'_>, d: &asc_findrefs::Defined) -> Result<Vec<RefResult>> {
2321 let label = d.label();
2322 defs::defined_methods(dex, d)?
2323 .into_iter()
2324 .map(|m| RefResult::method_row(dex, m, vec![Matched::Symbol(label.clone())], Vec::new()))
2325 .collect()
2326}
2327
2328pub fn results_from_hits(dex: &Dex<'_>, kind: RefKind, hits: &[Hit], aggregate: bool) -> Result<Vec<RefResult>> {
2330 if !aggregate {
2331 let mut out = Vec::new();
2332 for hit in hits {
2333 let Some(methods) = &hit.methods else { continue };
2334 let matched = hit_matched(dex, kind, hit.matched_idx, hit.note.as_deref(), hit.field_access)?;
2335 for &m in methods.ids() {
2336 let site = RefSite { offset: hit.offset, matched: 0, field_access: hit.field_access, pc: None, line: None };
2337 out.push(RefResult::method_row(dex, m, vec![matched.clone()], vec![site])?);
2338 }
2339 }
2340 return Ok(out);
2341 }
2342 type Key<'h> = (u32, Option<&'h str>, Option<FieldAccess>);
2344 type Group<'h> = (std::collections::BTreeSet<Key<'h>>, Vec<(u32, Key<'h>)>);
2345 let mut grouped: BTreeMap<u32, Group<'_>> = BTreeMap::new();
2346 for hit in hits {
2347 let Some(methods) = &hit.methods else { continue };
2348 let key = (hit.matched_idx, hit.note.as_deref(), hit.field_access);
2349 for &m in methods.ids() {
2350 let g = grouped.entry(m).or_default();
2351 g.0.insert(key);
2352 g.1.push((hit.offset, key));
2353 }
2354 }
2355 let mut out = Vec::with_capacity(grouped.len());
2356 for (m, (idxs, offsets)) in grouped {
2357 let idxs: Vec<Key<'_>> = idxs.into_iter().collect();
2358 let matched = idxs.iter().map(|&(i, note, access)| hit_matched(dex, kind, i, note, access)).collect::<Result<Vec<_>>>()?;
2359 let sites = offsets
2360 .into_iter()
2361 .map(|(offset, idx)| RefSite {
2362 offset,
2363 matched: idxs.binary_search(&idx).unwrap_or(0),
2364 field_access: idx.2,
2365 pc: None,
2366 line: None,
2367 })
2368 .collect();
2369 out.push(RefResult::method_row(dex, m, matched, sites)?);
2370 }
2371 Ok(out)
2372}
2373
2374fn attach_context<'r>(fr: &FindRefs<'_, '_, '_>, rows: impl IntoIterator<Item = &'r mut RefResult>, ctx: RefContext) -> Result<()> {
2377 let dex = fr.dex();
2378 let bounds = fr.insn_locator()?.method_bounds();
2379 for row in rows {
2380 let start = bounds.get(row.method_idx as usize).copied().unwrap_or(u32::MAX);
2381 let listing = match asc_smali::disassemble_method(dex, row.method_idx, SmaliOptions::default()) {
2382 Ok(l) => l,
2383 Err(e) => {
2384 row.context = Some(format!(" # context unavailable: {e}\n"));
2385 continue;
2386 }
2387 };
2388 for site in &mut row.sites {
2389 if let Some(pc) = site.offset.checked_sub(start).filter(|_| start != u32::MAX).map(|b| b / 2) {
2390 site.pc = Some(pc);
2391 site.line = listing.source_line(pc);
2392 }
2393 }
2394 row.context = Some(render_context(&listing, &row.sites, ctx));
2395 }
2396 Ok(())
2397}
2398
2399const CONTEXT_MAX_LINES: usize = 150;
2401const METHOD_FALLBACK_LINES: usize = 10;
2404
2405fn render_context(listing: &asc_smali::MethodListing, sites: &[RefSite], ctx: RefContext) -> String {
2408 let mut out = String::from(" @");
2409 for (i, s) in sites.iter().enumerate() {
2410 out.push_str(if i == 0 { " " } else { ", " });
2411 match (s.pc, s.line) {
2412 (Some(pc), Some(line)) => out.push_str(&format!("pc 0x{pc:x} line {line}")),
2413 (Some(pc), None) => out.push_str(&format!("pc 0x{pc:x}")),
2414 _ => out.push_str(&format!("offset 0x{:x}", s.offset)),
2415 }
2416 if let Some(access) = s.field_access {
2417 out.push_str(&format!(" {}", access.as_str()));
2418 }
2419 }
2420 out.push('\n');
2421 let lines: Vec<&str> = listing.text.lines().collect();
2422 let mut hits: Vec<usize> = sites.iter().filter_map(|s| s.pc).filter_map(|pc| listing.line_of(pc)).map(|l| l as usize).collect();
2423 hits.sort_unstable();
2424 hits.dedup();
2425 let around = |n: usize| {
2426 let mut w: Vec<(usize, usize)> = Vec::new();
2427 for &h in &hits {
2428 let (a, b) = (h.saturating_sub(n), (h + n + 1).min(lines.len()));
2429 match w.last_mut() {
2430 Some(last) if a <= last.1 => last.1 = last.1.max(b),
2431 _ => w.push((a, b)),
2432 }
2433 }
2434 w
2435 };
2436 let windows = match ctx {
2437 RefContext::None => return out,
2438 RefContext::Method if lines.len() <= CONTEXT_MAX_LINES => vec![(0, lines.len())],
2439 RefContext::Method => {
2440 out.push_str(&format!(" # method has {} lines; showing {METHOD_FALLBACK_LINES} around each reference\n", lines.len()));
2441 around(METHOD_FALLBACK_LINES)
2442 }
2443 RefContext::Lines(n) => around(n),
2444 };
2445 let mut shown = 0;
2446 for (i, &(a, b)) in windows.iter().enumerate() {
2447 if shown + (b - a) > CONTEXT_MAX_LINES {
2448 let rest: usize = windows[i..].iter().map(|w| w.1 - w.0).sum::<usize>();
2449 out.push_str(&format!(" # cut at {CONTEXT_MAX_LINES} lines ({rest} more); read the method with getclass\n"));
2450 break;
2451 }
2452 if i > 0 {
2453 out.push_str(" --\n");
2454 }
2455 for (j, line) in lines[a..b].iter().enumerate() {
2456 out.push_str(if hits.binary_search(&(a + j)).is_ok() { " > " } else { " " });
2457 out.push_str(line);
2458 out.push('\n');
2459 }
2460 shown += b - a;
2461 }
2462 out
2463}
2464
2465pub fn find_refs_lines(dex_name: &str, buf: &[u8], query: &Query, aggregate: bool) -> Result<Vec<String>> {
2467 Ok(find_refs_in_dex(dex_name, buf, query, aggregate)?.iter().map(RefResult::line).collect())
2468}
2469
2470pub fn decompile_dex_class(buf: &[u8], descriptor: &str, opts: SmaliOptions) -> Result<(Rebuilt, String)> {
2472 decompile_dex_class_method(buf, descriptor, opts, None)
2473}
2474
2475pub fn decompile_dex_class_method(buf: &[u8], descriptor: &str, opts: SmaliOptions, method: Option<&str>) -> Result<(Rebuilt, String)> {
2477 let dex = Dex::parse(buf)?;
2478 let rebuilt = asc_rebuild::rebuild_class(&dex, descriptor)?;
2479 let new_dex = Dex::parse(&rebuilt.bytes)?;
2480 let source = asc_smali::disassemble_class_method(&new_dex, descriptor, opts, method)?;
2481 Ok((rebuilt, source))
2482}
2483
2484#[cfg(test)]
2485mod tests {
2486 use super::*;
2487
2488 fn multi_dex_apk(dir: &Path, n: usize) -> (PathBuf, DiskCache, Vec<String>) {
2491 let dex = asc_fixture::make_dex();
2492 let names: Vec<String> = (1..=n).map(|i| if i == 1 { "classes.dex".into() } else { format!("classes{i}.dex") }).collect();
2493 let entries: Vec<(&str, &[u8])> = names.iter().map(|name| (name.as_str(), dex.as_slice())).collect();
2494 let apk = dir.join("multi.apk");
2495 std::fs::write(&apk, asc_fixture::make_apk(&entries, true)).unwrap();
2496 (apk, l1_cache(&dir.join("cache")), names)
2497 }
2498
2499 fn l1_cache(dir: &Path) -> DiskCache {
2500 DiskCache::open(dir).with_capacity(1 << 30).with_min_dex_bytes(0).with_tiers(&[Tier::Dex])
2501 }
2502
2503 #[test]
2504 fn a_full_write_queue_defers_l1_writes_instead_of_dropping_them() {
2505 let dir = tempfile::tempdir().unwrap();
2506 let (apk, disk, names) = multi_dex_apk(dir.path(), 8);
2507 let mut engine = Engine::new(&apk).with_threads(4).with_cache(CacheConfig { disk: Some(disk), ..CacheConfig::default() });
2508 Arc::get_mut(engine.cache.as_mut().unwrap()).unwrap().queue_cap = 1;
2510 engine.find_refs(&Query::String("token".into()), |batch| assert!(batch.error.is_none())).unwrap();
2511 engine.flush_cache_writes();
2512 let cache = engine.cache.as_deref().unwrap();
2513 assert_eq!(cache.pending_bytes.load(Ordering::Relaxed), 0);
2514 let key = engine.apk_key(&Apk::open(&apk).unwrap()).unwrap();
2515 let len = asc_fixture::make_dex().len() as u64;
2516 for name in &names {
2517 assert!(cache.disk.as_ref().unwrap().get(&key, name, len).is_some(), "{name} missing from L1");
2518 }
2519 }
2520
2521 #[test]
2522 fn the_write_queue_stays_within_the_memory_budget() {
2523 let dir = tempfile::tempdir().unwrap();
2524 let disk = || Some(DiskCache::open(dir.path().join("cache")));
2525 let cap = |engine: &Engine| engine.cache.as_deref().unwrap().queue_cap();
2526 assert_eq!(cap(&Engine::new("x.apk").with_cache(CacheConfig { disk: disk(), ..CacheConfig::default() })), CacheConfig::WRITE_QUEUE);
2527 let small = Engine::new("x.apk").with_cache(CacheConfig { memory_bytes: 64 << 20, disk: disk(), ..CacheConfig::default() });
2528 assert_eq!(cap(&small), 64 << 20);
2529 let large = Engine::new("x.apk").with_cache(CacheConfig { memory_bytes: 4 << 30, disk: disk(), ..CacheConfig::default() });
2530 assert_eq!(cap(&large), CacheConfig::WRITE_QUEUE);
2531 let set = Engine::new("x.apk").with_cache(CacheConfig {
2532 memory_bytes: 4 << 30,
2533 disk: disk(),
2534 write_queue_bytes: 1 << 20,
2535 ..CacheConfig::default()
2536 });
2537 assert_eq!(cap(&set), 1 << 20);
2538 assert_eq!((CacheConfig::auto_writers(4), CacheConfig::auto_writers(16), CacheConfig::auto_writers(64)), (1, 2, 4));
2539 }
2540
2541 #[test]
2542 fn a_deferred_write_inflates_its_entry_again_unless_the_apk_changed() {
2543 let dir = tempfile::tempdir().unwrap();
2544 let (apk_path, disk, names) = multi_dex_apk(dir.path(), 2);
2545 let apk = Apk::open(&apk_path).unwrap();
2546 let key = ApkKey::for_apk(&apk).unwrap();
2547 let entries = apk.dex_entries().unwrap();
2548 let limits = apk.limits();
2549 let job = |entry: &Entry| WriteJob {
2550 key: key.clone(),
2551 apk: Arc::from(apk_path.as_path()),
2552 kind: Job::Dex { entry: entry.clone(), limits, data: Arc::new(DexData::Owned(vec![0; 64])), remap: None },
2553 };
2554 let deferred = job(&entries[0]).defer().unwrap();
2556 assert!(matches!(deferred.kind, Job::Reinflate { .. }));
2557 assert_eq!(deferred.held_bytes(), 0);
2558 run_job(&disk, None, deferred, &mut WriterState::default());
2559 let written = disk.get(&key, &entries[0].name, entries[0].uncomp_size).expect("inflated again and written");
2560 assert_eq!(&*written, &*apk.read_entry(&entries[0]).unwrap());
2561 drop(apk);
2563 let replacement = dir.path().join("replacement.apk");
2564 std::fs::write(&replacement, asc_fixture::make_apk(&[(names[0].as_str(), &asc_fixture::make_dex()[..])], false)).unwrap();
2565 std::fs::rename(&replacement, &apk_path).unwrap();
2566 let other = l1_cache(&dir.path().join("other"));
2567 run_job(&other, None, job(&entries[1]).defer().unwrap(), &mut WriterState::default());
2568 assert!(other.get(&key, &entries[1].name, entries[1].uncomp_size).is_none());
2569 }
2570}