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};
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    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/// Where a call graph starts.
49#[derive(Debug, Clone)]
50pub enum CallRoots {
51    /// The overloads of one method.
52    Method {
53        /// Declaring class, as a Dalvik descriptor (`Lcom/poc/Main;`).
54        class: String,
55        /// Method name (every overload) or exact `name(params)ret`.
56        method: String,
57    },
58    /// Every method whose code matches the condition (a findrefs rule).
59    Matching(Query),
60}
61
62/// A call graph query.
63#[derive(Debug, Clone)]
64pub struct CallQuery {
65    pub roots: CallRoots,
66    pub direction: Direction,
67    /// Levels to expand (1 = direct callers / callees).
68    pub depth: usize,
69    /// Stop discovering methods beyond this many (roots included).
70    pub max_nodes: usize,
71    /// Follow virtual dispatch through the class hierarchy (see the module docs).
72    pub virtual_calls: bool,
73    /// Callers only: keep just the branches that end at a method whose code
74    /// matches this (the paths from such a method to a root), and stop
75    /// there.
76    pub from: Option<Query>,
77    /// Leave out every method whose code matches one of these, with
78    /// everything reached only through it.
79    pub unless: Vec<Query>,
80}
81
82impl CallQuery {
83    /// Callers or callees of the overloads of `class`'s `method`: 2 levels,
84    /// at most 500 methods, static calls only.
85    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/// One line of the tree, in depth-first order.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct CallRow {
97    /// 0 for a root.
98    pub depth: usize,
99    /// Index of the parent row (`None` for a root).
100    pub parent: Option<usize>,
101    /// `Lcls;->name(params)ret`
102    pub method: String,
103    /// DEX files defining a body for it (empty when none does).
104    pub dex: Vec<String>,
105    /// Shown again: expanded at another row.
106    pub repeated: bool,
107    /// Reached through virtual dispatch: the supertype whose method this
108    /// caller invokes, or (callees) the class of the callee this overrides.
109    pub via: Option<String>,
110    /// Its code matches [`CallQuery::from`].
111    pub from: bool,
112}
113
114/// Result of [`Engine::call_graph`].
115#[derive(Debug, Clone)]
116pub struct CallTree {
117    pub direction: Direction,
118    pub depth: usize,
119    pub rows: Vec<CallRow>,
120    /// Distinct methods discovered (roots included).
121    pub methods: usize,
122    /// More callers / callees exist than `max_nodes` allowed.
123    pub truncated: bool,
124    /// Methods left out because their code matches [`CallQuery::unless`].
125    pub pruned: usize,
126    /// With [`CallQuery::from`]: the distinct `from` methods reached.
127    pub reached: Option<usize>,
128    /// DEX entries that could not be loaded or searched.
129    pub errors: Vec<String>,
130    /// What the conditions left out on purpose ([`crate::RefSummary::notes`]).
131    pub notes: Vec<String>,
132}
133
134impl CallTree {
135    /// One line per row: indentation and the depth number, an arrow
136    /// (`<-` caller, `->` callee), the signature and flags:
137    /// `[no body]` (defined in no DEX of the APK, e.g. a framework method),
138    /// `[services.jar]` (in a program of several archives: those defining
139    /// it) and `(repeated)` (expanded elsewhere in the tree).
140    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                // In a program of several archives: which defines it.
154                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    /// Footer comment summarising the walk.
178    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
197/// `Lcls;->name(params)ret` split into its parts.
198struct 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
210/// Method ids of `dex` declared by `class` named `name`, with the proto
211/// `proto` when given. `method_id`s are sorted by (class, name, proto).
212fn 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
239/// Subclasses checked for overrides of one callee (`virtual_calls`).
240const MAX_OVERRIDE_SCAN: usize = 4096;
241/// Supertypes whose signature stands for a frontier method (`virtual_calls`).
242const MAX_SUPERS: usize = 256;
243
244/// What one DEX says about one frontier method.
245#[derive(Default)]
246struct Found {
247    /// Neighbours (callers or callees), as signatures.
248    next: BTreeSet<String>,
249    /// DEX files with a body for the method.
250    bodies: BTreeSet<String>,
251}
252
253impl Engine {
254    /// Callers or callees of a method, `depth` levels deep, as a tree.
255    ///
256    /// The roots are every overload of `method` declared by `class` (or the
257    /// exact signature) that some DEX references; an unknown method is an
258    /// error. Each method is expanded once, at its shallowest occurrence;
259    /// other occurrences are marked `repeated`.
260    pub fn call_graph(&self, q: &CallQuery) -> Result<CallTree> {
261        self.call_graph_cancellable(q, &NOT_CANCELLED)
262    }
263
264    /// [`Engine::call_graph`] that stops, with `ApkError::Cancelled`, once
265    /// `cancel` is raised.
266    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        // Every condition (roots, from, unless) answered in one pass.
272        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        // The conditions read every DEX, and the walk reads them again.
278        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        // The tables built before a cancellation stay charged to the cache.
290        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        // Breadth-first: `depth_of` is each method's shallowest level, `next`
306        // its neighbours, `bodies` where it is defined.
307        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        // (parent, child) -> the class virtual dispatch went through
311        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            // The last level is only looked up (for `bodies`), not expanded.
319            let expand = level < q.depth;
320            // Callers through a supertype: look its signatures up too.
321            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                // A `from` method ends its paths.
338                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                // A direct edge wins over the same edge through dispatch.
360                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        // Depth-first rendering, expanding each method once at its BFS depth.
410        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    /// Every overload of `class`'s `method` (or that exact signature) some
439    /// DEX knows; an unknown method is an error.
440    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    /// Look every frontier method up in every DEX (in parallel) and collect
464    /// where it has a body and, when `expand`, its neighbours.
465    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
507/// The methods on a path to one of `ends` along the `next` edges (`ends`
508/// included); the graph may have cycles.
509fn 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
528/// For each frontier method (not a constructor / initializer), the same
529/// signature on each supertype of its class: `sig -> [(alias, supertype)]`.
530fn 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
546/// Methods overriding `sig` in subclasses of its class (a constructor /
547/// initializer has none), and whether the subclass scan was cut short.
548fn 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    // Building the graph walks every body; skip it for a DEX that defines and
571    // references none of the frontier.
572    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}