use crate::{CompiledQuery, Engine, FindOptions, Job, Query, RefBatch, RefContext, SearchTarget, Shape, WriteJob, tiers};
use asc_apk::Entry;
use asc_cache::{LockExt, Tier};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock};
use std::time::{Duration, SystemTime};
type Ident = [u8; 32];
type GroupKey = (u32, u64, Vec<usize>);
#[derive(Default)]
pub struct DexDedup {
groups: Mutex<HashMap<GroupKey, Arc<Group>>>,
shared: Arc<AtomicUsize>,
}
#[derive(Clone)]
pub struct Claim {
group: Arc<Group>,
lead: bool,
shared: Arc<AtomicUsize>,
}
#[derive(Default)]
struct Group {
members: AtomicUsize,
result: Mutex<Option<Option<Arc<Shared>>>>,
ready: Condvar,
}
struct Shared {
ident: Ident,
batches: Vec<RefBatch>,
}
impl DexDedup {
pub fn new() -> DexDedup {
DexDedup::default()
}
pub fn claim(&self, entry: &Entry) -> Claim {
self.claim_rules(entry, &[])
}
pub fn claim_rules(&self, entry: &Entry, rules: &[usize]) -> Claim {
let mut groups = self.groups.lock_or_default();
let group = groups.entry((entry.crc32, entry.uncomp_size, rules.to_vec())).or_default();
let lead = group.members.fetch_add(1, Ordering::Relaxed) == 0;
Claim { group: Arc::clone(group), lead, shared: Arc::clone(&self.shared) }
}
pub fn follows(claim: &Claim) -> bool {
!claim.lead
}
pub fn shared(&self) -> usize {
self.shared.load(Ordering::Relaxed)
}
}
struct Publish<'a>(&'a Group);
impl Publish<'_> {
fn set(self, value: Option<Arc<Shared>>) {
let mut r = self.0.result.lock().expect("shared result mutex poisoned");
*r = Some(value);
self.0.ready.notify_all();
drop(r);
std::mem::forget(self);
}
}
impl Drop for Publish<'_> {
fn drop(&mut self) {
let mut r = self.0.result.lock_or_default();
if r.is_none() {
*r = Some(None);
}
self.0.ready.notify_all();
}
}
impl Engine {
pub fn cached_refs(&self, target: &SearchTarget, query: &Query, aggregate: bool) -> Option<Vec<RefBatch>> {
let (disk, key) = self.rendered_tier(target)?;
tiers::decode_batches(disk.get_rendered_view(key, &tiers::findrefs_key(query, sweep_opts(aggregate), Shape::Results))?)
.map(|r| r.batches)
}
pub fn store_refs(&self, query: &Query, aggregate: bool, batches: &[RefBatch]) {
if !self.cache.as_ref().and_then(|c| c.disk.as_deref()).is_some_and(|d| d.uses(Tier::Rendered)) {
return;
}
let Some(apk) = self.open().ok() else { return };
let Some(key) = self.apk_key(&apk) else { return };
let mut recorder = tiers::ResultRecorder::new(tiers::L3_MAX_RESULT);
for b in batches {
recorder.record(b);
}
if let Some(payload) = recorder.finish(&[]) {
let query = tiers::findrefs_key(query, sweep_opts(aggregate), Shape::Results);
let job = WriteJob { key, apk: Arc::from(apk.path()), kind: Job::Rendered { query, payload } };
self.cache.as_ref().expect("an L3 tier implies a cache").put_async(job);
}
}
fn rendered_tier<'a>(&'a self, target: &'a SearchTarget) -> Option<(&'a asc_cache::DiskCache, &'a asc_cache::ApkKey)> {
let disk = self.cache.as_ref()?.disk.as_deref().filter(|d| d.uses(Tier::Rendered))?;
Some((disk, target.key.as_ref()?))
}
pub fn search_target_entry_dedup(
&self,
target: &SearchTarget,
entry: &Entry,
query: &CompiledQuery,
aggregate: bool,
cancel: &AtomicBool,
claim: &Claim,
) -> RefBatch {
let search = || vec![self.search_target_entry(target, entry, query, aggregate, cancel)];
self.dedup_search(target, entry, cancel, claim, search).pop().expect("one batch per query")
}
pub fn search_target_entry_rules_dedup(
&self,
target: &SearchTarget,
entry: &Entry,
rules: &[&CompiledQuery],
aggregate: bool,
cancel: &AtomicBool,
claim: &Claim,
) -> Vec<RefBatch> {
let search = || self.search_target_entry_rules(target, entry, rules, aggregate, cancel);
self.dedup_search(target, entry, cancel, claim, search)
}
fn dedup_search(
&self,
target: &SearchTarget,
entry: &Entry,
cancel: &AtomicBool,
claim: &Claim,
search: impl FnOnce() -> Vec<RefBatch>,
) -> Vec<RefBatch> {
let group = &claim.group;
if claim.lead {
let publish = Publish(group);
let batches = search();
if group.members.load(Ordering::Relaxed) > 1 {
let ident = content_id(target, entry, || {
self.entry_bytes(&target.apk, target.key.as_ref(), entry).ok().map(|b| Sha256::digest(&**b).into())
});
publish.set(ident.map(|ident| Arc::new(Shared { ident, batches: batches.clone() })));
} else {
publish.set(None);
}
return batches;
}
if let Some(shared) = wait_shared(group, cancel) {
let ident = content_id(target, entry, || {
target.apk.read_entry_cancellable(entry, Some(cancel)).ok().map(|b| Sha256::digest(&*b).into())
});
if ident == Some(shared.ident) {
claim.shared.fetch_add(1, Ordering::Relaxed);
return shared.batches.iter().map(|b| renamed(b.clone(), &entry.name)).collect();
}
}
search()
}
}
fn sweep_opts(aggregate: bool) -> FindOptions {
FindOptions { aggregate, context: RefContext::None }
}
fn wait_shared(group: &Group, cancel: &AtomicBool) -> Option<Arc<Shared>> {
let mut r = group.result.lock().expect("shared result mutex poisoned");
while r.is_none() {
if cancel.load(Ordering::Relaxed) {
return None;
}
r = group.ready.wait_timeout(r, Duration::from_millis(50)).expect("shared result mutex poisoned").0;
}
r.clone().flatten()
}
type ContentKey = (PathBuf, u64, Option<SystemTime>, String, u32, u64);
const MAX_CONTENT_IDS: usize = 1 << 16;
fn content_id(target: &SearchTarget, entry: &Entry, hash: impl FnOnce() -> Option<Ident>) -> Option<Ident> {
static IDS: OnceLock<Mutex<HashMap<ContentKey, Ident>>> = OnceLock::new();
let (size, modified) = target.apk.stamp().ok()?;
let key = (target.apk.path().to_path_buf(), size, modified, entry.name.clone(), entry.crc32, entry.uncomp_size);
let ids = IDS.get_or_init(Mutex::default);
if let Some(id) = ids.lock_or_default().get(&key) {
return Some(*id);
}
let id = hash()?;
let mut ids = ids.lock_or_default();
if ids.len() >= MAX_CONTENT_IDS {
ids.clear();
}
ids.insert(key, id);
Some(id)
}
fn renamed(mut batch: RefBatch, name: &str) -> RefBatch {
let from = std::mem::replace(&mut batch.entry_name, name.to_string());
if from != name {
for r in &mut batch.results {
if let Some(rest) = r.dex_name.strip_prefix(&from) {
r.dex_name = format!("{name}{rest}");
}
}
if let Some(e) = &mut batch.error {
*e = e.replace(&format!("{from}!"), &format!("{name}!"));
}
}
batch.source = "shared";
batch
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RefResult;
#[test]
fn renamed_moves_rows_and_container_errors_to_the_follower() {
let row = |dex: &str| RefResult {
dex_name: dex.into(),
method_idx: 0,
method: "La;->m".into(),
signature: "La;->m()V".into(),
matched: vec![],
relation: None,
sites: vec![],
context: None,
};
let batch = RefBatch {
entry_name: "classes2.dex".into(),
results: vec![row("classes2.dex"), row("classes2.dex!classes1.dex")],
text: None,
inflate_us: 0,
search_us: 0,
source: "inflate",
error: Some("classes2.dex!classes2.dex: bad".into()),
};
let b = renamed(batch, "classes5.dex");
assert_eq!(b.entry_name, "classes5.dex");
assert_eq!(b.results.iter().map(|r| r.dex_name.as_str()).collect::<Vec<_>>(), ["classes5.dex", "classes5.dex!classes1.dex"]);
assert_eq!(b.error.as_deref(), Some("classes5.dex!classes2.dex: bad"));
assert_eq!(b.source, "shared");
}
#[test]
fn a_cancelled_or_unwound_leader_releases_followers() {
let group = Group::default();
drop(Publish(&group));
assert!(wait_shared(&group, &AtomicBool::new(false)).is_none());
let pending = Group::default();
assert!(wait_shared(&pending, &AtomicBool::new(true)).is_none());
}
}