Skip to main content

asc_engine/
hierarchy.rs

1//! Class hierarchy across every DEX of an APK: transitive subclasses /
2//! implementers, supertypes, and which of them define (override) a method.
3//!
4//! The graph is built from the `class_def_item`s of every DEX (superclass and
5//! interface list per class, interned descriptors, edges sorted both ways,
6//! the class annotations of each class) and kept on the engine for later
7//! queries until the APK changes or [`Engine::release_memory_cache`] drops
8//! it. With a disk cache it is also persisted to L2 ([`GRAPH_KIND`]), so a
9//! later process maps it back instead of reading every class definition. A type the APK only references
10//! (a framework class) is a node without a definition: its own supertypes are
11//! unknown.
12
13use crate::resources::{FileIdent, file_ident};
14use crate::{DexView, Engine, EngineError, Job, NOT_CANCELLED, Result, WriteJob, cancelled, names};
15use asc_cache::{LockExt, Tier};
16use asc_dex::Dex;
17use asc_findrefs::ClassRelation;
18use std::collections::{HashMap, HashSet, VecDeque};
19use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20use std::sync::{Arc, Mutex};
21use std::time::Instant;
22
23mod packed;
24
25use packed::{Builder, Packed};
26
27/// No `superclass_idx` (`java.lang.Object` itself).
28const NO_INDEX: u32 = u32::MAX;
29
30/// L2 kind of the persisted class graph, stored under [`GRAPH_NAME`].
31pub(crate) const GRAPH_KIND: &str = "classgraph";
32/// The graph spans the whole APK (or program): one per APK key.
33pub(crate) const GRAPH_NAME: &str = "apk";
34/// Layout version of [`ClassGraph::bytes`]; bump on any change.
35pub(crate) const GRAPH_FORMAT: u32 = 1;
36
37/// Which way to walk the hierarchy.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Lineage {
40    /// Classes extending / implementing the target, transitively.
41    Subclasses,
42    /// The target's superclasses and interfaces, transitively.
43    Supers,
44}
45
46impl Lineage {
47    /// `subclasses` or `supers`, as in the query syntax and output.
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Lineage::Subclasses => "subclasses",
51            Lineage::Supers => "supers",
52        }
53    }
54
55    /// The word before a class reached through `rel`.
56    fn word(self, rel: ClassRelation) -> &'static str {
57        match (self, rel) {
58            (Lineage::Subclasses, ClassRelation::Extends) => "subclass",
59            (Lineage::Subclasses, ClassRelation::Implements) => "implementer",
60            (Lineage::Supers, rel) => rel.verb(),
61        }
62    }
63}
64
65/// A hierarchy query.
66#[derive(Debug, Clone)]
67#[non_exhaustive]
68pub struct HierarchyQuery {
69    /// Dalvik descriptor (`Lcom/poc/Base;`).
70    pub class: String,
71    /// Whether to walk down to subclasses or up to supertypes.
72    pub direction: Lineage,
73    /// Levels to walk (1 = direct subclasses / supertypes).
74    pub depth: usize,
75    /// Stop discovering classes beyond this many (the root included).
76    pub max_nodes: usize,
77    /// Mark the classes that define this method: a name (every overload) or
78    /// exact `name(params)ret`.
79    pub method: Option<String>,
80}
81
82impl HierarchyQuery {
83    /// The query with [`depth`](#structfield.depth) set.
84    #[must_use]
85    pub fn depth(self, depth: usize) -> Self {
86        HierarchyQuery { depth, ..self }
87    }
88
89    /// The query with [`max_nodes`](#structfield.max_nodes) set.
90    #[must_use]
91    pub fn max_nodes(self, max_nodes: usize) -> Self {
92        HierarchyQuery { max_nodes, ..self }
93    }
94
95    /// The query with [`method`](#structfield.method) set.
96    #[must_use]
97    pub fn method(self, method: Option<String>) -> Self {
98        HierarchyQuery { method, ..self }
99    }
100}
101
102impl HierarchyQuery {
103    /// A walk from `class` (a Dalvik descriptor): 16 levels, at most 2000
104    /// classes, no method marked.
105    pub fn new(class: impl Into<String>, direction: Lineage) -> HierarchyQuery {
106        HierarchyQuery { class: class.into(), direction, depth: 16, max_nodes: 2000, method: None }
107    }
108}
109
110/// One line of the tree, in depth-first order.
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct HierarchyRow {
114    /// 0 for the root.
115    pub depth: usize,
116    /// Index of the parent row (`None` for the root).
117    pub parent: Option<usize>,
118    /// The class descriptor.
119    pub class: String,
120    /// How the row relates to its parent row (`extends` / `implements`, from
121    /// the subclass's side).
122    pub relation: Option<ClassRelation>,
123    /// The DEX defining the class; `None` when the APK only references it.
124    pub dex: Option<String>,
125    /// Shown again: expanded at another row.
126    pub repeated: bool,
127    /// Signatures (`name(params)ret`) of the queried method this class defines.
128    pub defines: Vec<String>,
129}
130
131/// Result of [`Engine::hierarchy`].
132#[derive(Debug, Clone)]
133#[non_exhaustive]
134pub struct HierarchyTree {
135    /// The direction walked.
136    pub direction: Lineage,
137    /// The depth asked for.
138    pub depth: usize,
139    /// The tree, one row per line in depth-first order.
140    pub rows: Vec<HierarchyRow>,
141    /// Distinct classes discovered (the root included).
142    pub classes: usize,
143    /// More classes exist than `max_nodes` allowed.
144    pub truncated: bool,
145    /// The queried method, as given.
146    pub method: Option<String>,
147    /// DEX entries that could not be loaded or read.
148    pub errors: Vec<String>,
149}
150
151impl HierarchyTree {
152    /// One line per row: indentation and depth, how it relates to its parent
153    /// (`subclass` / `implementer` going down, `extends` / `implements` going
154    /// up), the class, its DEX or `[not in APK]`, `[defines m(..)V]` and
155    /// `(repeated)` flags.
156    pub fn lines(&self) -> Vec<String> {
157        self.rows
158            .iter()
159            .map(|r| {
160                let mut s = "  ".repeat(r.depth);
161                match r.relation {
162                    Some(rel) if r.depth > 0 => s.push_str(&format!("{} {} {}", r.depth, self.direction.word(rel), r.class)),
163                    _ => s.push_str(&format!("0 {}", r.class)),
164                }
165                match &r.dex {
166                    Some(d) => s.push_str(&format!(" {d}")),
167                    None => s.push_str(" [not in APK]"),
168                }
169                for d in &r.defines {
170                    s.push_str(&format!(" [defines {d}]"));
171                }
172                if r.repeated {
173                    s.push_str(" (repeated)");
174                }
175                s
176            })
177            .collect()
178    }
179
180    /// `Lcls;->m(..)V dex` of every definition of the queried method below
181    /// (or above) the root, one per line, prefixed `override` / `inherited`.
182    pub fn definition_lines(&self) -> Vec<String> {
183        let word = match self.direction {
184            Lineage::Subclasses => "override",
185            Lineage::Supers => "inherited",
186        };
187        let mut seen = HashSet::new();
188        let mut out = Vec::new();
189        for r in self.rows.iter().filter(|r| r.depth > 0 && seen.insert(&r.class)) {
190            for d in &r.defines {
191                out.push(format!("{word} {}->{d} {}", r.class, r.dex.as_deref().unwrap_or("-")));
192            }
193        }
194        out
195    }
196
197    /// Footer comment summarising the walk.
198    pub fn footer(&self) -> String {
199        let mut s =
200            format!("# {} rows, {} distinct classes ({}, depth {})", self.rows.len(), self.classes, self.direction.as_str(), self.depth);
201        if let Some(m) = &self.method {
202            let n = self.definition_lines().len();
203            s.push_str(&format!("; {n} define {m}"));
204        }
205        if self.truncated {
206            s.push_str("; truncated (raise max_nodes or lower depth)");
207        }
208        s
209    }
210}
211
212/// Every class the APK defines or references through a class definition,
213/// with the edges between them and the class annotations of each.
214pub(crate) struct ClassGraph(Packed);
215
216/// One class definition of a DEX, by descriptor.
217struct DefinedClass {
218    class: String,
219    superclass: Option<String>,
220    interfaces: Vec<String>,
221    /// Its index in the DEX's `class_defs`.
222    def_idx: u32,
223    /// Descriptors of its class annotations (any visibility).
224    annotations: Vec<String>,
225}
226
227fn dex_classes(v: &DexView) -> Result<Vec<DefinedClass>> {
228    let dex = Dex::with_name(&v.data, &v.name)?;
229    let mut out = Vec::with_capacity(dex.class_def_count() as usize);
230    for i in 0..dex.class_def_count() {
231        let def = dex.class_def(i)?;
232        let sup = (def.superclass_idx != NO_INDEX).then(|| dex.type_descriptor(def.superclass_idx)).transpose()?;
233        let ifaces = if def.interfaces_off == 0 {
234            Vec::new()
235        } else {
236            dex.type_list(def.interfaces_off)?.into_iter().map(|t| dex.type_descriptor(t as u32)).collect::<std::result::Result<_, _>>()?
237        };
238        let annotations = class_annotations(&dex, def.annotations_off)?;
239        out.push(DefinedClass { class: dex.type_descriptor(def.class_idx)?, superclass: sup, interfaces: ifaces, def_idx: i, annotations });
240    }
241    Ok(out)
242}
243
244/// Descriptors of the class annotations of the `annotations_directory_item`
245/// at `off` (0: none). Only the directory's first word is read: the member
246/// annotations it lists are left alone.
247fn class_annotations(dex: &Dex<'_>, off: u32) -> Result<Vec<String>> {
248    if off == 0 {
249        return Ok(Vec::new());
250    }
251    let buf = dex.buf();
252    let set_off = buf
253        .get(off as usize..off as usize + 4)
254        .map(|b| u32::from_le_bytes(b.try_into().expect("4 bytes")))
255        .ok_or(asc_dex::DexError::BadRange { what: "annotations_directory_item", offset: off as usize })?;
256    if set_off == 0 {
257        return Ok(Vec::new());
258    }
259    let mut out = Vec::new();
260    for item_off in asc_dex::annotations::parse_annotation_set(buf, set_off as usize)? {
261        let (item, _) = asc_dex::encoded::parse_annotation_item(buf, item_off as usize)?;
262        out.push(dex.type_descriptor(item.annotation.type_idx)?);
263    }
264    Ok(out)
265}
266
267impl ClassGraph {
268    /// Heap held by the graph, roughly: none once mapped from L2.
269    pub(crate) fn heap_bytes(&self) -> usize {
270        self.0.heap_bytes()
271    }
272
273    /// `None` once `cancel` is raised.
274    fn build(views: &[DexView], threads: usize, errors: &mut Vec<String>, cancel: &AtomicBool) -> Option<ClassGraph> {
275        let next = AtomicUsize::new(0);
276        let parsed: Mutex<Vec<(usize, Vec<DefinedClass>)>> = Mutex::new(Vec::new());
277        let errs = Mutex::new(Vec::new());
278        std::thread::scope(|s| {
279            for _ in 0..threads.min(views.len()).max(1) {
280                s.spawn(|| {
281                    loop {
282                        if cancel.load(Ordering::Relaxed) {
283                            break;
284                        }
285                        let i = next.fetch_add(1, Ordering::Relaxed);
286                        let Some(v) = views.get(i) else { break };
287                        match dex_classes(v) {
288                            Ok(c) => parsed.lock().expect("parsed classes mutex poisoned").push((i, c)),
289                            Err(e) => errs.lock().expect("errors mutex poisoned").push(format!("{}: {e}", v.name)),
290                        }
291                    }
292                });
293            }
294        });
295        if cancel.load(Ordering::Relaxed) {
296            return None;
297        }
298        errors.extend(errs.into_inner().expect("errors mutex poisoned"));
299        let mut parsed = parsed.into_inner().expect("parsed classes mutex poisoned");
300        // Views in entry order, so "first definition" means the first DEX.
301        parsed.sort_by_key(|(i, _)| *i);
302        let mut b = Builder::default();
303        for (view, classes) in parsed {
304            for DefinedClass { class, superclass, interfaces, def_idx, annotations } in classes {
305                let c = b.intern(&class);
306                if !b.define(c, view as u32, def_idx) {
307                    continue; // a duplicate definition: the first one wins
308                }
309                for a in annotations {
310                    b.annotate(c, a);
311                }
312                let parents =
313                    superclass.iter().map(|s| (s, ClassRelation::Extends)).chain(interfaces.iter().map(|i| (i, ClassRelation::Implements)));
314                for (p, rel) in parents {
315                    let p = b.intern(p);
316                    b.edge(c, p, rel);
317                }
318            }
319        }
320        Some(ClassGraph(b.finish(views.iter().map(|v| v.name.as_str()))))
321    }
322
323    /// The graph as persisted in L2 (read back by [`ClassGraph::from_mapped`]).
324    pub(crate) fn bytes(&self) -> &[u8] {
325        self.0.bytes()
326    }
327
328    /// A graph [`ClassGraph::bytes`] stored, used in place; `None` for bytes
329    /// that are not one.
330    fn from_mapped(d: asc_cache::Derived) -> Option<ClassGraph> {
331        Packed::from_mapped(d).map(ClassGraph)
332    }
333
334    fn id(&self, name: &str) -> Option<u32> {
335        self.0.id(name)
336    }
337
338    fn name(&self, id: u32) -> &str {
339        self.0.name(id)
340    }
341
342    /// Neighbours of `id` one level up or down.
343    fn neighbours(&self, id: u32, dir: Lineage) -> impl Iterator<Item = (u32, ClassRelation)> + '_ {
344        self.0.edges_of(id, dir == Lineage::Subclasses).map(move |(c, p, rel)| match dir {
345            Lineage::Supers => (p, rel),
346            Lineage::Subclasses => (c, rel),
347        })
348    }
349
350    /// Every class reachable from `class` going `dir` (not `class` itself),
351    /// nearest first; at most `limit`.
352    pub(crate) fn reachable(&self, class: &str, dir: Lineage, limit: usize) -> Vec<String> {
353        let Some(root) = self.id(class) else { return Vec::new() };
354        let mut seen = HashSet::from([root]);
355        let mut queue = VecDeque::from([root]);
356        let mut out = Vec::new();
357        while let Some(c) = queue.pop_front() {
358            for (n, _) in self.neighbours(c, dir) {
359                if out.len() >= limit {
360                    return out;
361                }
362                if seen.insert(n) {
363                    out.push(self.name(n).to_string());
364                    queue.push_back(n);
365                }
366            }
367        }
368        out
369    }
370
371    fn dex_of(&self, id: u32) -> Option<&str> {
372        let (view, _) = self.0.defined(id);
373        (view != 0).then(|| self.0.dex_name(view as usize - 1))
374    }
375
376    /// Descriptors of the classes this APK defines whose descriptor matches
377    /// `re` (for `in:{type:...}`).
378    pub(crate) fn defined_classes_matching(&self, re: &regex::bytes::Regex) -> Vec<String> {
379        (0..self.0.classes() as u32)
380            .filter(|&id| self.0.defined(id).0 != 0)
381            .map(|id| self.name(id))
382            .filter(|name| re.is_match(name.as_bytes()))
383            .map(String::from)
384            .collect()
385    }
386
387    /// Descriptors of the classes carrying the class annotation `annotation`
388    /// (for `in:{annotation:...}`), sorted.
389    pub(crate) fn classes_annotated(&self, annotation: &str) -> Vec<String> {
390        let mut out: Vec<String> = self.0.annotated(annotation).map(|id| self.name(id).to_string()).collect();
391        out.sort_unstable();
392        out
393    }
394
395    /// `name(params)ret` of the methods `class` defines whose name is `name`
396    /// (and proto `proto` when given).
397    pub(crate) fn defines(&self, views: &[DexView], class: &str, name: &str, proto: Option<&str>) -> Result<Vec<String>> {
398        let Some(id) = self.id(class) else { return Ok(Vec::new()) };
399        let (view, def_idx) = self.0.defined(id);
400        if view == 0 {
401            return Ok(Vec::new());
402        }
403        let v = &views[view as usize - 1];
404        let dex = Dex::with_name(&v.data, &v.name)?;
405        let def = dex.class_def(def_idx)?;
406        let data = dex.class_data(def.class_data_off)?;
407        let mut out = Vec::new();
408        for m in data.direct_methods.iter().chain(&data.virtual_methods) {
409            let mid = dex.method_id(m.method_idx)?;
410            if dex.string_bytes(mid.name_idx)? != name.as_bytes() {
411                continue;
412            }
413            let p = asc_smali::proto_descriptor(&dex, mid.proto_idx as u32)?;
414            if proto.is_none_or(|want| names::proto_matches(&p, want)) {
415                out.push(format!("{name}{p}"));
416            }
417        }
418        Ok(out)
419    }
420
421    /// Where the program first defines `class`: `(view index, class_def
422    /// index)`; `None` when it does not.
423    pub(crate) fn definition(&self, class: &str) -> Option<(usize, u32)> {
424        let (view, def_idx) = self.0.defined(self.id(class)?);
425        (view != 0).then(|| (view as usize - 1, def_idx))
426    }
427
428    /// The classes of `class`'s subtree (itself included) `defines` says
429    /// define the method: those a call naming it on `class` may run. The
430    /// walk stops after `max_classes` classes.
431    pub(crate) fn defining(&self, class: &str, max_classes: usize, mut defines: impl FnMut(&str) -> Result<bool>) -> Result<Defining> {
432        let Some(root) = self.id(class) else { return Ok(Defining { count: 0, complete: true }) };
433        let mut seen = HashSet::from([root]);
434        let mut queue = VecDeque::from([root]);
435        let mut count = usize::from(defines(class)?);
436        while let Some(c) = queue.pop_front() {
437            for (n, _) in self.neighbours(c, Lineage::Subclasses) {
438                if seen.len() >= max_classes {
439                    return Ok(Defining { count, complete: false });
440                }
441                if seen.insert(n) {
442                    count += usize::from(defines(self.name(n))?);
443                    queue.push_back(n);
444                }
445            }
446        }
447        Ok(Defining { count, complete: true })
448    }
449
450    /// `class` and the subclasses that inherit the method from it: a subtree
451    /// stops at a class `defines` says defines it itself. Nearest first, at
452    /// most `limit`.
453    pub(crate) fn inheritors(&self, class: &str, limit: usize, mut defines: impl FnMut(&str) -> Result<bool>) -> Result<Vec<String>> {
454        let Some(root) = self.id(class) else { return Ok(Vec::new()) };
455        let mut seen = HashSet::from([root]);
456        let mut queue = VecDeque::from([root]);
457        let mut out = vec![class.to_string()];
458        while let Some(c) = queue.pop_front() {
459            for (n, _) in self.neighbours(c, Lineage::Subclasses) {
460                if out.len() >= limit {
461                    return Ok(out);
462                }
463                if seen.insert(n) && !defines(self.name(n))? {
464                    out.push(self.name(n).to_string());
465                    queue.push_back(n);
466                }
467            }
468        }
469        Ok(out)
470    }
471}
472
473/// The classes defining a method in a subtree, as [`ClassGraph::defining`]
474/// counted them.
475#[derive(Debug, Clone, Copy)]
476pub(crate) struct Defining {
477    pub count: usize,
478    /// The whole subtree was walked: `count` is exact, not a lower bound.
479    pub complete: bool,
480}
481
482/// The cached graph and the APK file it was built from.
483pub(crate) type CachedGraph = (Option<FileIdent>, Arc<ClassGraph>);
484
485/// `name` or `name(params)ret` split into name and normalised proto. A
486/// signature without return type (`run(int)`, `entry(com.a.Outer.Inner)`)
487/// gives a `(params)` proto, matched as a prefix ([`names::proto_matches`]).
488pub(crate) fn split_method(method: &str) -> Result<(String, Option<String>)> {
489    match method.find('(') {
490        Some(_) => match asc_smali::normalize_method_signature(method) {
491            Ok(sig) => {
492                let p = sig.find('(').expect("normalised signature has a paren");
493                Ok((sig[..p].to_string(), Some(sig[p..].to_string())))
494            }
495            Err(e) => names::partial_proto(method).map(|(n, proto)| (n, Some(proto))).ok_or_else(|| e.into()),
496        },
497        None => Ok((method.to_string(), None)),
498    }
499}
500
501impl Engine {
502    /// The class graph of the APK, built over `views` unless one built from
503    /// the same APK file is kept, in memory or in L2. A cancelled build is
504    /// not kept; one that met unreadable DEX files is not persisted (a later
505    /// process should report them too).
506    pub(crate) fn class_graph(&self, views: &[DexView], errors: &mut Vec<String>, cancel: &AtomicBool) -> Result<Arc<ClassGraph>> {
507        let apk = self.open()?;
508        let ident = file_ident(&apk);
509        let kept = || {
510            let slot = self.class_graph.lock_or_default();
511            slot.as_ref().filter(|(id, _)| ident.is_some() && *id == ident).map(|(_, g)| Arc::clone(g))
512        };
513        if let Some(g) = kept() {
514            return Ok(g);
515        }
516        // One build at a time, so concurrent first calls share it instead of
517        // each holding a graph's heap. The slot's own lock is not held across
518        // the build: a panic in it (the DEX is untrusted) must not poison the
519        // graph slot that `resident_bytes` reads after every request. This
520        // lock guards no data, so its poison means nothing.
521        let _building = self.graph_build.lock_or_default();
522        if let Some(g) = kept() {
523            return Ok(g);
524        }
525        let install = |g: &Arc<ClassGraph>| *self.class_graph.lock_or_default() = Some((ident, Arc::clone(g)));
526        let disk = self.cache.as_ref().and_then(|c| c.disk.as_deref()).filter(|d| d.uses(Tier::Derived));
527        let key = disk.and_then(|_| self.apk_key(&apk));
528        if let (Some(disk), Some(key)) = (disk, &key) {
529            let stored = disk.get_derived(key, GRAPH_NAME, GRAPH_KIND, GRAPH_FORMAT).and_then(ClassGraph::from_mapped);
530            // The key covers the archives; the DEX names pin the view order
531            // the definitions index.
532            if let Some(g) = stored.filter(|g| g.0.dex_names().eq(views.iter().map(|v| v.name.as_str()))) {
533                self.stats.derived_hits.fetch_add(1, Ordering::Relaxed);
534                let g = Arc::new(g);
535                install(&g);
536                return Ok(g);
537            }
538        }
539        let t0 = Instant::now();
540        let errors_before = errors.len();
541        let g = Arc::new(ClassGraph::build(views, self.threads(), errors, cancel).ok_or_else(cancelled)?);
542        let cost = t0.elapsed();
543        if let (Some(disk), Some(key)) = (disk, key)
544            && errors.len() == errors_before
545            && disk.admits_derived(cost)
546        {
547            let job = WriteJob { key, apk: Arc::from(apk.path()), kind: Job::Graph { graph: g.clone(), cost } };
548            self.cache.as_ref().expect("a disk implies a cache").put_async(job);
549        }
550        install(&g);
551        Ok(g)
552    }
553
554    /// Transitive subclasses / implementers (or supertypes) of a class, as a
555    /// tree, with the classes defining `method` marked.
556    ///
557    /// # Errors
558    ///
559    /// [`EngineError::Query`] when no DEX defines or references the class,
560    /// [`EngineError::Smali`] for a malformed method signature, and
561    /// [`EngineError::Apk`] when the APK cannot be opened.
562    pub fn hierarchy(&self, q: &HierarchyQuery) -> Result<HierarchyTree> {
563        self.hierarchy_cancellable(q, &NOT_CANCELLED)
564    }
565
566    /// [`Engine::hierarchy`] that stops, with `ApkError::Cancelled`, once
567    /// `cancel` is raised.
568    ///
569    /// # Errors
570    ///
571    /// As [`Engine::hierarchy`], plus the cancellation.
572    pub fn hierarchy_cancellable(&self, q: &HierarchyQuery, cancel: &AtomicBool) -> Result<HierarchyTree> {
573        let method = q.method.as_deref().map(split_method).transpose()?;
574        let (views, mut errors) = self.load_views(None, cancel)?;
575        let g = self.class_graph(&views, &mut errors, cancel)?;
576        // A class written as Java / Kotlin source spells it (`com.a.Outer.Inner`)
577        // is looked up as the DEX class it stands for when only that exists.
578        let Some(root) = g.id(&q.class).or_else(|| names::denormalize_class_candidates(&q.class).iter().find_map(|c| g.id(c))) else {
579            return Err(EngineError::Query(format!("no class {} in this APK", q.class)));
580        };
581        // Breadth-first: each class's shallowest depth and its children.
582        let max_nodes = q.max_nodes.max(1);
583        let mut depth_of: HashMap<u32, usize> = HashMap::from([(root, 0)]);
584        let mut children: HashMap<u32, Vec<(u32, ClassRelation)>> = HashMap::new();
585        let mut frontier = vec![root];
586        let mut truncated = false;
587        for level in 0..q.depth {
588            let mut following = Vec::new();
589            for &c in &frontier {
590                let mut kept = Vec::new();
591                for (n, rel) in g.neighbours(c, q.direction) {
592                    if depth_of.contains_key(&n) {
593                        kept.push((n, rel));
594                    } else if depth_of.len() < max_nodes {
595                        depth_of.insert(n, level + 1);
596                        following.push(n);
597                        kept.push((n, rel));
598                    } else {
599                        truncated = true;
600                    }
601                }
602                children.insert(c, kept);
603            }
604            if following.is_empty() {
605                break;
606            }
607            frontier = following;
608        }
609        // Classes at the last level may still have neighbours.
610        truncated |= frontier.iter().any(|&c| !children.contains_key(&c) && g.neighbours(c, q.direction).next().is_some());
611
612        let mut defines: HashMap<u32, Vec<String>> = HashMap::new();
613        if let Some((name, proto)) = &method {
614            for &c in depth_of.keys() {
615                if cancel.load(Ordering::Relaxed) {
616                    return Err(cancelled());
617                }
618                match g.defines(&views, g.name(c), name, proto.as_deref()) {
619                    Ok(d) if !d.is_empty() => {
620                        defines.insert(c, d);
621                    }
622                    Ok(_) => {}
623                    Err(e) => errors.push(format!("{}: {e}", g.name(c))),
624                }
625            }
626            // Defined nowhere as written: the first reading of the method as
627            // decompiled source spells it (`Cdo`, `m$2`, the class name for a
628            // constructor) that some class of the tree defines.
629            if defines.is_empty()
630                && let Some(raw) = q.method.as_deref()
631            {
632                for cand in names::denormalize_method_candidates(Some(g.name(root)), raw) {
633                    let Ok((name, proto)) = split_method(&cand) else { continue };
634                    for &c in depth_of.keys() {
635                        if let Ok(d) = g.defines(&views, g.name(c), &name, proto.as_deref())
636                            && !d.is_empty()
637                        {
638                            defines.insert(c, d);
639                        }
640                    }
641                    if !defines.is_empty() {
642                        break;
643                    }
644                }
645            }
646        }
647
648        // Depth-first rendering, expanding each class once at its BFS depth.
649        let mut rows = Vec::new();
650        let mut expanded = HashSet::new();
651        let mut stack: Vec<(u32, usize, Option<usize>, Option<ClassRelation>)> = vec![(root, 0, None, None)];
652        while let Some((c, depth, parent, relation)) = stack.pop() {
653            let expand_here = depth_of.get(&c) == Some(&depth) && !expanded.contains(&c);
654            let has_children = children.get(&c).is_some_and(|n| !n.is_empty());
655            rows.push(HierarchyRow {
656                depth,
657                parent,
658                class: g.name(c).to_string(),
659                relation,
660                dex: g.dex_of(c).map(String::from),
661                repeated: !expand_here && has_children,
662                defines: defines.get(&c).cloned().unwrap_or_default(),
663            });
664            if expand_here {
665                expanded.insert(c);
666                let me = rows.len() - 1;
667                let mut kids: Vec<_> = children.get(&c).cloned().unwrap_or_default();
668                kids.sort_by(|a, b| g.name(b.0).cmp(g.name(a.0)));
669                for (n, rel) in kids {
670                    stack.push((n, depth + 1, Some(me), Some(rel)));
671                }
672            }
673        }
674        errors.sort();
675        errors.dedup();
676        Ok(HierarchyTree {
677            direction: q.direction,
678            depth: q.depth,
679            rows,
680            classes: depth_of.len(),
681            truncated,
682            method: q.method.clone(),
683            errors,
684        })
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use std::collections::BTreeSet;
692
693    /// `B extends A implements I`, `C extends B` (in the second DEX), `D`
694    /// with a non-ASCII name; `B` and `C` carry `@M`; `I` is only referenced.
695    fn sample() -> ClassGraph {
696        let mut b = Builder::default();
697        let [a, bb, c, i, d] = ["LA;", "LB;", "LC;", "LI;", "Lé;"].map(|n| b.intern(n));
698        assert!(b.define(a, 0, 0) && b.define(bb, 0, 1) && b.define(c, 1, 0) && b.define(d, 0, 2));
699        assert!(!b.define(c, 0, 9), "the first definition wins");
700        b.edge(bb, a, ClassRelation::Extends);
701        b.edge(bb, i, ClassRelation::Implements);
702        b.edge(c, bb, ClassRelation::Extends);
703        b.edge(c, bb, ClassRelation::Extends);
704        b.annotate(c, "LM;".into());
705        b.annotate(bb, "LM;".into());
706        b.annotate(bb, "LN;".into());
707        ClassGraph(b.finish(["classes.dex", "lib.jar/classes.dex"].into_iter()))
708    }
709
710    fn check(g: &ClassGraph) {
711        assert_eq!(g.reachable("LA;", Lineage::Subclasses, 10), ["LB;", "LC;"]);
712        assert_eq!(g.reachable("LC;", Lineage::Supers, 10), ["LB;", "LA;", "LI;"]);
713        assert_eq!(g.reachable("LC;", Lineage::Supers, 2), ["LB;", "LA;"]);
714        assert!(g.reachable("LZ;", Lineage::Supers, 10).is_empty());
715        assert_eq!(g.classes_annotated("LM;"), ["LB;", "LC;"]);
716        assert_eq!(g.classes_annotated("LN;"), ["LB;"]);
717        assert!(g.classes_annotated("LL;").is_empty() && g.classes_annotated("LZ;").is_empty());
718        assert_eq!(g.dex_of(g.id("LC;").unwrap()), Some("lib.jar/classes.dex"));
719        assert_eq!(g.dex_of(g.id("LI;").unwrap()), None);
720        assert_eq!(g.name(g.id("Lé;").unwrap()), "Lé;");
721        let re = regex::bytes::Regex::new("^L[A-C];$").unwrap();
722        assert_eq!(
723            g.defined_classes_matching(&re).into_iter().collect::<BTreeSet<_>>(),
724            BTreeSet::from(["LA;".into(), "LB;".into(), "LC;".into()])
725        );
726        assert_eq!(g.0.dex_names().collect::<Vec<_>>(), ["classes.dex", "lib.jar/classes.dex"]);
727    }
728
729    /// An L2 directory to hand bytes back as the disk cache would.
730    struct Store {
731        _dir: tempfile::TempDir,
732        disk: asc_cache::DiskCache,
733        key: asc_cache::ApkKey,
734    }
735
736    impl Store {
737        fn new() -> Store {
738            let dir = tempfile::tempdir().unwrap();
739            let disk = asc_cache::DiskCache::open(dir.path().join("cache")).with_capacity(1 << 30);
740            let apk = dir.path().join("a.apk");
741            std::fs::write(&apk, asc_fixture::make_apk(&[("classes.dex", &asc_fixture::make_rich_dex())], true)).unwrap();
742            let key = asc_cache::ApkKey::for_apk(&asc_apk::Apk::open(&apk).unwrap()).unwrap();
743            Store { _dir: dir, disk, key }
744        }
745
746        fn mapped(&self, bytes: &[u8]) -> Option<ClassGraph> {
747            self.disk.put_derived(&self.key, GRAPH_NAME, GRAPH_KIND, GRAPH_FORMAT, std::time::Duration::ZERO, bytes).unwrap();
748            ClassGraph::from_mapped(self.disk.get_derived(&self.key, GRAPH_NAME, GRAPH_KIND, GRAPH_FORMAT).unwrap())
749        }
750    }
751
752    #[test]
753    fn graphs_answer_alike_built_and_mapped() {
754        let store = Store::new();
755        let g = sample();
756        check(&g);
757        let back = store.mapped(g.bytes()).unwrap();
758        check(&back);
759        assert_eq!(back.heap_bytes(), 0);
760        assert!(g.heap_bytes() >= g.bytes().len());
761        // No classes at all.
762        let empty = ClassGraph(Builder::default().finish(std::iter::empty()));
763        assert!(store.mapped(empty.bytes()).unwrap().reachable("LA;", Lineage::Supers, 1).is_empty());
764        // Enough classes to grow the table, all still found.
765        let mut b = Builder::default();
766        let names: Vec<String> = (0..5000).map(|i| format!("Lc{i};")).collect();
767        let ids: Vec<u32> = names.iter().map(|n| b.intern(n)).collect();
768        assert_eq!(names.iter().map(|n| b.intern(n)).collect::<Vec<_>>(), ids);
769        let g = store.mapped(ClassGraph(b.finish(std::iter::empty())).bytes()).unwrap();
770        assert!(names.iter().zip(&ids).all(|(n, &id)| g.id(n) == Some(id) && g.name(id) == n));
771    }
772
773    #[test]
774    fn damaged_bytes_never_index_out_of_bounds() {
775        let store = Store::new();
776        let bytes = sample().bytes().to_vec();
777        for n in 0..bytes.len() {
778            assert!(store.mapped(&bytes[..n]).is_none(), "truncated to {n}");
779        }
780        // Any single-byte change is rejected or still answers without
781        // panicking.
782        let any = regex::bytes::Regex::new(".").unwrap();
783        for at in 0..bytes.len() {
784            let mut b = bytes.clone();
785            b[at] ^= 0xa5;
786            if let Some(g) = store.mapped(&b) {
787                for name in ["LA;", "LB;", "LC;", "LI;", "Lé;"] {
788                    g.reachable(name, Lineage::Subclasses, 10);
789                    g.reachable(name, Lineage::Supers, 10);
790                    g.id(name).map(|id| g.dex_of(id));
791                }
792                g.classes_annotated("LM;");
793                g.defined_classes_matching(&any);
794            }
795        }
796    }
797
798    /// Panic while holding `lock`, as a panicking request does.
799    fn poison<T>(lock: &Mutex<T>) {
800        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
801            let _held = lock.lock();
802            panic!("a request panics under the lock");
803        }));
804        assert!(caught.is_err() && lock.is_poisoned());
805    }
806
807    #[test]
808    fn a_panic_under_a_cache_lock_costs_a_rebuild_not_the_engine() {
809        let dir = tempfile::tempdir().unwrap();
810        let apk = dir.path().join("a.apk");
811        std::fs::write(&apk, asc_fixture::make_apk(&[("classes.dex", &asc_fixture::make_rich_dex())], true)).unwrap();
812        let engine = Engine::new(&apk).with_threads(2).with_cache(crate::CacheConfig { memory_bytes: 64 << 20, ..Default::default() });
813        let q = HierarchyQuery { class: "Lexample/Custom;".into(), direction: Lineage::Supers, depth: 4, max_nodes: 100, method: None };
814        let before = engine.hierarchy(&q).unwrap().rows;
815        assert!(before.len() > 1, "{before:?}");
816        assert!(engine.resident_bytes() > 0);
817        // Every cache slot a query or `resident_bytes` takes, and the build
818        // lock a panicking graph build leaves poisoned.
819        poison(&engine.class_graph);
820        poison(&engine.graph_build);
821        poison(&engine.resources);
822        poison(&engine.opened);
823        poison(&engine.key_cache);
824        poison(engine.cache.as_ref().and_then(|c| c.memory.as_deref()).expect("a memory cache"));
825        // The poisoned slots read as empty, then fill again.
826        assert_eq!(engine.resident_bytes(), 0);
827        assert_eq!(engine.memory_cache_bytes(), 0);
828        assert_eq!(engine.hierarchy(&q).unwrap().rows, before);
829        assert!(engine.resident_bytes() > 0);
830        assert!(!engine.class_graph.is_poisoned() && !engine.graph_build.is_poisoned());
831        engine.release_memory_cache();
832        assert_eq!((engine.resident_bytes(), engine.memory_cache_bytes()), (0, 0));
833    }
834}