1use crate::hierarchy::{ClassGraph, Lineage};
16use crate::{DexView, Engine, EngineError, NOT_CANCELLED, Result, cancelled, names};
17use asc_dex::Dex;
18use asc_findrefs::{FindRefs, Query};
19use std::collections::{BTreeSet, HashMap, HashSet};
20use std::sync::Mutex;
21use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Direction {
26 Callers,
28 Callees,
30}
31
32impl Direction {
33 pub fn as_str(self) -> &'static str {
35 match self {
36 Direction::Callers => "callers",
37 Direction::Callees => "callees",
38 }
39 }
40
41 fn arrow(self) -> &'static str {
42 match self {
43 Direction::Callers => "<-",
44 Direction::Callees => "->",
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub enum CallRoots {
53 Method {
55 class: String,
57 method: String,
59 },
60 Matching(Query),
62}
63
64#[derive(Debug, Clone)]
66#[non_exhaustive]
67pub struct CallQuery {
68 pub roots: CallRoots,
70 pub direction: Direction,
72 pub depth: usize,
74 pub max_nodes: usize,
76 pub virtual_calls: bool,
78 pub from: Option<Query>,
82 pub unless: Vec<Query>,
85}
86
87impl CallQuery {
88 #[must_use]
90 pub fn depth(self, depth: usize) -> Self {
91 CallQuery { depth, ..self }
92 }
93
94 #[must_use]
96 pub fn max_nodes(self, max_nodes: usize) -> Self {
97 CallQuery { max_nodes, ..self }
98 }
99
100 #[must_use]
102 pub fn virtual_calls(self, virtual_calls: bool) -> Self {
103 CallQuery { virtual_calls, ..self }
104 }
105
106 #[must_use]
108 pub fn from(self, from: Option<Query>) -> Self {
109 CallQuery { from, ..self }
110 }
111
112 #[must_use]
114 pub fn unless(self, unless: Vec<Query>) -> Self {
115 CallQuery { unless, ..self }
116 }
117}
118
119impl CallQuery {
120 pub fn method(class: impl Into<String>, method: impl Into<String>, direction: Direction) -> CallQuery {
123 CallQuery::new(CallRoots::Method { class: class.into(), method: method.into() }, direction)
124 }
125
126 pub fn new(roots: CallRoots, direction: Direction) -> CallQuery {
128 CallQuery { roots, direction, depth: 2, max_nodes: 500, virtual_calls: false, from: None, unless: Vec::new() }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134#[non_exhaustive]
135pub struct CallRow {
136 pub depth: usize,
138 pub parent: Option<usize>,
140 pub method: String,
142 pub dex: Vec<String>,
144 pub repeated: bool,
146 pub via: Option<String>,
149 pub from: bool,
151}
152
153#[derive(Debug, Clone)]
155#[non_exhaustive]
156pub struct CallTree {
157 pub direction: Direction,
159 pub depth: usize,
161 pub rows: Vec<CallRow>,
163 pub methods: usize,
165 pub truncated: bool,
167 pub pruned: usize,
169 pub reached: Option<usize>,
171 pub errors: Vec<String>,
173 pub notes: Vec<String>,
175}
176
177impl CallTree {
178 pub fn lines(&self) -> Vec<String> {
184 self.rows
185 .iter()
186 .map(|r| {
187 let mut s = " ".repeat(r.depth);
188 if r.depth == 0 {
189 s.push_str(&format!("0 {}", r.method));
190 } else {
191 s.push_str(&format!("{} {} {}", r.depth, self.direction.arrow(), r.method));
192 }
193 if r.dex.is_empty() {
194 s.push_str(" [no body]");
195 }
196 let mut archives: Vec<&str> = r.dex.iter().filter_map(|d| crate::archive_of(d)).collect();
198 archives.sort_unstable();
199 archives.dedup();
200 if !archives.is_empty() {
201 s.push_str(&format!(" [{}]", archives.join(", ")));
202 }
203 if let Some(v) = &r.via {
204 match self.direction {
205 Direction::Callers => s.push_str(&format!(" (via {v})")),
206 Direction::Callees => s.push_str(&format!(" (override of {v})")),
207 }
208 }
209 if r.from {
210 s.push_str(" [from]");
211 }
212 if r.repeated {
213 s.push_str(" (repeated)");
214 }
215 s
216 })
217 .collect()
218 }
219
220 pub fn footer(&self) -> String {
222 let mut s =
223 format!("# {} rows, {} distinct methods ({}, depth {})", self.rows.len(), self.methods, self.direction.as_str(), self.depth);
224 if let Some(n) = self.reached {
225 s.push_str(&format!("; {n} from methods reached"));
226 }
227 if self.pruned > 0 {
228 s.push_str(&format!("; {} methods left out by unless", self.pruned));
229 }
230 if self.truncated {
231 s.push_str("; truncated (raise max_nodes or lower depth)");
232 }
233 for n in &self.notes {
234 s.push_str(&format!("\n# {n}"));
235 }
236 s
237 }
238}
239
240struct Signature<'s> {
242 class: &'s str,
243 name: &'s str,
244 proto: &'s str,
245}
246
247fn split_signature(sig: &str) -> Option<Signature<'_>> {
248 let (class, rest) = sig.split_once("->")?;
249 let p = rest.find('(')?;
250 Some(Signature { class, name: &rest[..p], proto: &rest[p..] })
251}
252
253fn method_ids(dex: &Dex<'_>, class: &str, name: &str, proto: Option<&str>) -> Result<Vec<u32>> {
256 let Some(class_idx) = dex.find_type_idx(&asc_dex::mutf8::encode(class))? else { return Ok(Vec::new()) };
257 let Some(name_idx) = dex.find_string_idx(&asc_dex::mutf8::encode(name))? else { return Ok(Vec::new()) };
258 let target = (class_idx, name_idx);
259 let (mut lo, mut hi) = (0u32, dex.method_count());
260 while lo < hi {
261 let mid = lo + (hi - lo) / 2;
262 let id = dex.method_id(mid)?;
263 if ((id.class_idx as u32), id.name_idx) < target {
264 lo = mid + 1;
265 } else {
266 hi = mid;
267 }
268 }
269 let mut out = Vec::new();
270 for i in lo..dex.method_count() {
271 let id = dex.method_id(i)?;
272 if ((id.class_idx as u32), id.name_idx) != target {
273 break;
274 }
275 if proto.is_none_or(|p| asc_smali::proto_descriptor(dex, id.proto_idx as u32).is_ok_and(|d| names::proto_matches(&d, p))) {
276 out.push(i);
277 }
278 }
279 Ok(out)
280}
281
282const MAX_OVERRIDE_SCAN: usize = 4096;
284const MAX_SUPERS: usize = 256;
286
287#[derive(Default)]
289struct Found {
290 next: BTreeSet<String>,
292 bodies: BTreeSet<String>,
294}
295
296impl Engine {
297 pub fn call_graph(&self, q: &CallQuery) -> Result<CallTree> {
310 self.call_graph_cancellable(q, &NOT_CANCELLED)
311 }
312
313 pub fn call_graph_cancellable(&self, q: &CallQuery, cancel: &AtomicBool) -> Result<CallTree> {
320 if q.from.is_some() && q.direction == Direction::Callees {
321 return Err(EngineError::Query("`from` keeps paths of a callers walk: walk the callers of the target instead".into()));
322 }
323 let t0 = std::time::Instant::now();
324 let matching_roots = match &q.roots {
326 CallRoots::Matching(c) => Some(c),
327 CallRoots::Method { .. } => None,
328 };
329 let conditions: Vec<Query> = matching_roots.into_iter().chain(&q.from).chain(&q.unless).cloned().collect();
330 if let Some(engine) = self.transient(!conditions.is_empty())? {
332 return engine.call_graph_cancellable(q, cancel);
333 }
334 let matching = if conditions.is_empty() { None } else { Some(self.matching_methods(&conditions, cancel)?) };
335 let mut notes = matching.as_ref().map(|m| m.notes.clone()).unwrap_or_default();
336 let mut sets = matching.map(|m| m.sets).unwrap_or_default().into_iter();
337 let matched_roots = matching_roots.and_then(|_| sets.next());
338 let from = q.from.as_ref().and_then(|_| sets.next());
339 let unless: HashSet<String> = sets.flatten().collect();
340 let _query = self.prebuild.query();
341 let (views, mut errors) = self.load_views(None, cancel)?;
342 let stop = |views: &[DexView], e: EngineError| {
344 self.recharge_views(views);
345 e
346 };
347 let graph = if q.virtual_calls { Some(self.class_graph(&views, &mut errors, cancel).map_err(|e| stop(&views, e))?) } else { None };
348 let load_us = t0.elapsed().as_micros();
349 let mut roots = match (&q.roots, matched_roots) {
350 (CallRoots::Method { class, method }, _) => self.method_roots(&views, class, method, &mut notes)?,
351 (CallRoots::Matching(_), Some(set)) if !set.is_empty() => set,
352 (CallRoots::Matching(_), _) => return Err(EngineError::Query("no method's code matches the root condition".into())),
353 };
354 let mut pruned: HashSet<String> = roots.iter().filter(|r| unless.contains(*r)).cloned().collect();
355 roots.retain(|r| !pruned.contains(r));
356 let is_from = |sig: &str| from.as_ref().is_some_and(|f| f.contains(sig));
357 let max_nodes = q.max_nodes.max(roots.len());
358 let mut depth_of: HashMap<String, usize> = roots.iter().map(|r| (r.clone(), 0)).collect();
361 let mut next: HashMap<String, Vec<String>> = HashMap::new();
362 let mut bodies: HashMap<String, Vec<String>> = HashMap::new();
363 let mut via: HashMap<(String, String), String> = HashMap::new();
365 let mut frontier: Vec<String> = roots.iter().cloned().collect();
366 let mut truncated = false;
367 for level in 0..=q.depth {
368 if frontier.is_empty() {
369 break;
370 }
371 let expand = level < q.depth;
373 let aliases = match (&graph, q.direction) {
375 (Some(g), Direction::Callers) if expand => super_aliases(g, &frontier),
376 _ => HashMap::new(),
377 };
378 let mut lookup = frontier.clone();
379 lookup.extend(aliases.values().flatten().map(|(alias, _)| alias.clone()));
380 lookup.sort();
381 lookup.dedup();
382 let found = self.expand(&views, &lookup, q.direction, expand, &mut errors, cancel);
383 if cancel.load(Ordering::Relaxed) {
384 return Err(stop(&views, cancelled()));
385 }
386 let mut following = Vec::new();
387 for sig in &frontier {
388 let f = found.get(sig);
389 bodies.insert(sig.clone(), f.map(|f| f.bodies.iter().cloned().collect()).unwrap_or_default());
390 if !expand || is_from(sig) {
392 continue;
393 }
394 let mut neighbours: Vec<(String, Option<String>)> =
395 f.map(|f| f.next.iter()).into_iter().flatten().map(|n| (n.clone(), None)).collect();
396 for (alias, class) in aliases.get(sig).into_iter().flatten() {
397 for n in found.get(alias).map(|f| f.next.iter()).into_iter().flatten() {
398 neighbours.push((n.clone(), Some(class.clone())));
399 }
400 }
401 if let (Some(g), Direction::Callees) = (&graph, q.direction) {
402 let direct: Vec<String> = neighbours.iter().map(|(n, _)| n.clone()).collect();
403 for n in direct {
404 if cancel.load(Ordering::Relaxed) {
405 return Err(stop(&views, cancelled()));
406 }
407 let (overrides, cut) = overrides_of(g, &views, &n, &mut errors);
408 truncated |= cut;
409 neighbours.extend(overrides.into_iter().map(|o| (o, split_signature(&n).map(|p| p.class.to_string()))));
410 }
411 }
412 let mut seen = HashSet::new();
414 neighbours.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.is_some().cmp(&b.1.is_some())));
415 neighbours.retain(|(n, _)| seen.insert(n.clone()));
416 let mut kept = Vec::new();
417 for (n, through) in &neighbours {
418 if unless.contains(n) {
419 pruned.insert(n.clone());
420 continue;
421 }
422 if let Some(c) = through {
423 via.insert((sig.clone(), n.clone()), c.clone());
424 }
425 if depth_of.contains_key(n) {
426 kept.push(n.clone());
427 } else if depth_of.len() < max_nodes {
428 depth_of.insert(n.clone(), level + 1);
429 following.push(n.clone());
430 kept.push(n.clone());
431 } else {
432 truncated = true;
433 }
434 }
435 next.insert(sig.clone(), kept);
436 }
437 frontier = following;
438 }
439 self.recharge_views(&views);
440 let mut methods = depth_of.len();
441 let reached = from.as_ref().map(|from| {
442 let keep = on_paths_to(&next, depth_of.keys().filter(|m| from.contains(*m)));
443 for kids in next.values_mut() {
444 kids.retain(|k| keep.contains(k));
445 }
446 roots.retain(|r| keep.contains(r));
447 methods = keep.len();
448 depth_of.keys().filter(|m| from.contains(*m)).count()
449 });
450 if self.debug {
451 let graphs: Vec<_> = views.iter().filter_map(|v| v.state.call_graph()).collect();
452 self.log(format!(
453 "[APK] callgraph load={load_us} us total={} us dex={} graphs={} edges={} graph_bytes={}",
454 t0.elapsed().as_micros(),
455 views.len(),
456 graphs.len(),
457 graphs.iter().map(|g| g.edge_count()).sum::<usize>(),
458 graphs.iter().map(|g| g.heap_bytes()).sum::<usize>()
459 ));
460 }
461
462 let mut rows: Vec<CallRow> = Vec::new();
464 let mut expanded = HashSet::new();
465 let mut stack: Vec<(String, usize, Option<usize>)> = roots.iter().rev().map(|r| (r.clone(), 0, None)).collect();
466 while let Some((sig, depth, parent)) = stack.pop() {
467 let expand_here = depth_of.get(&sig) == Some(&depth) && !expanded.contains(&sig);
468 let has_children = next.get(&sig).is_some_and(|n| !n.is_empty());
469 rows.push(CallRow {
470 depth,
471 parent,
472 method: sig.clone(),
473 dex: bodies.get(&sig).cloned().unwrap_or_default(),
474 repeated: !expand_here && has_children,
475 via: parent.and_then(|p| via.get(&(rows[p].method.clone(), sig.clone())).cloned()),
476 from: is_from(&sig),
477 });
478 if expand_here {
479 expanded.insert(sig.clone());
480 let me = rows.len() - 1;
481 for n in next.get(&sig).into_iter().flatten().rev() {
482 stack.push((n.clone(), depth + 1, Some(me)));
483 }
484 }
485 }
486 errors.sort();
487 errors.dedup();
488 Ok(CallTree { direction: q.direction, depth: q.depth, rows, methods, truncated, pruned: pruned.len(), reached, errors, notes })
489 }
490
491 fn method_roots(&self, views: &[DexView], class: &str, method: &str, notes: &mut Vec<String>) -> Result<BTreeSet<String>> {
500 let split = |spec: &str| -> Result<(String, Option<String>)> {
501 let Some(p) = spec.find('(') else { return Ok((spec.to_string(), None)) };
502 match asc_smali::normalize_method_signature(spec) {
503 Ok(sig) => {
504 let p2 = sig.find('(').unwrap_or(p);
505 Ok((sig[..p2].to_string(), Some(sig[p2..].to_string())))
506 }
507 Err(e) => names::partial_proto(spec).map(|(n, proto)| (n, Some(proto))).ok_or_else(|| e.into()),
509 }
510 };
511 let collect = |cls: &str, name: &str, proto: Option<&str>| -> Result<BTreeSet<String>> {
512 let mut roots = BTreeSet::new();
513 for v in views {
514 let dex = Dex::with_name(&v.data, &v.name)?;
515 for m in method_ids(&dex, cls, name, proto)? {
516 roots.insert(asc_smali::method_signature(&dex, m)?);
517 }
518 }
519 Ok(roots)
520 };
521 let (name, proto) = split(method)?;
522 let roots = collect(class, &name, proto.as_deref())?;
523 if !roots.is_empty() {
524 return Ok(roots);
525 }
526 let classes = std::iter::once(class.to_string()).chain(names::denormalize_class_candidates(class));
527 for cls in classes {
528 let methods = std::iter::once(method.to_string()).chain(names::denormalize_method_candidates(Some(&cls), method));
529 for m in methods {
530 if cls == class && m == method {
531 continue;
532 }
533 let Ok((name, proto)) = split(&m) else { continue };
534 let roots = collect(&cls, &name, proto.as_deref())?;
535 if !roots.is_empty() {
536 notes.push(format!("{class}->{method} read as the Java/Kotlin spelling of {cls}->{m}"));
537 return Ok(roots);
538 }
539 }
540 }
541 let hint = self.member_not_found_hint(class, Some(&name), true);
542 Err(EngineError::Query(format!("no method {class}->{method} in this APK{hint}")))
543 }
544
545 fn expand(
548 &self,
549 views: &[DexView],
550 frontier: &[String],
551 dir: crate::Direction,
552 expand: bool,
553 errors: &mut Vec<String>,
554 cancel: &AtomicBool,
555 ) -> HashMap<String, Found> {
556 let next_view = AtomicUsize::new(0);
557 let merged: Mutex<HashMap<String, Found>> = Mutex::new(HashMap::new());
558 let errs = Mutex::new(Vec::new());
559 let workers = self.threads().min(views.len()).max(1);
560 std::thread::scope(|s| {
561 for _ in 0..workers {
562 s.spawn(|| {
563 loop {
564 if cancel.load(Ordering::Relaxed) {
565 break;
566 }
567 let i = next_view.fetch_add(1, Ordering::Relaxed);
568 let Some(v) = views.get(i) else { break };
569 match view_neighbours(v, frontier, dir, expand) {
570 Ok(local) => {
571 let mut m = merged.lock().expect("merged callers mutex poisoned");
572 for (sig, f) in local {
573 let e = m.entry(sig).or_default();
574 e.next.extend(f.next);
575 e.bodies.extend(f.bodies);
576 }
577 }
578 Err(e) => errs.lock().expect("errors mutex poisoned").push(format!("{}: {e}", v.name)),
579 }
580 }
581 });
582 }
583 });
584 errors.extend(errs.into_inner().expect("errors mutex poisoned"));
585 merged.into_inner().expect("merged callers mutex poisoned")
586 }
587}
588
589fn on_paths_to<'s>(next: &HashMap<String, Vec<String>>, ends: impl Iterator<Item = &'s String>) -> HashSet<String> {
592 let mut parents: HashMap<&str, Vec<&str>> = HashMap::new();
593 for (p, kids) in next {
594 for k in kids {
595 parents.entry(k).or_default().push(p);
596 }
597 }
598 let mut keep: HashSet<String> = ends.cloned().collect();
599 let mut todo: Vec<String> = keep.iter().cloned().collect();
600 while let Some(m) = todo.pop() {
601 for &p in parents.get(m.as_str()).into_iter().flatten() {
602 if keep.insert(p.to_string()) {
603 todo.push(p.to_string());
604 }
605 }
606 }
607 keep
608}
609
610fn super_aliases(g: &ClassGraph, frontier: &[String]) -> HashMap<String, Vec<(String, String)>> {
613 let mut out = HashMap::new();
614 for sig in frontier {
615 let Some(p) = split_signature(sig) else { continue };
616 if p.name.starts_with('<') {
617 continue;
618 }
619 let aliases: Vec<(String, String)> =
620 g.reachable(p.class, Lineage::Supers, MAX_SUPERS).into_iter().map(|s| (format!("{s}->{}{}", p.name, p.proto), s)).collect();
621 if !aliases.is_empty() {
622 out.insert(sig.clone(), aliases);
623 }
624 }
625 out
626}
627
628fn overrides_of(g: &ClassGraph, views: &[DexView], sig: &str, errors: &mut Vec<String>) -> (Vec<String>, bool) {
631 let Some(p) = split_signature(sig) else { return (Vec::new(), false) };
632 if p.name.starts_with('<') {
633 return (Vec::new(), false);
634 }
635 let subs = g.reachable(p.class, Lineage::Subclasses, MAX_OVERRIDE_SCAN);
636 let cut = subs.len() >= MAX_OVERRIDE_SCAN;
637 let mut out = Vec::new();
638 for s in subs {
639 match g.defines(views, &s, p.name, Some(p.proto)) {
640 Ok(d) if !d.is_empty() => out.push(format!("{s}->{}{}", p.name, p.proto)),
641 Ok(_) => {}
642 Err(e) => errors.push(format!("{s}: {e}")),
643 }
644 }
645 (out, cut)
646}
647
648fn view_neighbours(v: &DexView, frontier: &[String], dir: Direction, expand: bool) -> Result<HashMap<String, Found>> {
649 let dex = Dex::with_name(&v.data, &v.name)?;
650 let fr = FindRefs::with_state(&dex, &v.state);
651 let mut out: HashMap<String, Found> = HashMap::new();
652 let mut graph = None;
655 for sig in frontier {
656 let Some(parts) = split_signature(sig) else { continue };
657 for m in method_ids(&dex, parts.class, parts.name, Some(parts.proto))? {
658 let g = match graph {
659 Some(g) => g,
660 None => *graph.insert(fr.call_graph()?),
661 };
662 let f = out.entry(sig.clone()).or_default();
663 if g.has_body(m) {
664 f.bodies.insert(v.name.clone());
665 }
666 if !expand {
667 continue;
668 }
669 let ids = match dir {
670 Direction::Callers => g.callers(m),
671 Direction::Callees => g.callees(m),
672 };
673 for &n in ids {
674 f.next.insert(asc_smali::method_signature(&dex, n as u32)?);
675 }
676 }
677 }
678 Ok(out)
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 #[test]
686 fn signatures_split_at_the_arrow_and_paren() {
687 let s = split_signature("Lcom/a;->run(ILjava/lang/String;)V").unwrap();
688 assert_eq!((s.class, s.name, s.proto), ("Lcom/a;", "run", "(ILjava/lang/String;)V"));
689 assert!(split_signature("Lcom/a;->run").is_none());
690 assert!(split_signature("run()V").is_none());
691 }
692}