1use crate::hierarchy::{ClassGraph, Lineage};
16use crate::{DexView, Engine, EngineError, NOT_CANCELLED, Result, cancelled};
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 {
34 match self {
35 Direction::Callers => "callers",
36 Direction::Callees => "callees",
37 }
38 }
39
40 fn arrow(self) -> &'static str {
41 match self {
42 Direction::Callers => "<-",
43 Direction::Callees => "->",
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50pub enum CallRoots {
51 Method {
53 class: String,
55 method: String,
57 },
58 Matching(Query),
60}
61
62#[derive(Debug, Clone)]
64pub struct CallQuery {
65 pub roots: CallRoots,
66 pub direction: Direction,
67 pub depth: usize,
69 pub max_nodes: usize,
71 pub virtual_calls: bool,
73 pub from: Option<Query>,
77 pub unless: Vec<Query>,
80}
81
82impl CallQuery {
83 pub fn method(class: impl Into<String>, method: impl Into<String>, direction: Direction) -> CallQuery {
86 CallQuery::new(CallRoots::Method { class: class.into(), method: method.into() }, direction)
87 }
88
89 pub fn new(roots: CallRoots, direction: Direction) -> CallQuery {
90 CallQuery { roots, direction, depth: 2, max_nodes: 500, virtual_calls: false, from: None, unless: Vec::new() }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct CallRow {
97 pub depth: usize,
99 pub parent: Option<usize>,
101 pub method: String,
103 pub dex: Vec<String>,
105 pub repeated: bool,
107 pub via: Option<String>,
110 pub from: bool,
112}
113
114#[derive(Debug, Clone)]
116pub struct CallTree {
117 pub direction: Direction,
118 pub depth: usize,
119 pub rows: Vec<CallRow>,
120 pub methods: usize,
122 pub truncated: bool,
124 pub pruned: usize,
126 pub reached: Option<usize>,
128 pub errors: Vec<String>,
130 pub notes: Vec<String>,
132}
133
134impl CallTree {
135 pub fn lines(&self) -> Vec<String> {
141 self.rows
142 .iter()
143 .map(|r| {
144 let mut s = " ".repeat(r.depth);
145 if r.depth == 0 {
146 s.push_str(&format!("0 {}", r.method));
147 } else {
148 s.push_str(&format!("{} {} {}", r.depth, self.direction.arrow(), r.method));
149 }
150 if r.dex.is_empty() {
151 s.push_str(" [no body]");
152 }
153 let mut archives: Vec<&str> = r.dex.iter().filter_map(|d| crate::archive_of(d)).collect();
155 archives.sort_unstable();
156 archives.dedup();
157 if !archives.is_empty() {
158 s.push_str(&format!(" [{}]", archives.join(", ")));
159 }
160 if let Some(v) = &r.via {
161 match self.direction {
162 Direction::Callers => s.push_str(&format!(" (via {v})")),
163 Direction::Callees => s.push_str(&format!(" (override of {v})")),
164 }
165 }
166 if r.from {
167 s.push_str(" [from]");
168 }
169 if r.repeated {
170 s.push_str(" (repeated)");
171 }
172 s
173 })
174 .collect()
175 }
176
177 pub fn footer(&self) -> String {
179 let mut s =
180 format!("# {} rows, {} distinct methods ({}, depth {})", self.rows.len(), self.methods, self.direction.as_str(), self.depth);
181 if let Some(n) = self.reached {
182 s.push_str(&format!("; {n} from methods reached"));
183 }
184 if self.pruned > 0 {
185 s.push_str(&format!("; {} methods left out by unless", self.pruned));
186 }
187 if self.truncated {
188 s.push_str("; truncated (raise max_nodes or lower depth)");
189 }
190 for n in &self.notes {
191 s.push_str(&format!("\n# {n}"));
192 }
193 s
194 }
195}
196
197struct Signature<'s> {
199 class: &'s str,
200 name: &'s str,
201 proto: &'s str,
202}
203
204fn split_signature(sig: &str) -> Option<Signature<'_>> {
205 let (class, rest) = sig.split_once("->")?;
206 let p = rest.find('(')?;
207 Some(Signature { class, name: &rest[..p], proto: &rest[p..] })
208}
209
210fn method_ids(dex: &Dex<'_>, class: &str, name: &str, proto: Option<&str>) -> Result<Vec<u32>> {
213 let Some(class_idx) = dex.find_type_idx(&asc_dex::mutf8::encode(class))? else { return Ok(Vec::new()) };
214 let Some(name_idx) = dex.find_string_idx(&asc_dex::mutf8::encode(name))? else { return Ok(Vec::new()) };
215 let target = (class_idx, name_idx);
216 let (mut lo, mut hi) = (0u32, dex.method_count());
217 while lo < hi {
218 let mid = lo + (hi - lo) / 2;
219 let id = dex.method_id(mid)?;
220 if ((id.class_idx as u32), id.name_idx) < target {
221 lo = mid + 1;
222 } else {
223 hi = mid;
224 }
225 }
226 let mut out = Vec::new();
227 for i in lo..dex.method_count() {
228 let id = dex.method_id(i)?;
229 if ((id.class_idx as u32), id.name_idx) != target {
230 break;
231 }
232 if proto.is_none_or(|p| asc_smali::proto_descriptor(dex, id.proto_idx as u32).is_ok_and(|d| d == p)) {
233 out.push(i);
234 }
235 }
236 Ok(out)
237}
238
239const MAX_OVERRIDE_SCAN: usize = 4096;
241const MAX_SUPERS: usize = 256;
243
244#[derive(Default)]
246struct Found {
247 next: BTreeSet<String>,
249 bodies: BTreeSet<String>,
251}
252
253impl Engine {
254 pub fn call_graph(&self, q: &CallQuery) -> Result<CallTree> {
261 self.call_graph_cancellable(q, &NOT_CANCELLED)
262 }
263
264 pub fn call_graph_cancellable(&self, q: &CallQuery, cancel: &AtomicBool) -> Result<CallTree> {
267 if q.from.is_some() && q.direction == Direction::Callees {
268 return Err(EngineError::Query("`from` keeps paths of a callers walk: walk the callers of the target instead".into()));
269 }
270 let t0 = std::time::Instant::now();
271 let matching_roots = match &q.roots {
273 CallRoots::Matching(c) => Some(c),
274 CallRoots::Method { .. } => None,
275 };
276 let conditions: Vec<Query> = matching_roots.into_iter().chain(&q.from).chain(&q.unless).cloned().collect();
277 if let Some(engine) = self.transient(!conditions.is_empty())? {
279 return engine.call_graph_cancellable(q, cancel);
280 }
281 let matching = if conditions.is_empty() { None } else { Some(self.matching_methods(&conditions, cancel)?) };
282 let notes = matching.as_ref().map(|m| m.notes.clone()).unwrap_or_default();
283 let mut sets = matching.map(|m| m.sets).unwrap_or_default().into_iter();
284 let matched_roots = matching_roots.and_then(|_| sets.next());
285 let from = q.from.as_ref().and_then(|_| sets.next());
286 let unless: HashSet<String> = sets.flatten().collect();
287 let _query = self.prebuild.query();
288 let (views, mut errors) = self.load_views(None, cancel)?;
289 let stop = |views: &[DexView], e: EngineError| {
291 self.recharge_views(views);
292 e
293 };
294 let graph = if q.virtual_calls { Some(self.class_graph(&views, &mut errors, cancel).map_err(|e| stop(&views, e))?) } else { None };
295 let load_us = t0.elapsed().as_micros();
296 let mut roots = match (&q.roots, matched_roots) {
297 (CallRoots::Method { class, method }, _) => self.method_roots(&views, class, method)?,
298 (CallRoots::Matching(_), Some(set)) if !set.is_empty() => set,
299 (CallRoots::Matching(_), _) => return Err(EngineError::Query("no method's code matches the root condition".into())),
300 };
301 let mut pruned: HashSet<String> = roots.iter().filter(|r| unless.contains(*r)).cloned().collect();
302 roots.retain(|r| !pruned.contains(r));
303 let is_from = |sig: &str| from.as_ref().is_some_and(|f| f.contains(sig));
304 let max_nodes = q.max_nodes.max(roots.len());
305 let mut depth_of: HashMap<String, usize> = roots.iter().map(|r| (r.clone(), 0)).collect();
308 let mut next: HashMap<String, Vec<String>> = HashMap::new();
309 let mut bodies: HashMap<String, Vec<String>> = HashMap::new();
310 let mut via: HashMap<(String, String), String> = HashMap::new();
312 let mut frontier: Vec<String> = roots.iter().cloned().collect();
313 let mut truncated = false;
314 for level in 0..=q.depth {
315 if frontier.is_empty() {
316 break;
317 }
318 let expand = level < q.depth;
320 let aliases = match (&graph, q.direction) {
322 (Some(g), Direction::Callers) if expand => super_aliases(g, &frontier),
323 _ => HashMap::new(),
324 };
325 let mut lookup = frontier.clone();
326 lookup.extend(aliases.values().flatten().map(|(alias, _)| alias.clone()));
327 lookup.sort();
328 lookup.dedup();
329 let found = self.expand(&views, &lookup, q.direction, expand, &mut errors, cancel);
330 if cancel.load(Ordering::Relaxed) {
331 return Err(stop(&views, cancelled()));
332 }
333 let mut following = Vec::new();
334 for sig in &frontier {
335 let f = found.get(sig);
336 bodies.insert(sig.clone(), f.map(|f| f.bodies.iter().cloned().collect()).unwrap_or_default());
337 if !expand || is_from(sig) {
339 continue;
340 }
341 let mut neighbours: Vec<(String, Option<String>)> =
342 f.map(|f| f.next.iter()).into_iter().flatten().map(|n| (n.clone(), None)).collect();
343 for (alias, class) in aliases.get(sig).into_iter().flatten() {
344 for n in found.get(alias).map(|f| f.next.iter()).into_iter().flatten() {
345 neighbours.push((n.clone(), Some(class.clone())));
346 }
347 }
348 if let (Some(g), Direction::Callees) = (&graph, q.direction) {
349 let direct: Vec<String> = neighbours.iter().map(|(n, _)| n.clone()).collect();
350 for n in direct {
351 if cancel.load(Ordering::Relaxed) {
352 return Err(stop(&views, cancelled()));
353 }
354 let (overrides, cut) = overrides_of(g, &views, &n, &mut errors);
355 truncated |= cut;
356 neighbours.extend(overrides.into_iter().map(|o| (o, split_signature(&n).map(|p| p.class.to_string()))));
357 }
358 }
359 let mut seen = HashSet::new();
361 neighbours.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.is_some().cmp(&b.1.is_some())));
362 neighbours.retain(|(n, _)| seen.insert(n.clone()));
363 let mut kept = Vec::new();
364 for (n, through) in &neighbours {
365 if unless.contains(n) {
366 pruned.insert(n.clone());
367 continue;
368 }
369 if let Some(c) = through {
370 via.insert((sig.clone(), n.clone()), c.clone());
371 }
372 if depth_of.contains_key(n) {
373 kept.push(n.clone());
374 } else if depth_of.len() < max_nodes {
375 depth_of.insert(n.clone(), level + 1);
376 following.push(n.clone());
377 kept.push(n.clone());
378 } else {
379 truncated = true;
380 }
381 }
382 next.insert(sig.clone(), kept);
383 }
384 frontier = following;
385 }
386 self.recharge_views(&views);
387 let mut methods = depth_of.len();
388 let reached = from.as_ref().map(|from| {
389 let keep = on_paths_to(&next, depth_of.keys().filter(|m| from.contains(*m)));
390 for kids in next.values_mut() {
391 kids.retain(|k| keep.contains(k));
392 }
393 roots.retain(|r| keep.contains(r));
394 methods = keep.len();
395 depth_of.keys().filter(|m| from.contains(*m)).count()
396 });
397 if self.debug {
398 let graphs: Vec<_> = views.iter().filter_map(|v| v.state.call_graph()).collect();
399 self.log(format!(
400 "[APK] callgraph load={load_us} us total={} us dex={} graphs={} edges={} graph_bytes={}",
401 t0.elapsed().as_micros(),
402 views.len(),
403 graphs.len(),
404 graphs.iter().map(|g| g.edge_count()).sum::<usize>(),
405 graphs.iter().map(|g| g.heap_bytes()).sum::<usize>()
406 ));
407 }
408
409 let mut rows: Vec<CallRow> = Vec::new();
411 let mut expanded = HashSet::new();
412 let mut stack: Vec<(String, usize, Option<usize>)> = roots.iter().rev().map(|r| (r.clone(), 0, None)).collect();
413 while let Some((sig, depth, parent)) = stack.pop() {
414 let expand_here = depth_of.get(&sig) == Some(&depth) && !expanded.contains(&sig);
415 let has_children = next.get(&sig).is_some_and(|n| !n.is_empty());
416 rows.push(CallRow {
417 depth,
418 parent,
419 method: sig.clone(),
420 dex: bodies.get(&sig).cloned().unwrap_or_default(),
421 repeated: !expand_here && has_children,
422 via: parent.and_then(|p| via.get(&(rows[p].method.clone(), sig.clone())).cloned()),
423 from: is_from(&sig),
424 });
425 if expand_here {
426 expanded.insert(sig.clone());
427 let me = rows.len() - 1;
428 for n in next.get(&sig).into_iter().flatten().rev() {
429 stack.push((n.clone(), depth + 1, Some(me)));
430 }
431 }
432 }
433 errors.sort();
434 errors.dedup();
435 Ok(CallTree { direction: q.direction, depth: q.depth, rows, methods, truncated, pruned: pruned.len(), reached, errors, notes })
436 }
437
438 fn method_roots(&self, views: &[DexView], class: &str, method: &str) -> Result<BTreeSet<String>> {
441 let (name, proto) = match method.find('(') {
442 Some(p) => {
443 let sig = asc_smali::normalize_method_signature(method)?;
444 let p2 = sig.find('(').unwrap_or(p);
445 (sig[..p2].to_string(), Some(sig[p2..].to_string()))
446 }
447 None => (method.to_string(), None),
448 };
449 let mut roots = BTreeSet::new();
450 for v in views {
451 let dex = Dex::with_name(&v.data, &v.name)?;
452 for m in method_ids(&dex, class, &name, proto.as_deref())? {
453 roots.insert(asc_smali::method_signature(&dex, m)?);
454 }
455 }
456 if roots.is_empty() {
457 let hint = self.member_not_found_hint(class, Some(&name), true);
458 return Err(EngineError::Query(format!("no method {class}->{method} in this APK{hint}")));
459 }
460 Ok(roots)
461 }
462
463 fn expand(
466 &self,
467 views: &[DexView],
468 frontier: &[String],
469 dir: crate::Direction,
470 expand: bool,
471 errors: &mut Vec<String>,
472 cancel: &AtomicBool,
473 ) -> HashMap<String, Found> {
474 let next_view = AtomicUsize::new(0);
475 let merged: Mutex<HashMap<String, Found>> = Mutex::new(HashMap::new());
476 let errs = Mutex::new(Vec::new());
477 let workers = self.threads().min(views.len()).max(1);
478 std::thread::scope(|s| {
479 for _ in 0..workers {
480 s.spawn(|| {
481 loop {
482 if cancel.load(Ordering::Relaxed) {
483 break;
484 }
485 let i = next_view.fetch_add(1, Ordering::Relaxed);
486 let Some(v) = views.get(i) else { break };
487 match view_neighbours(v, frontier, dir, expand) {
488 Ok(local) => {
489 let mut m = merged.lock().unwrap();
490 for (sig, f) in local {
491 let e = m.entry(sig).or_default();
492 e.next.extend(f.next);
493 e.bodies.extend(f.bodies);
494 }
495 }
496 Err(e) => errs.lock().unwrap().push(format!("{}: {e}", v.name)),
497 }
498 }
499 });
500 }
501 });
502 errors.extend(errs.into_inner().unwrap());
503 merged.into_inner().unwrap()
504 }
505}
506
507fn on_paths_to<'s>(next: &HashMap<String, Vec<String>>, ends: impl Iterator<Item = &'s String>) -> HashSet<String> {
510 let mut parents: HashMap<&str, Vec<&str>> = HashMap::new();
511 for (p, kids) in next {
512 for k in kids {
513 parents.entry(k).or_default().push(p);
514 }
515 }
516 let mut keep: HashSet<String> = ends.cloned().collect();
517 let mut todo: Vec<String> = keep.iter().cloned().collect();
518 while let Some(m) = todo.pop() {
519 for &p in parents.get(m.as_str()).into_iter().flatten() {
520 if keep.insert(p.to_string()) {
521 todo.push(p.to_string());
522 }
523 }
524 }
525 keep
526}
527
528fn super_aliases(g: &ClassGraph, frontier: &[String]) -> HashMap<String, Vec<(String, String)>> {
531 let mut out = HashMap::new();
532 for sig in frontier {
533 let Some(p) = split_signature(sig) else { continue };
534 if p.name.starts_with('<') {
535 continue;
536 }
537 let aliases: Vec<(String, String)> =
538 g.reachable(p.class, Lineage::Supers, MAX_SUPERS).into_iter().map(|s| (format!("{s}->{}{}", p.name, p.proto), s)).collect();
539 if !aliases.is_empty() {
540 out.insert(sig.clone(), aliases);
541 }
542 }
543 out
544}
545
546fn overrides_of(g: &ClassGraph, views: &[DexView], sig: &str, errors: &mut Vec<String>) -> (Vec<String>, bool) {
549 let Some(p) = split_signature(sig) else { return (Vec::new(), false) };
550 if p.name.starts_with('<') {
551 return (Vec::new(), false);
552 }
553 let subs = g.reachable(p.class, Lineage::Subclasses, MAX_OVERRIDE_SCAN);
554 let cut = subs.len() >= MAX_OVERRIDE_SCAN;
555 let mut out = Vec::new();
556 for s in subs {
557 match g.defines(views, &s, p.name, Some(p.proto)) {
558 Ok(d) if !d.is_empty() => out.push(format!("{s}->{}{}", p.name, p.proto)),
559 Ok(_) => {}
560 Err(e) => errors.push(format!("{s}: {e}")),
561 }
562 }
563 (out, cut)
564}
565
566fn view_neighbours(v: &DexView, frontier: &[String], dir: Direction, expand: bool) -> Result<HashMap<String, Found>> {
567 let dex = Dex::with_name(&v.data, &v.name)?;
568 let fr = FindRefs::with_state(&dex, &v.state);
569 let mut out: HashMap<String, Found> = HashMap::new();
570 let mut graph = None;
573 for sig in frontier {
574 let Some(parts) = split_signature(sig) else { continue };
575 for m in method_ids(&dex, parts.class, parts.name, Some(parts.proto))? {
576 let g = match graph {
577 Some(g) => g,
578 None => *graph.insert(fr.call_graph()?),
579 };
580 let f = out.entry(sig.clone()).or_default();
581 if g.has_body(m) {
582 f.bodies.insert(v.name.clone());
583 }
584 if !expand {
585 continue;
586 }
587 let ids = match dir {
588 Direction::Callers => g.callers(m),
589 Direction::Callees => g.callees(m),
590 };
591 for &n in ids {
592 f.next.insert(asc_smali::method_signature(&dex, n as u32)?);
593 }
594 }
595 }
596 Ok(out)
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn signatures_split_at_the_arrow_and_paren() {
605 let s = split_signature("Lcom/a;->run(ILjava/lang/String;)V").unwrap();
606 assert_eq!((s.class, s.name, s.proto), ("Lcom/a;", "run", "(ILjava/lang/String;)V"));
607 assert!(split_signature("Lcom/a;->run").is_none());
608 assert!(split_signature("run()V").is_none());
609 }
610}