Skip to main content

asc_findrefs/
lib.rs

1//! Reference search over a single DEX without any global preprocessing.
2//!
3//! The pipeline mirrors the Python `findrefs` package:
4//!
5//! 1. A *locator* turns the user's query into a set of constant-pool indexes
6//!    (string, type, method or field ids). String matching runs a regex over
7//!    the raw string-data region; member lookups use small per-DEX maps.
8//! 2. The *code scanner* sweeps the raw code-item region once for opcodes that
9//!    carry an index of that kind and keeps the positions whose operand is in
10//!    the located set.
11//! 3. The *instruction locator* maps each surviving byte offset back to the
12//!    method that owns it through a dense 16-byte bucket table, and confirms
13//!    the offset really sits on an instruction boundary.
14//!
15//! String queries are a regex, or a plain substring found with SIMD `memmem`
16//! ([`Query::StringLiteral`]). `Extends` / `Implements` queries skip the
17//! pipeline: they compare `class_def_item`s with the target's type index
18//! ([`FindRefs::class_relation`]).
19//!
20//! Queries can be pre-compiled once per search ([`CompiledQuery`]) and run
21//! against many DEX files with [`FindRefs::find_ref_compiled`].
22
23#![forbid(unsafe_code)]
24
25mod callgraph;
26mod calls;
27mod classdefs;
28mod insn_locator;
29mod locators;
30mod persist;
31mod query;
32mod scanner;
33mod table;
34
35pub use callgraph::CallGraph;
36pub use calls::{CallConstraint, Certainty, ResultWant, Slot, Want};
37pub use insn_locator::{InsnLocator, MethodRef};
38pub use locators::{FieldLocator, MethodLocator, StringLocator, TypeLocator};
39pub use persist::FORMAT_VERSION as PERSIST_FORMAT_VERSION;
40pub use query::{CompiledQuery, Compound};
41pub use scanner::{CodeScanner, FieldAccess, IdxSet, RefKind};
42pub use table::Backing;
43
44use asc_dex::Dex;
45use query::Parts;
46use std::collections::{BTreeMap, BTreeSet, HashMap};
47use std::sync::{Mutex, OnceLock, PoisonError};
48
49/// Errors from reference search.
50#[derive(Debug, thiserror::Error)]
51pub enum FindError {
52    #[error(transparent)]
53    Dex(#[from] asc_dex::DexError),
54    #[error("invalid pattern: {0}")]
55    Pattern(#[from] regex::Error),
56    #[error("invalid query: {0}")]
57    Invalid(String),
58}
59
60pub type Result<T> = std::result::Result<T, FindError>;
61
62/// Class part of a member query.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct ClassQuery {
65    /// Exact Dalvik descriptor when `precise`, otherwise a fuzzy pattern.
66    pub pattern: String,
67    pub precise: bool,
68}
69
70/// Method or field query: at least one of `class` / `name` must be set.
71#[derive(Debug, Clone, PartialEq, Eq, Default)]
72pub struct MemberQuery {
73    pub class: Option<ClassQuery>,
74    /// Fuzzy member name (regex when `class` is absent or fuzzy, substring
75    /// when the class is precise), or the exact name when `exact_name` is set.
76    pub name: Option<String>,
77    /// Match `name` exactly (whatever the class constraint) instead of as a
78    /// regex / substring; used for "references to this member" lookups.
79    pub exact_name: bool,
80    /// Only the members of this signature: one overload of a method, or a
81    /// field of one type. Checked against the id tables, before any code is
82    /// scanned.
83    pub signature: Option<MemberSignature>,
84}
85
86/// The signature constraint of a [`MemberQuery`].
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum MemberSignature {
89    /// A method's parameters, and its return type when one is given.
90    Method { params: Vec<TypeMatch>, ret: Option<TypeMatch> },
91    /// A field's type.
92    Field(TypeMatch),
93}
94
95/// One type of a [`MemberSignature`].
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum TypeMatch {
98    /// Exactly this descriptor: `I`, `Ljava/lang/String;`, `[B`.
99    Descriptor(String),
100    /// A class of this simple name in any package, `dims` levels of array
101    /// deep (`String[]` written without its package).
102    Simple { name: String, dims: usize },
103}
104
105impl TypeMatch {
106    /// Whether the descriptor `descriptor` is of this type.
107    pub fn matches(&self, descriptor: &str) -> bool {
108        query::CompiledType::new(self).matches(&asc_dex::mutf8::encode(descriptor))
109    }
110}
111
112impl std::fmt::Display for TypeMatch {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            TypeMatch::Descriptor(d) => f.write_str(d),
116            TypeMatch::Simple { name, dims } => write!(f, "{name}{}", "[]".repeat(*dims)),
117        }
118    }
119}
120
121impl std::fmt::Display for MemberSignature {
122    /// `(I, String)V`, `(I)` or `:I`, the way the query reads.
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            MemberSignature::Method { params, ret } => {
126                f.write_str("(")?;
127                for (i, p) in params.iter().enumerate() {
128                    // Descriptors run together, as Dalvik writes them; a simple
129                    // name needs a separator to stay readable.
130                    let simple = |t: &TypeMatch| matches!(t, TypeMatch::Simple { .. });
131                    if i > 0 && (simple(p) || simple(&params[i - 1])) {
132                        f.write_str(", ")?;
133                    }
134                    write!(f, "{p}")?;
135                }
136                f.write_str(")")?;
137                ret.as_ref().map_or(Ok(()), |r| write!(f, "{r}"))
138            }
139            MemberSignature::Field(t) => write!(f, ":{t}"),
140        }
141    }
142}
143
144/// A reference search request.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Query {
147    /// Strings matching a regex.
148    String(String),
149    /// Strings containing a plain substring (no regex syntax).
150    StringLiteral(String),
151    Type(String),
152    Method(MemberQuery),
153    /// References to any of these methods: a set read from a file, or the
154    /// methods a [`Query::Nested`] condition resolved to.
155    MethodSet(Vec<MemberQuery>),
156    /// References to the methods whose own code matches the inner query
157    /// (`method:{RULE}`). The inner query has to see every DEX, so whoever
158    /// holds them all (the engine) resolves this into a [`Query::MethodSet`]
159    /// first; compiling it as it is fails.
160    Nested(Box<Query>),
161    /// The calls that may dispatch to a method of `target` (a `MethodSet` or
162    /// `Nested`): those naming it, and those naming the same method on a
163    /// supertype of its class or on a subclass inheriting it
164    /// (`method:{RULE}` and `method:@FILE` by default). The engine resolves
165    /// it over the class hierarchy into `target` plus those methods, `via`
166    /// mapping each added one (`Lcls;->name(params)ret`, or `Lcls;->name`
167    /// for every overload) to the classes of `target` it stands for; a hit on
168    /// one carries the note `(dispatch LIST)`.
169    Virtual {
170        target: Box<Query>,
171        via: BTreeMap<String, String>,
172        /// Most classes of the program that may define a supertype's method
173        /// for the calls naming it to be followed (`None`: no limit). A
174        /// method every lambda or callback implements says nothing about
175        /// which one runs.
176        fan_out: Option<usize>,
177    },
178    Field(MemberQuery),
179    /// Field references restricted to reads (`iget*` / `sget*`) or writes
180    /// (`iput*` / `sput*`). [`Query::Field`] keeps both.
181    FieldAccess {
182        field: MemberQuery,
183        access: FieldAccess,
184    },
185    /// The calls of a method query (`Method`, `MethodSet` or `Nested`) that
186    /// every constraint keeps (`method:X[arg1="id", ret=unused]`); see
187    /// [`CallConstraint`]. Its hits carry a note of the values judged.
188    Call {
189        target: Box<Query>,
190        constraints: Vec<CallConstraint>,
191    },
192    /// Classes whose superclass is exactly this descriptor (`Lcom/poc/Base;`).
193    /// Answered from `class_def_item`s, not by a code scan.
194    Extends(String),
195    /// Classes that directly implement exactly this interface descriptor.
196    Implements(String),
197    /// Methods matched by how they are declared, not by what their code
198    /// references (`flag:`, `annotation:`, `override:`, `in:{...}`). Answered
199    /// from the class definitions; the rows have no call site.
200    Defined(Defined),
201    /// Methods whose code matches every query of `all` and none of `none`
202    /// (string / type / method / field queries; at least one in `all`).
203    /// A row lists the constants each `all` query matched, in that order.
204    All {
205        all: Vec<Query>,
206        none: Vec<Query>,
207    },
208}
209
210impl Query {
211    /// The index table the query targets; class relations report
212    /// [`RefKind::Type`] (their results are classes).
213    pub fn kind(&self) -> RefKind {
214        match self {
215            Query::String(_) | Query::StringLiteral(_) => RefKind::String,
216            Query::Type(_) | Query::Extends(_) | Query::Implements(_) => RefKind::Type,
217            Query::Method(_) | Query::MethodSet(_) | Query::Nested(_) | Query::Virtual { .. } | Query::Call { .. } | Query::Defined(_) => {
218                RefKind::Method
219            }
220            Query::Field(_) | Query::FieldAccess { .. } => RefKind::Field,
221            // Rows of a compound query list constants of several kinds.
222            Query::All { all, .. } => all.first().map_or(RefKind::String, Query::kind),
223        }
224    }
225
226    /// The class relation of an `Extends` / `Implements` query.
227    pub fn relation(&self) -> Option<ClassRelation> {
228        match self {
229            Query::Extends(_) => Some(ClassRelation::Extends),
230            Query::Implements(_) => Some(ClassRelation::Implements),
231            _ => None,
232        }
233    }
234
235    /// The read/write restriction of a field query, if any.
236    pub fn field_access(&self) -> Option<FieldAccess> {
237        match self {
238            Query::FieldAccess { access, .. } => Some(*access),
239            _ => None,
240        }
241    }
242}
243
244/// A method-definition predicate ([`Query::Defined`]).
245///
246/// `Flags` and `Annotation` are matched against each method's own
247/// declaration. `Override` and `In` need the class hierarchy (which spans
248/// every DEX), so the engine resolves them into `Members` — a set of
249/// declaring classes, optionally with a name and proto — before a search;
250/// compiling them unresolved fails.
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub enum Defined {
253    /// Carries all of these access flags (`flag:public,static`).
254    Flags(u32),
255    /// Annotated with this annotation type descriptor (`annotation:Lx/Y;`).
256    Annotation(String),
257    /// Declared in one of `classes` (exact descriptors), and, when set, with
258    /// this `name` and `proto` (`(params)ret`). `override:` and `in:{...}`
259    /// resolve to this.
260    Members { classes: Vec<String>, name: Option<String>, proto: Option<String> },
261    /// `override:TARGET`: methods overriding or implementing it. The engine
262    /// resolves it into `Members` over the class hierarchy.
263    Override(MemberQuery),
264    /// `in:{CLASS-RULE}`: methods declared in a matching class. The engine
265    /// resolves it into `Members`.
266    In(Box<Query>),
267}
268
269impl Defined {
270    /// A short label for the matched column of a definitional row.
271    pub fn label(&self) -> String {
272        match self {
273            Defined::Flags(_) => "flag".into(),
274            Defined::Annotation(a) => format!("@{a}"),
275            Defined::Members { name: Some(n), .. } => format!("override {n}"),
276            Defined::Members { .. } => "in".into(),
277            Defined::Override(m) => format!("override {}", m.name.as_deref().unwrap_or("")),
278            Defined::In(_) => "in".into(),
279        }
280    }
281}
282
283/// How a class relates to the target of an `Extends` / `Implements` query.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
285pub enum ClassRelation {
286    /// The class's `superclass_idx` is the target.
287    Extends,
288    /// The target is in the class's `interfaces` list.
289    Implements,
290}
291
292impl ClassRelation {
293    /// The verb of the Python output line (`Lsub; extends Lbase;`).
294    pub fn verb(self) -> &'static str {
295        match self {
296            ClassRelation::Extends => "extends",
297            ClassRelation::Implements => "implements",
298        }
299    }
300}
301
302/// One instruction that references a located index.
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct Hit {
305    /// Absolute byte offset of the instruction in the DEX.
306    pub offset: u32,
307    /// The constant-pool index the instruction references.
308    pub matched_idx: u32,
309    /// Owning method(s); `None` when the offset failed instruction verification.
310    pub methods: Option<MethodRef>,
311    /// Whether a field instruction reads or writes the field. `None` for
312    /// string, type and method references.
313    pub field_access: Option<FieldAccess>,
314    /// What the constraints of a [`Query::Call`] judged at this call site
315    /// (`[arg1="id", ret=unused]`, `?` first when one is undetermined),
316    /// and for a [`Query::Virtual`] alias, the methods it may dispatch to
317    /// (`(dispatch Lcls;)`).
318    pub note: Option<String>,
319}
320
321/// Lazily-built locator tables for one DEX buffer.
322///
323/// The tables hold offsets into the buffer they were built from (no
324/// references or pointers), so a `SearchState` must only ever be used
325/// together with that same buffer or lookups will silently return nonsense.
326/// The engine caches one per inflated DEX so repeated queries skip the builds.
327///
328/// Each table is built once: a second caller asking for a table under
329/// construction waits for it rather than building its own copy (a query
330/// arriving while a background prebuild is half way through a table gets
331/// that table sooner than by starting over).
332#[derive(Default)]
333pub struct SearchState {
334    strings: OnceLock<StringLocator>,
335    types: OnceLock<TypeLocator>,
336    methods: OnceLock<MethodLocator>,
337    fields: OnceLock<FieldLocator>,
338    insns: OnceLock<InsnLocator>,
339    calls: OnceLock<CallGraph>,
340    /// One lock per table, held while that table is built.
341    building: [Mutex<()>; 6],
342}
343
344/// `cell`'s value, built by `build` under `lock` the first time.
345fn built<'c, T>(cell: &'c OnceLock<T>, lock: &Mutex<()>, build: impl FnOnce() -> Result<T>) -> Result<&'c T> {
346    if let Some(v) = cell.get() {
347        return Ok(v);
348    }
349    let _building = lock.lock().unwrap_or_else(PoisonError::into_inner);
350    if let Some(v) = cell.get() {
351        return Ok(v);
352    }
353    let v = build()?;
354    Ok(cell.get_or_init(|| v))
355}
356
357impl SearchState {
358    pub fn new() -> Self {
359        Self::default()
360    }
361
362    /// Whether the instruction table has been built (a proxy for "warm").
363    pub fn is_warm(&self) -> bool {
364        self.insns.get().is_some()
365    }
366
367    /// Heap bytes of the tables built so far (0 for a fresh state). Grows as
368    /// queries of new kinds build their locators.
369    pub fn heap_bytes(&self) -> usize {
370        self.strings.get().map_or(0, StringLocator::heap_bytes)
371            + self.types.get().map_or(0, TypeLocator::heap_bytes)
372            + self.methods.get().map_or(0, MethodLocator::heap_bytes)
373            + self.fields.get().map_or(0, FieldLocator::heap_bytes)
374            + self.insns.get().map_or(0, InsnLocator::heap_bytes)
375            + self.calls.get().map_or(0, CallGraph::heap_bytes)
376    }
377
378    /// The call graph, if a query has built it.
379    pub fn call_graph(&self) -> Option<&CallGraph> {
380        self.calls.get()
381    }
382
383    /// Install a call graph loaded from elsewhere (e.g. an on-disk cache of
384    /// [`CallGraph::to_bytes`]); `false` if one is already built. It must
385    /// describe the same buffer this state is used with.
386    pub fn set_call_graph(&self, graph: CallGraph) -> bool {
387        self.calls.set(graph).is_ok()
388    }
389}
390
391enum StateRef<'s> {
392    Owned(Box<SearchState>),
393    Shared(&'s SearchState),
394}
395
396/// Reference search over one DEX.
397pub struct FindRefs<'d, 'a, 's> {
398    dex: &'d Dex<'a>,
399    state: StateRef<'s>,
400}
401
402impl<'d, 'a, 's> FindRefs<'d, 'a, 's> {
403    /// Search with private, throw-away locator tables.
404    pub fn new(dex: &'d Dex<'a>) -> Self {
405        FindRefs { dex, state: StateRef::Owned(Box::default()) }
406    }
407
408    /// Search reusing tables built earlier for the *same* buffer.
409    pub fn with_state(dex: &'d Dex<'a>, state: &'s SearchState) -> Self {
410        FindRefs { dex, state: StateRef::Shared(state) }
411    }
412
413    fn state(&self) -> &SearchState {
414        match &self.state {
415            StateRef::Owned(s) => s,
416            StateRef::Shared(s) => s,
417        }
418    }
419
420    pub fn dex(&self) -> &'d Dex<'a> {
421        self.dex
422    }
423
424    pub fn string_locator(&self) -> Result<&StringLocator> {
425        let st = self.state();
426        built(&st.strings, &st.building[0], || StringLocator::build(self.dex))
427    }
428
429    pub fn type_locator(&self) -> Result<&TypeLocator> {
430        let st = self.state();
431        built(&st.types, &st.building[1], || TypeLocator::build(self.dex))
432    }
433
434    pub fn method_locator(&self) -> Result<&MethodLocator> {
435        let st = self.state();
436        built(&st.methods, &st.building[2], || MethodLocator::build(self.dex))
437    }
438
439    pub fn field_locator(&self) -> Result<&FieldLocator> {
440        let st = self.state();
441        built(&st.fields, &st.building[3], || FieldLocator::build(self.dex))
442    }
443
444    pub fn insn_locator(&self) -> Result<&InsnLocator> {
445        let st = self.state();
446        built(&st.insns, &st.building[4], || Ok(InsnLocator::build(self.dex)?))
447    }
448
449    /// The DEX's call graph, built on first use (the instruction table too).
450    pub fn call_graph(&self) -> Result<&CallGraph> {
451        let st = self.state();
452        built(&st.calls, &st.building[5], || Ok(CallGraph::build(self.dex.buf(), self.insn_locator()?)))
453    }
454
455    /// Resolve the query to constant-pool indexes (compiling its patterns).
456    pub fn locate(&self, query: &Query) -> Result<BTreeSet<u32>> {
457        self.locate_compiled(&CompiledQuery::new(query)?)
458    }
459
460    /// Resolve a pre-compiled query to constant-pool indexes.
461    pub fn locate_compiled(&self, query: &CompiledQuery) -> Result<BTreeSet<u32>> {
462        let buf = self.dex.buf();
463        match (query.kind(), query.parts()) {
464            (_, Parts::Pattern(None)) | (_, Parts::Literal(None)) => Ok(BTreeSet::new()),
465            (RefKind::String, Parts::Pattern(Some(re))) => Ok(self.string_locator()?.locate(buf, re)),
466            (RefKind::String, Parts::Literal(Some(f))) => Ok(self.string_locator()?.locate_literal(buf, f)),
467            (_, Parts::Relation(..)) => Ok(self.class_relation(query)?.unwrap_or_default().into_iter().collect()),
468            (RefKind::Type, Parts::Pattern(Some(re))) => Ok(self.type_locator()?.locate(buf, self.string_locator()?, re)),
469            (_, Parts::Call(c)) => self.locate_compiled(&c.target),
470            (_, Parts::Virtual(target, _)) => self.locate_compiled(target),
471            (RefKind::Method, Parts::Member(_) | Parts::MemberSet(_)) => {
472                self.method_locator()?.locate_compiled(self.dex, self.string_locator()?, self.type_locator()?, query)
473            }
474            (RefKind::Field, Parts::Member(_)) => {
475                self.field_locator()?.locate_compiled(self.dex, self.string_locator()?, self.type_locator()?, query)
476            }
477            // `CompiledQuery::new` always pairs the kind with the matching parts.
478            _ => Ok(BTreeSet::new()),
479        }
480    }
481
482    /// Whether `query` certainly has no hit in this DEX, judged from the id
483    /// tables and the string data alone: no code is read, so this also works
484    /// on a DEX prefix that holds those (an error then means the prefix was
485    /// not enough, and the caller has to search the whole DEX).
486    ///
487    /// `true` exactly when [`FindRefs::find_ref_compiled`] (or, for a class
488    /// relation, [`FindRefs::class_relation`]; for a compound query, its rows)
489    /// could only come out empty.
490    pub fn cannot_match(&self, query: &CompiledQuery) -> Result<bool> {
491        if let Some(c) = query.compound() {
492            // Rows need a hit of every `all` part in one method.
493            for part in &c.all {
494                if self.cannot_match(part)? {
495                    return Ok(true);
496                }
497            }
498            return Ok(false);
499        }
500        match query.parts() {
501            Parts::Call(c) => self.cannot_match(&c.target),
502            Parts::Virtual(target, _) => self.cannot_match(target),
503            // A class nobody references cannot be extended or implemented.
504            Parts::Relation(_, descriptor) => Ok(self.dex.find_type_idx(descriptor)?.is_none()),
505            // A definition predicate reads no code, so locating it yields
506            // nothing: rule it out only from the type table (flags could be
507            // on any method, so they never skip).
508            Parts::Defined(Defined::Flags(_)) => Ok(false),
509            Parts::Defined(Defined::Annotation(a)) => Ok(self.dex.find_type_idx(&asc_dex::mutf8::encode(a))?.is_none()),
510            Parts::Defined(Defined::Members { classes, .. }) => {
511                for c in classes {
512                    if self.dex.find_type_idx(&asc_dex::mutf8::encode(c))?.is_some() {
513                        return Ok(false);
514                    }
515                }
516                Ok(true)
517            }
518            // Unresolved predicates never reach a search.
519            Parts::Defined(Defined::Override(_) | Defined::In(_)) => Ok(false),
520            // Nor can its members be: one binary search, before the member
521            // tables are built (most DEX files do not reference the class).
522            Parts::Member(query::CompiledMember { class: Some(query::CompiledClass::Precise(descriptor)), .. })
523                if self.dex.find_type_idx(descriptor)?.is_none() =>
524            {
525                Ok(true)
526            }
527            _ => Ok(self.locate_compiled(query)?.is_empty()),
528        }
529    }
530
531    /// Full search: locate indexes, scan code, resolve owning methods.
532    pub fn find_ref(&self, query: &Query) -> Result<Vec<Hit>> {
533        self.find_ref_compiled(&CompiledQuery::new(query)?)
534    }
535
536    /// Full search with a query compiled once (typically shared by every DEX
537    /// of an APK).
538    ///
539    /// Class relations (`Extends` / `Implements`) reference no code and give
540    /// no hits here; [`FindRefs::class_relation`] answers them. Compound
541    /// queries give none either: run their parts with [`FindRefs::find_many`].
542    pub fn find_ref_compiled(&self, query: &CompiledQuery) -> Result<Vec<Hit>> {
543        if query.relation().is_some() || query.compound().is_some() || query.defined().is_some() {
544            return Ok(Vec::new());
545        }
546        let idxs = self.locate_compiled(query)?;
547        if idxs.is_empty() {
548            return Ok(Vec::new());
549        }
550        let mut hits = self.scan_idxs(query.kind(), &idxs)?;
551        if let Some(access) = query.query().field_access() {
552            hits.retain(|hit| hit.field_access == Some(access));
553        }
554        self.refine(query, hits)
555    }
556
557    /// The hits of `query`'s located indexes that it keeps, with their notes:
558    /// a [`Query::Virtual`] labels the calls it reached by dispatch, a
559    /// [`Query::Call`] judges the values of its target's calls.
560    fn refine(&self, query: &CompiledQuery, hits: Vec<Hit>) -> Result<Vec<Hit>> {
561        match query.parts() {
562            Parts::Call(c) => {
563                let hits = self.refine(&c.target, hits)?;
564                self.constrain(c, hits)
565            }
566            Parts::Virtual(_, via) => self.label_dispatch(via, hits),
567            _ => Ok(hits),
568        }
569    }
570
571    /// `hits` with a `(dispatch LIST)` note on each call [`Query::Virtual`]
572    /// added for dispatch.
573    fn label_dispatch(&self, via: &BTreeMap<String, String>, mut hits: Vec<Hit>) -> Result<Vec<Hit>> {
574        if via.is_empty() {
575            return Ok(hits);
576        }
577        let mut labels: HashMap<u32, Option<&str>> = HashMap::new();
578        for hit in &mut hits {
579            let label = match labels.get(&hit.matched_idx) {
580                Some(&l) => l,
581                None => {
582                    let m = self.dex.method_id(hit.matched_idx)?;
583                    let name = self.dex.method_display(hit.matched_idx)?;
584                    let proto = self.dex.proto_id(u32::from(m.proto_idx))?;
585                    let mut sig = format!("{name}(");
586                    for p in self.dex.type_list(proto.parameters_off)? {
587                        sig.push_str(&self.dex.type_descriptor(u32::from(p))?);
588                    }
589                    sig.push(')');
590                    sig.push_str(&self.dex.type_descriptor(proto.return_type_idx)?);
591                    let l = via.get(&sig).or_else(|| via.get(&name)).map(String::as_str);
592                    labels.insert(hit.matched_idx, l);
593                    l
594                }
595            };
596            if let Some(label) = label {
597                hit.note = Some(format!("(dispatch {label})"));
598            }
599        }
600        Ok(hits)
601    }
602
603    /// Hits of several queries with one code scan per index kind: the
604    /// located sets of the queries of a kind are merged, scanned once, and
605    /// each hit is handed to every query whose set holds its index. Gives,
606    /// per query, what [`FindRefs::find_ref_compiled`] would (class relations
607    /// and compound queries: nothing).
608    pub fn find_many(&self, queries: &[&CompiledQuery]) -> Result<Vec<Vec<Hit>>> {
609        let mut out: Vec<Vec<Hit>> = (0..queries.len()).map(|_| Vec::new()).collect();
610        let mut located: Vec<Option<BTreeSet<u32>>> = Vec::with_capacity(queries.len());
611        for q in queries {
612            let skip = q.relation().is_some() || q.compound().is_some();
613            located.push(if skip { None } else { Some(self.locate_compiled(q)?).filter(|s| !s.is_empty()) });
614        }
615        for kind in [RefKind::String, RefKind::Type, RefKind::Method, RefKind::Field] {
616            let mine: Vec<usize> = (0..queries.len()).filter(|&i| queries[i].kind() == kind && located[i].is_some()).collect();
617            let union: BTreeSet<u32> = match mine.as_slice() {
618                [] => continue,
619                [only] => located[*only].clone().unwrap_or_default(),
620                _ => mine.iter().flat_map(|&i| located[i].iter().flatten().copied()).collect(),
621            };
622            for hit in self.scan_idxs(kind, &union)? {
623                for &i in &mine {
624                    if located[i].as_ref().is_some_and(|s| s.contains(&hit.matched_idx)) {
625                        let access = queries[i].query().field_access();
626                        if access.is_none() || access == hit.field_access {
627                            out[i].push(hit.clone());
628                        }
629                    }
630                }
631            }
632        }
633        for (q, hits) in queries.iter().zip(out.iter_mut()) {
634            *hits = self.refine(q, std::mem::take(hits))?;
635        }
636        Ok(out)
637    }
638
639    /// Answer an `Extends` / `Implements` query: the `class_idx` of every
640    /// class definition that extends / implements the target, in
641    /// `class_defs` order. `None` for any other query.
642    pub fn class_relation(&self, query: &CompiledQuery) -> Result<Option<Vec<u32>>> {
643        match query.parts() {
644            Parts::Relation(rel, descriptor) => Ok(Some(classdefs::related_classes(self.dex, *rel, descriptor)?)),
645            _ => Ok(None),
646        }
647    }
648
649    /// Scan for already-located indexes of `kind`.
650    pub fn scan_idxs(&self, kind: RefKind, idxs: &BTreeSet<u32>) -> Result<Vec<Hit>> {
651        let buf = self.dex.buf();
652        let insn = self.insn_locator()?;
653        let scanner = CodeScanner::new(insn);
654        // Every id table has at least 4-byte entries, so an index at or above
655        // `buf.len() / 4` cannot name anything in this DEX; dropping such
656        // targets also bounds the dense bitset for callers passing arbitrary
657        // indexes (`u32::MAX` would otherwise allocate 512 MiB).
658        let max_idx = (buf.len() / 4) as u32;
659        let set = IdxSet::from_iter(idxs.iter().copied().filter(|&i| i < max_idx));
660        // CodeScanner's public raw API retains unverified byte-pattern
661        // candidates. FindRefs asks its internal instruction-aware variant to
662        // omit odd offsets before allocating candidate storage; no DEX opcode
663        // can start there.
664        let matches = scanner.scan_instruction_candidates(buf, kind, &set);
665        let offsets: Vec<u32> = matches.iter().map(|m| m.0).collect();
666        let methods = insn.locate(buf, &offsets);
667        Ok(matches
668            .into_iter()
669            .zip(methods)
670            .map(|((offset, matched_idx), methods)| Hit {
671                offset,
672                matched_idx,
673                methods,
674                field_access: (kind == RefKind::Field).then(|| FieldAccess::from_opcode(buf[offset as usize])).flatten(),
675                note: None,
676            })
677            .collect())
678    }
679}