Skip to main content

asc_engine/
callgraph.rs

1//! Callers / callees of a method across every DEX of an APK.
2//!
3//! Each DEX has its own static call graph ([`asc_findrefs::CallGraph`], built
4//! on first use and kept with the cached entry's locator tables). Method ids
5//! are local to a DEX, so the walk keys methods by signature
6//! (`Lcls;->name(params)ret`) and, level by level, looks each frontier method
7//! up in every DEX (binary search over the sorted `method_id` table), then
8//! follows that DEX's edges. Calls are static references: a call through
9//! `Lsub;->m()V` does not count as a call of `Lbase;->m()V`, unless the query
10//! asks for virtual dispatch ([`CallQuery::virtual_calls`]): then the callers
11//! of a method also include the callers of the same signature on each of its
12//! supertypes (marked `via`), and the callees of a method also include the
13//! overrides of each callee in its subclasses (marked `override of`).
14
15use 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/// Which way to walk.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Direction {
26    /// Methods whose body invokes the target.
27    Callers,
28    /// Methods the target's body invokes.
29    Callees,
30}
31
32impl Direction {
33    /// `callers` or `callees`, as in the query syntax and output.
34    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/// Where a call graph starts.
50#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub enum CallRoots {
53    /// The overloads of one method.
54    Method {
55        /// Declaring class, as a Dalvik descriptor (`Lcom/poc/Main;`).
56        class: String,
57        /// Method name (every overload) or exact `name(params)ret`.
58        method: String,
59    },
60    /// Every method whose code matches the condition (a findrefs rule).
61    Matching(Query),
62}
63
64/// A call graph query.
65#[derive(Debug, Clone)]
66#[non_exhaustive]
67pub struct CallQuery {
68    /// Where the walk starts.
69    pub roots: CallRoots,
70    /// Whether to walk callers or callees.
71    pub direction: Direction,
72    /// Levels to expand (1 = direct callers / callees).
73    pub depth: usize,
74    /// Stop discovering methods beyond this many (roots included).
75    pub max_nodes: usize,
76    /// Follow virtual dispatch through the class hierarchy (see the module docs).
77    pub virtual_calls: bool,
78    /// Callers only: keep just the branches that end at a method whose code
79    /// matches this (the paths from such a method to a root), and stop
80    /// there.
81    pub from: Option<Query>,
82    /// Leave out every method whose code matches one of these, with
83    /// everything reached only through it.
84    pub unless: Vec<Query>,
85}
86
87impl CallQuery {
88    /// The query with [`depth`](#structfield.depth) set.
89    #[must_use]
90    pub fn depth(self, depth: usize) -> Self {
91        CallQuery { depth, ..self }
92    }
93
94    /// The query with [`max_nodes`](#structfield.max_nodes) set.
95    #[must_use]
96    pub fn max_nodes(self, max_nodes: usize) -> Self {
97        CallQuery { max_nodes, ..self }
98    }
99
100    /// The query with [`virtual_calls`](#structfield.virtual_calls) set.
101    #[must_use]
102    pub fn virtual_calls(self, virtual_calls: bool) -> Self {
103        CallQuery { virtual_calls, ..self }
104    }
105
106    /// The query with [`from`](#structfield.from) set.
107    #[must_use]
108    pub fn from(self, from: Option<Query>) -> Self {
109        CallQuery { from, ..self }
110    }
111
112    /// The query with [`unless`](#structfield.unless) set.
113    #[must_use]
114    pub fn unless(self, unless: Vec<Query>) -> Self {
115        CallQuery { unless, ..self }
116    }
117}
118
119impl CallQuery {
120    /// Callers or callees of the overloads of `class`'s `method`: 2 levels,
121    /// at most 500 methods, static calls only.
122    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    /// A walk from `roots`: 2 levels, at most 500 methods, static calls only.
127    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/// One line of the tree, in depth-first order.
133#[derive(Debug, Clone, PartialEq, Eq)]
134#[non_exhaustive]
135pub struct CallRow {
136    /// 0 for a root.
137    pub depth: usize,
138    /// Index of the parent row (`None` for a root).
139    pub parent: Option<usize>,
140    /// `Lcls;->name(params)ret`
141    pub method: String,
142    /// DEX files defining a body for it (empty when none does).
143    pub dex: Vec<String>,
144    /// Shown again: expanded at another row.
145    pub repeated: bool,
146    /// Reached through virtual dispatch: the supertype whose method this
147    /// caller invokes, or (callees) the class of the callee this overrides.
148    pub via: Option<String>,
149    /// Its code matches [`CallQuery::from`].
150    pub from: bool,
151}
152
153/// Result of [`Engine::call_graph`].
154#[derive(Debug, Clone)]
155#[non_exhaustive]
156pub struct CallTree {
157    /// The direction walked.
158    pub direction: Direction,
159    /// The depth asked for.
160    pub depth: usize,
161    /// The tree, one row per line in depth-first order.
162    pub rows: Vec<CallRow>,
163    /// Distinct methods discovered (roots included).
164    pub methods: usize,
165    /// More callers / callees exist than `max_nodes` allowed.
166    pub truncated: bool,
167    /// Methods left out because their code matches [`CallQuery::unless`].
168    pub pruned: usize,
169    /// With [`CallQuery::from`]: the distinct `from` methods reached.
170    pub reached: Option<usize>,
171    /// DEX entries that could not be loaded or searched.
172    pub errors: Vec<String>,
173    /// What the conditions left out on purpose ([`crate::RefSummary::notes`]).
174    pub notes: Vec<String>,
175}
176
177impl CallTree {
178    /// One line per row: indentation and the depth number, an arrow
179    /// (`<-` caller, `->` callee), the signature and flags:
180    /// `[no body]` (defined in no DEX of the APK, e.g. a framework method),
181    /// `[services.jar]` (in a program of several archives: those defining
182    /// it) and `(repeated)` (expanded elsewhere in the tree).
183    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                // In a program of several archives: which defines it.
197                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    /// Footer comment summarising the walk.
221    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
240/// `Lcls;->name(params)ret` split into its parts.
241struct 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
253/// Method ids of `dex` declared by `class` named `name`, with the proto
254/// `proto` when given. `method_id`s are sorted by (class, name, proto).
255fn 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
282/// Subclasses checked for overrides of one callee (`virtual_calls`).
283const MAX_OVERRIDE_SCAN: usize = 4096;
284/// Supertypes whose signature stands for a frontier method (`virtual_calls`).
285const MAX_SUPERS: usize = 256;
286
287/// What one DEX says about one frontier method.
288#[derive(Default)]
289struct Found {
290    /// Neighbours (callers or callees), as signatures.
291    next: BTreeSet<String>,
292    /// DEX files with a body for the method.
293    bodies: BTreeSet<String>,
294}
295
296impl Engine {
297    /// Callers or callees of a method, `depth` levels deep, as a tree.
298    ///
299    /// The roots are every overload of `method` declared by `class` (or the
300    /// exact signature) that some DEX references; an unknown method is an
301    /// error. Each method is expanded once, at its shallowest occurrence;
302    /// other occurrences are marked `repeated`.
303    ///
304    /// # Errors
305    ///
306    /// [`EngineError::Query`] for an inconsistent query (`from` on a callees
307    /// walk), a root method no DEX references or a condition no method
308    /// matches; [`EngineError::Apk`] when the APK cannot be opened.
309    pub fn call_graph(&self, q: &CallQuery) -> Result<CallTree> {
310        self.call_graph_cancellable(q, &NOT_CANCELLED)
311    }
312
313    /// [`Engine::call_graph`] that stops, with `ApkError::Cancelled`, once
314    /// `cancel` is raised.
315    ///
316    /// # Errors
317    ///
318    /// As [`Engine::call_graph`], plus the cancellation.
319    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        // Every condition (roots, from, unless) answered in one pass.
325        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        // The conditions read every DEX, and the walk reads them again.
331        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        // The tables built before a cancellation stay charged to the cache.
343        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        // Breadth-first: `depth_of` is each method's shallowest level, `next`
359        // its neighbours, `bodies` where it is defined.
360        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        // (parent, child) -> the class virtual dispatch went through
364        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            // The last level is only looked up (for `bodies`), not expanded.
372            let expand = level < q.depth;
373            // Callers through a supertype: look its signatures up too.
374            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                // A `from` method ends its paths.
391                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                // A direct edge wins over the same edge through dispatch.
413                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        // Depth-first rendering, expanding each method once at its BFS depth.
463        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    /// Every overload of `class`'s `method` (or that exact signature) some
492    /// DEX knows; an unknown method is an error.
493    ///
494    /// A signature may leave out the return type (`run(int)`). When nothing
495    /// matches as written, the class and method are also read as decompiled
496    /// Java / Kotlin source spells them (`com.a.Outer.Inner`, `Cdo`, the
497    /// class name for a constructor), the first reading that matches
498    /// winning; `notes` then says so.
499    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                // `run(int)`, `entry(com.a.Outer.Inner)`: no return type.
508                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    /// Look every frontier method up in every DEX (in parallel) and collect
546    /// where it has a body and, when `expand`, its neighbours.
547    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
589/// The methods on a path to one of `ends` along the `next` edges (`ends`
590/// included); the graph may have cycles.
591fn 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
610/// For each frontier method (not a constructor / initializer), the same
611/// signature on each supertype of its class: `sig -> [(alias, supertype)]`.
612fn 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
628/// Methods overriding `sig` in subclasses of its class (a constructor /
629/// initializer has none), and whether the subclass scan was cut short.
630fn 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    // Building the graph walks every body; skip it for a DEX that defines and
653    // references none of the frontier.
654    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}