use asc_engine::{CacheConfig, Engine, FindOptions, names};
use asc_findrefs::{ClassQuery, MemberQuery, Query};
use asc_fixture::{make_apk, make_padded_dex};
use std::io::Write;
use std::sync::atomic::AtomicBool;
const PAD: usize = 1 << 20;
fn temp_apk(dex: &[u8], deflate: bool) -> tempfile::NamedTempFile {
let bytes = make_apk(&[("classes.dex", dex)], deflate);
let mut f = tempfile::Builder::new().suffix(".apk").tempfile().unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
f
}
fn search(engine: &Engine, q: &Query) -> (Vec<String>, u64) {
let before = engine.cache_stats().skipped;
let mut out = Vec::new();
engine.find_refs(q, |b| out.extend(b.results.iter().map(|r| r.line()))).unwrap();
out.sort();
(out, engine.cache_stats().skipped - before)
}
fn queries() -> Vec<(Query, bool)> {
let method = |class: Option<&str>, name: &str| {
Query::Method(MemberQuery::default().with_class(class.map(ClassQuery::exact)).with_name(Some(name.into())))
};
vec![
(Query::String("tok".into()), false),
(Query::String("absent".into()), true),
(Query::StringLiteral("token".into()), false),
(Query::StringLiteral("nothing here".into()), true),
(Query::Type("Lexample/Test;".into()), false),
(Query::Type("Lmissing/Type;".into()), true),
(method(Some("Lexample/Test;"), "first"), false),
(method(Some("Lexample/Test;"), "third"), true),
(method(Some("Lmissing/Type;"), "first"), true),
(method(None, "sec"), false),
(Query::Extends("Ljava/lang/Object;".into()), false),
(Query::Extends("Lmissing/Base;".into()), true),
(Query::Implements("Lmissing/Iface;".into()), true),
(Query::All { all: vec![Query::String("tok".into()), Query::String("absent".into())], none: vec![] }, true),
(Query::All { all: vec![Query::String("tok".into())], none: vec![Query::String("absent".into())] }, false),
]
}
#[test]
fn front_string_data_skips_only_what_cannot_match() {
let dex = make_padded_dex(0, PAD);
let deflated = temp_apk(&dex, true);
let stored = temp_apk(&dex, false);
let (probed, plain) = (Engine::new(deflated.path()).with_threads(1), Engine::new(stored.path()).with_threads(1));
for (q, ruled_out) in queries() {
let (rows, skipped) = search(&probed, &q);
let (expected, never) = search(&plain, &q);
assert_eq!(rows, expected, "{q:?}");
assert_eq!(never, 0, "stored entries are never probed");
assert_eq!(skipped, u64::from(ruled_out), "{q:?}");
}
}
#[test]
fn late_string_data_or_a_kept_entry_is_searched_whole() {
let late = temp_apk(&make_padded_dex(PAD, 0), true);
let engine = Engine::new(late.path()).with_threads(1);
for (q, _) in queries() {
assert_eq!(search(&engine, &q).1, 0, "{q:?}");
}
assert_eq!(search(&engine, &Query::String("tok".into())).0.len(), 2);
let front = temp_apk(&make_padded_dex(0, PAD), true);
let cached = Engine::new(front.path()).with_cache(CacheConfig::default().memory_bytes(64 << 20));
assert_eq!(search(&cached, &Query::String("absent".into())), (Vec::new(), 0));
}
#[test]
fn rule_sets_skip_an_entry_only_when_no_rule_can_match() {
let front = temp_apk(&make_padded_dex(0, PAD), true);
let engine = Engine::new(front.path()).with_threads(1);
let run = |rules: &[&str]| {
let rules: Vec<Query> = rules.iter().map(|r| names::parse_condition(r).unwrap()).collect();
let before = engine.cache_stats().skipped;
let mut totals = vec![0; rules.len()];
let opts = FindOptions::default().aggregate(true);
engine.find_refs_rules(&rules, opts, &AtomicBool::new(false), &mut |i, b| totals[i] += b.results.len()).unwrap();
(totals, engine.cache_stats().skipped - before)
};
assert_eq!(run(&["string:absent", "type:Lmissing/Type;"]), (vec![0, 0], 1));
assert_eq!(run(&["string:absent", "string:tok"]), (vec![0, 2], 0));
}
#[test]
fn signatures_and_their_zero_result_hints_read_the_prefix() {
let dex = make_padded_dex(0, PAD);
let (deflated, stored) = (temp_apk(&dex, true), temp_apk(&dex, false));
let (probed, plain) = (Engine::new(deflated.path()).with_threads(1), Engine::new(stored.path()).with_threads(1));
let rules = [
("method:Lexample/Test;->first()V", false, "no code calls it"),
("method:Lexample/Test;->first(I)V", true, "has no overload (I)V"),
("method:example.Test.first(int)", true, "has no overload (I)"),
];
for (rule, ruled_out, hint) in rules {
let q = names::parse_condition(rule).unwrap();
let (rows, skipped) = search(&probed, &q);
assert_eq!(rows, search(&plain, &q).0, "{rule}");
assert_eq!(skipped, u64::from(ruled_out), "{rule}");
if rows.is_empty() {
let inflates = probed.cache_stats().inflates;
let hints = probed.no_results_hints(&q, 1);
assert_eq!(hints, plain.no_results_hints(&q, 1), "{rule}");
assert!(hints.iter().any(|h| h.contains(hint)), "{hints:?}");
assert_eq!(probed.cache_stats().inflates, inflates, "the hint reads the prefix, not the whole entry");
}
}
}