Skip to main content

asc_engine/
rules.rs

1//! Compound queries (one method matching several conditions) and rule sets
2//! (several queries answered by one pass over each DEX).
3//!
4//! Both run their parts through [`FindRefs::find_many`], which merges the
5//! located indexes of every part of the same kind and scans the code once
6//! per kind, then splits the hits back per part.
7
8use crate::{
9    Engine, FindOptions, Job, RefBatch, RefContext, RefResult, RefSite, RefSummary, Result, Rows, SearchTarget, Shape, WriteJob,
10    attach_context, hit_matched, results_from_hits, rows_compiled, search_entry_with, skip, tiers,
11};
12use asc_apk::Entry;
13use asc_cache::Tier;
14use asc_findrefs::{CompiledQuery, Compound, Defined, FieldAccess, FindRefs, Query};
15use std::collections::BTreeMap;
16use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
17use std::sync::{Arc, mpsc};
18use std::time::Instant;
19
20/// Rows of a compound query ([`Query::All`]) over one DEX: one per method
21/// that matches every `all` part and no `none` part. `matched` lists, part
22/// by part, the constants each `all` part matched there; the sites are every
23/// matching instruction of the `all` parts, in offset order.
24pub(crate) fn compound_rows(fr: &FindRefs<'_, '_, '_>, c: &Compound, opts: FindOptions) -> Result<Vec<RefResult>> {
25    let dex = fr.dex();
26    // Code parts are found with a shared scan; definition parts (`flag:`,
27    // `override:`, ...) name methods from the class definitions instead.
28    let code_all: Vec<&CompiledQuery> = c.all.iter().filter(|q| q.defined().is_none()).collect();
29    let code_none: Vec<&CompiledQuery> = c.none.iter().filter(|q| q.defined().is_none()).collect();
30    let def_all: Vec<&Defined> = c.all.iter().filter_map(|q| q.defined()).collect();
31    let def_none: Vec<&Defined> = c.none.iter().filter_map(|q| q.defined()).collect();
32
33    let parts: Vec<&CompiledQuery> = code_all.iter().chain(&code_none).copied().collect();
34    let hits = fr.find_many(&parts)?;
35    let n = code_all.len();
36    // method -> per code `all` part, its (offset, matched index, note, field access) hits
37    type PartHits<'h> = Vec<(u32, u32, Option<&'h str>, Option<FieldAccess>)>;
38    let mut methods: BTreeMap<u32, Vec<PartHits<'_>>> = BTreeMap::new();
39    if n == 0 {
40        // No code part proposes methods: seed from the first definition part.
41        let seed = defined_method_set(dex, def_all[0])?;
42        for m in seed {
43            methods.insert(m, Vec::new());
44        }
45    } else {
46        for (p, part_hits) in hits[..n].iter().enumerate() {
47            for h in part_hits {
48                let Some(ms) = &h.methods else { continue };
49                for &m in ms.ids() {
50                    // The first part proposes the methods, the others can only keep them.
51                    let slot = if p == 0 { Some(methods.entry(m).or_insert_with(|| vec![Vec::new(); n])) } else { methods.get_mut(&m) };
52                    if let Some(slot) = slot {
53                        slot[p].push((h.offset, h.matched_idx, h.note.as_deref(), h.field_access));
54                    }
55                }
56            }
57        }
58        methods.retain(|_, per_part| per_part.iter().all(|h| !h.is_empty()));
59    }
60    // Keep only methods every definition `all` part also names (`def_all[0]`
61    // already seeded the set when there is no code part).
62    // Both are checked candidate by candidate, reading only their classes.
63    for d in def_all.iter().skip(usize::from(n == 0)) {
64        retain_defined(dex, &mut methods, d, true)?;
65    }
66    // Drop methods a `none` part matches, code or definition.
67    for h in hits[n..].iter().flatten() {
68        for &m in h.methods.iter().flat_map(|ms| ms.ids()) {
69            methods.remove(&m);
70        }
71    }
72    for d in &def_none {
73        retain_defined(dex, &mut methods, d, false)?;
74    }
75    let def_labels: Vec<String> = def_all.iter().map(|d| d.label()).collect();
76    let mut rows = Vec::with_capacity(methods.len());
77    for (m, per_part) in methods {
78        let mut matched = Vec::new();
79        let mut sites = Vec::new();
80        for (part, part_hits) in code_all.iter().zip(per_part) {
81            let mut idxs: Vec<(u32, Option<&str>, Option<FieldAccess>)> =
82                part_hits.iter().map(|&(_, i, note, access)| (i, note, access)).collect();
83            idxs.sort_unstable();
84            idxs.dedup();
85            let base = matched.len();
86            for &(i, note, access) in &idxs {
87                matched.push(hit_matched(dex, part.kind(), i, note, access)?);
88            }
89            for (offset, i, note, access) in part_hits {
90                sites.push(RefSite {
91                    offset,
92                    matched: base + idxs.binary_search(&(i, note, access)).unwrap_or(0),
93                    field_access: access,
94                    pc: None,
95                    line: None,
96                });
97            }
98        }
99        for label in &def_labels {
100            matched.push(crate::Matched::Symbol(label.clone()));
101        }
102        sites.sort_by_key(|s| s.offset);
103        rows.push(RefResult::method_row(dex, m, matched, sites)?);
104    }
105    if opts.context != RefContext::None {
106        attach_context(fr, &mut rows, opts.context)?;
107    }
108    Ok(rows)
109}
110
111/// Keep the methods of `methods` whose definition matches `d` (`keep`) or
112/// does not (`!keep`).
113fn retain_defined<V>(dex: &asc_dex::Dex<'_>, methods: &mut BTreeMap<u32, V>, d: &Defined, keep: bool) -> Result<()> {
114    let mut filter = crate::defs::DefinedFilter::new(dex, d)?;
115    let mut out = Vec::new();
116    for &m in methods.keys() {
117        if filter.matches(m)? != keep {
118            out.push(m);
119        }
120    }
121    for m in out {
122        methods.remove(&m);
123    }
124    Ok(())
125}
126
127/// The method ids a resolved definition predicate names in this DEX, as a set.
128fn defined_method_set(dex: &asc_dex::Dex<'_>, d: &Defined) -> Result<std::collections::BTreeSet<u32>> {
129    Ok(crate::defs::defined_methods(dex, d)?.into_iter().collect())
130}
131
132/// Rows of every rule over one DEX, one list per rule. Plain code queries
133/// share [`FindRefs::find_many`]'s scans; class relations and compound
134/// queries run on their own.
135pub(crate) fn rule_rows(fr: &FindRefs<'_, '_, '_>, rules: &[&CompiledQuery], opts: FindOptions) -> Result<Vec<Vec<RefResult>>> {
136    let plain: Vec<usize> = (0..rules.len())
137        .filter(|&i| rules[i].relation().is_none() && rules[i].compound().is_none() && rules[i].defined().is_none())
138        .collect();
139    let hits = fr.find_many(&plain.iter().map(|&i| rules[i]).collect::<Vec<_>>())?;
140    let mut out: Vec<Option<Vec<RefResult>>> = (0..rules.len()).map(|_| None).collect();
141    for (&i, hits) in plain.iter().zip(hits) {
142        let mut rows = results_from_hits(fr.dex(), rules[i].kind(), &hits, opts.aggregate)?;
143        if opts.context != RefContext::None {
144            attach_context(fr, &mut rows, opts.context)?;
145        }
146        out[i] = Some(rows);
147    }
148    for (i, slot) in out.iter_mut().enumerate() {
149        if slot.is_none() {
150            *slot = Some(rows_compiled(fr, rules[i], opts)?);
151        }
152    }
153    Ok(out.into_iter().map(Option::unwrap_or_default).collect())
154}
155
156/// Compile every rule; the first invalid one fails them all.
157///
158/// # Errors
159///
160/// [`EngineError::Find`](crate::EngineError::Find) for the first rule that
161/// does not compile (an invalid regex, ...).
162pub fn compile_rules(rules: &[Query]) -> Result<Vec<CompiledQuery>> {
163    Ok(rules.iter().map(CompiledQuery::new).collect::<std::result::Result<_, _>>()?)
164}
165
166impl Engine {
167    /// Several queries in one pass: each DEX is loaded once and scanned once
168    /// per index kind for all of them. `sink` gets `(rule index, batch)`,
169    /// one batch per rule and entry, entries in completion order.
170    ///
171    /// Each rule shares L3 with [`Engine::find_refs_opts`]: rules an earlier
172    /// run left there are replayed, only the others are searched, and each of
173    /// those is kept under its own key.
174    ///
175    /// # Errors
176    ///
177    /// As [`Engine::find_refs`], for the first invalid rule.
178    ///
179    /// # Panics
180    ///
181    /// Never: results are only recorded for L3 when a disk cache is in use.
182    pub fn find_refs_rules(
183        &self,
184        rules: &[Query],
185        opts: FindOptions,
186        cancel: &AtomicBool,
187        sink: &mut dyn FnMut(usize, RefBatch),
188    ) -> Result<RefSummary> {
189        let t0 = Instant::now();
190        for rule in rules {
191            crate::compose::check_patterns(rule)?;
192        }
193        if let Some(engine) = self.transient(rules.iter().any(crate::compose::needs_program))? {
194            return engine.find_refs_rules(rules, opts, cancel, sink);
195        }
196        let apk = self.open()?;
197        let mut entries = apk.dex_entries()?;
198        // Largest first, as in `find_refs_opts`.
199        entries.reverse();
200        let key = self.apk_key(&apk);
201        let mut summary = RefSummary { entries: entries.len(), ..Default::default() };
202        if entries.is_empty() || rules.is_empty() {
203            return Ok(summary);
204        }
205        let disk = self.cache.as_ref().and_then(|c| c.disk.as_deref()).filter(|d| d.uses(Tier::Rendered));
206        let rendered = disk.zip(key.as_ref());
207        let mut missing = Vec::new();
208        for (i, rule) in rules.iter().enumerate() {
209            let replay = rendered
210                .and_then(|(d, k)| d.get_rendered_view(k, &tiers::findrefs_key(rule, opts, Shape::Results)))
211                .and_then(tiers::decode_batches);
212            match replay {
213                Some(recorded) => {
214                    for batch in recorded.batches {
215                        summary.total_results += batch.results.len();
216                        sink(i, batch);
217                    }
218                    summary.notes.extend(recorded.notes);
219                }
220                None => missing.push(i),
221            }
222        }
223        if missing.is_empty() {
224            summary.notes = crate::compose::merge_notes(std::mem::take(&mut summary.notes));
225            summary.total_us = t0.elapsed().as_micros();
226            self.log(format!("[APK] for_each_findrefs_rules total={} us rules={} source=result", summary.total_us, rules.len()));
227            return Ok(summary);
228        }
229        // The `method:{RULE}` conditions of the rules to search, all in one
230        // pass first. L3 keys on the rules as given, so replays skip this.
231        let todo: Vec<Query> = missing.iter().map(|&i| rules[i].clone()).collect();
232        let resolved = self.resolve(&todo, cancel)?;
233        let rule_notes = resolved.notes;
234        summary.notes.extend(rule_notes.iter().flatten().cloned());
235        summary.notes = crate::compose::merge_notes(std::mem::take(&mut summary.notes));
236        let _query = self.prebuild.query();
237        let compiled = compile_rules(resolved.queries.as_deref().unwrap_or(&todo))?;
238        let mut recorders: Vec<_> = missing.iter().map(|_| rendered.map(|_| tiers::ResultRecorder::new(tiers::L3_MAX_RESULT))).collect();
239        let next = AtomicUsize::new(0);
240        let (tx, rx) = mpsc::channel::<Vec<RefBatch>>();
241        let workers = self.threads().min(entries.len()).max(1);
242        let searched: Vec<&CompiledQuery> = compiled.iter().collect();
243        let (cache, stats, searched) = (self.cache.as_deref(), &self.stats, &searched);
244        let run = |fr: &FindRefs<'_, '_, '_>| rule_rows(fr, searched, opts).map(|rows| rows.into_iter().map(Rows::Results).collect());
245        // An entry is left out only when no rule can match it.
246        let probe = skip::Probe::new(searched.clone());
247        let probe = &probe;
248        std::thread::scope(|s| {
249            for _ in 0..workers {
250                let tx = tx.clone();
251                let (apk, entries, next, key, run) = (&apk, &entries, &next, key.as_ref(), &run);
252                s.spawn(move || {
253                    loop {
254                        if cancel.load(Ordering::Relaxed) {
255                            break;
256                        }
257                        let i = next.fetch_add(1, Ordering::Relaxed);
258                        let Some(entry) = entries.get(i) else { break };
259                        let batches = search_entry_with(apk, key, entry, searched.len(), run, Some(probe), cancel, cache, stats);
260                        if tx.send(batches).is_err() {
261                            break;
262                        }
263                    }
264                });
265            }
266            drop(tx);
267            for batches in rx {
268                for (j, batch) in batches.into_iter().enumerate() {
269                    summary.total_results += batch.results.len();
270                    if let Some(r) = recorders[j].as_mut() {
271                        r.record(&batch);
272                    }
273                    sink(missing[j], batch);
274                }
275            }
276        });
277        // A cancelled run may be missing entries.
278        if let Some((_, k)) = rendered.filter(|_| !cancel.load(Ordering::Relaxed)) {
279            for ((&i, recorder), notes) in missing.iter().zip(recorders).zip(&rule_notes) {
280                if let Some(payload) = recorder.and_then(|r| r.finish(notes)) {
281                    let query = tiers::findrefs_key(&rules[i], opts, Shape::Results);
282                    let job = WriteJob { key: k.clone(), apk: Arc::from(apk.path()), kind: Job::Rendered { query, payload } };
283                    self.cache.as_ref().expect("rendered implies a cache").put_async(job);
284                }
285            }
286        }
287        summary.total_us = t0.elapsed().as_micros();
288        self.log(format!(
289            "[APK] for_each_findrefs_rules total={} us count={} rules={} searched={} workers={workers}",
290            summary.total_us,
291            entries.len(),
292            rules.len(),
293            missing.len()
294        ));
295        Ok(summary)
296    }
297
298    /// [`Engine::search_target_entry`] for a rule set: one batch per rule.
299    pub fn search_target_entry_rules(
300        &self,
301        target: &SearchTarget,
302        entry: &Entry,
303        rules: &[&CompiledQuery],
304        aggregate: bool,
305        cancel: &AtomicBool,
306    ) -> Vec<RefBatch> {
307        let opts = FindOptions { aggregate, context: RefContext::None };
308        let run = |fr: &FindRefs<'_, '_, '_>| rule_rows(fr, rules, opts).map(|rows| rows.into_iter().map(Rows::Results).collect());
309        search_entry_with(&target.apk, target.key.as_ref(), entry, rules.len(), &run, None, cancel, self.cache.as_deref(), &self.stats)
310    }
311}