use asc_engine::{CacheConfig, CallQuery, CallRoots, Direction, Engine, names};
use asc_findrefs::Query;
use asc_fixture::{ClassSpec, CodeSpec, DexBuilder, MethodSpec, make_apk};
use std::sync::atomic::AtomicBool;
const IFACE: &str = "Lp/I;";
const IMPL: &str = "Lp/Impl;";
const CALLER: &str = "Lp/Caller;";
const OBJ: &str = "Ljava/lang/Object;";
const STR: &str = "Ljava/lang/String;";
fn le16(v: u32) -> [u8; 2] {
(v as u16).to_le_bytes()
}
fn app_dex() -> Vec<u8> {
let mut d = DexBuilder::new();
d.string("android_id").type_(OBJ).type_(STR).type_(IFACE);
d.method(IFACE, "leak", "V", &[STR]).method(CALLER, "call", "V", &[IFACE]);
let mut ids = d.resolve();
let leak = ids.method(IFACE, "leak", "V", &[STR]);
let mut call = vec![0x1a, 0x00]; call.extend_from_slice(&le16(ids.string("android_id")));
call.extend_from_slice(&[0x72, 0x20]); call.extend_from_slice(&le16(leak));
call.extend_from_slice(&[0x01, 0x00]); call.extend_from_slice(&[0x0e, 0x00]); let call_code = CodeSpec { registers: 2, ins: 1, outs: 2, insns: call, ..Default::default() };
ids.add_class(ClassSpec {
descriptor: IFACE.into(),
access: 0x1 | 0x200 | 0x400,
superclass: Some(OBJ.into()),
virtual_methods: vec![MethodSpec { idx: leak, access: 0x1 | 0x400, code: None }],
..Default::default()
});
let call_idx = ids.method(CALLER, "call", "V", &[IFACE]);
ids.add_class(ClassSpec {
descriptor: CALLER.into(),
access: 0x1,
superclass: Some(OBJ.into()),
direct_methods: vec![MethodSpec { idx: call_idx, access: 0x1 | 0x8, code: Some(call_code) }],
..Default::default()
});
ids.finish()
}
fn lib_dex() -> Vec<u8> {
let mut d = DexBuilder::new();
d.string("android_id").type_(OBJ).type_(STR).type_(IFACE);
d.method(IMPL, "leak", "V", &[STR]);
let mut ids = d.resolve();
let leak = ids.method(IMPL, "leak", "V", &[STR]);
let mut body = vec![0x1a, 0x00]; body.extend_from_slice(&le16(ids.string("android_id")));
body.extend_from_slice(&[0x0e, 0x00]); let code = CodeSpec { registers: 3, ins: 2, insns: body, ..Default::default() };
ids.add_class(ClassSpec {
descriptor: IMPL.into(),
access: 0x1,
superclass: Some(OBJ.into()),
interfaces: vec![IFACE.into()],
virtual_methods: vec![MethodSpec { idx: leak, access: 0x1, code: Some(code) }],
..Default::default()
});
ids.finish()
}
const MANY: &str = "Lp/Many;";
const MANY_CALLER: &str = "Lp/ManyCaller;";
const TAG: &str = "Lp/Tag;";
const IMPLEMENTERS: usize = asc_engine::DEFAULT_FAN_OUT + 1;
fn many_dex() -> Vec<u8> {
let impls: Vec<String> = (0..IMPLEMENTERS).map(|i| format!("Lp/M{i};")).collect();
let mut d = DexBuilder::new();
d.string("serial").type_(OBJ).type_(MANY).type_(TAG);
d.method(MANY, "run", "V", &[]).method(MANY_CALLER, "go", "V", &[MANY]);
for c in &impls {
d.type_(c).method(c, "run", "V", &[]);
}
let mut ids = d.resolve();
let run = ids.method(MANY, "run", "V", &[]);
ids.add_class(ClassSpec { descriptor: TAG.into(), access: 0x1 | 0x200 | 0x400, superclass: Some(OBJ.into()), ..Default::default() });
ids.add_class(ClassSpec {
descriptor: MANY.into(),
access: 0x1 | 0x200 | 0x400,
superclass: Some(OBJ.into()),
virtual_methods: vec![MethodSpec { idx: run, access: 0x1 | 0x400, code: None }],
..Default::default()
});
for (i, c) in impls.iter().enumerate() {
let mut body = Vec::new();
if i == 0 {
body.extend_from_slice(&[0x1a, 0x00]); body.extend_from_slice(&le16(ids.string("serial")));
}
body.extend_from_slice(&[0x0e, 0x00]); let code = CodeSpec { registers: 2, ins: 1, insns: body, ..Default::default() };
let idx = ids.method(c, "run", "V", &[]);
ids.add_class(ClassSpec {
descriptor: c.clone(),
access: 0x1,
superclass: Some(OBJ.into()),
interfaces: vec![MANY.into(), TAG.into()],
virtual_methods: vec![MethodSpec { idx, access: 0x1, code: Some(code) }],
..Default::default()
});
}
let mut go = vec![0x72, 0x10]; go.extend_from_slice(&le16(run));
go.extend_from_slice(&[0x00, 0x00, 0x0e, 0x00]); let go_idx = ids.method(MANY_CALLER, "go", "V", &[MANY]);
ids.add_class(ClassSpec {
descriptor: MANY_CALLER.into(),
access: 0x1,
superclass: Some(OBJ.into()),
direct_methods: vec![MethodSpec {
idx: go_idx,
access: 0x1 | 0x8,
code: Some(CodeSpec { registers: 1, ins: 1, outs: 1, insns: go, ..Default::default() }),
}],
..Default::default()
});
ids.finish()
}
fn program() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("app.jar"), make_apk(&[("classes.dex", &app_dex())], true)).unwrap();
std::fs::write(dir.path().join("lib.jar"), make_apk(&[("classes.dex", &lib_dex())], true)).unwrap();
std::fs::write(dir.path().join("many.jar"), make_apk(&[("classes.dex", &many_dex())], true)).unwrap();
dir
}
fn rows(engine: &Engine, q: &Query) -> Vec<String> {
rows_and_notes(engine, q).0
}
fn rows_and_notes(engine: &Engine, q: &Query) -> (Vec<String>, Vec<String>) {
let mut out = Vec::new();
let summary = engine.find_refs(q, |b| out.extend(b.results.iter().map(|r| r.line()))).unwrap();
out.sort();
(out, summary.notes)
}
#[test]
fn dispatch_nesting_constraints_and_defs_compose_across_jars() {
let dir = program();
let cache_dir = tempfile::tempdir().unwrap();
let cached = || {
let disk = asc_engine::asc_cache::DiskCache::open(cache_dir.path().join("c")).with_capacity(1 << 30).with_min_dex_bytes(0);
Engine::new(dir.path()).with_threads(2).with_cache(CacheConfig { memory_bytes: 64 << 20, disk: Some(disk), ..Default::default() })
};
for engine in [Engine::new(dir.path()).with_threads(2), cached()] {
let r = rows(&engine, &names::parse_rule("method:{literal:android_id}").unwrap());
assert_eq!(r, ["app.jar/classes.dex | Lp/Caller;->call(Lp/I;)V | Lp/I;->leak(Ljava/lang/String;)V (dispatch Lp/Impl;)"]);
assert!(rows(&engine, &names::parse_rule("method:={literal:android_id}").unwrap()).is_empty());
let r = rows(&engine, &names::parse_rule(r#"method:{literal:android_id}[arg0="android_id"]"#).unwrap());
assert_eq!(r.len(), 1, "{r:?}");
assert!(r[0].contains(r#"[arg0="android_id"]"#) && r[0].contains("(dispatch Lp/Impl;)"), "{}", r[0]);
assert!(rows(&engine, &names::parse_rule(r#"method:{literal:android_id}[arg0="other"]"#).unwrap()).is_empty());
let r = rows(&engine, &names::parse_rule("override:p.I->leak").unwrap());
assert_eq!(
r,
[
"app.jar/classes.dex | Lp/I;->leak(Ljava/lang/String;)V | override leak",
"lib.jar/classes.dex | Lp/Impl;->leak(Ljava/lang/String;)V | override leak",
]
);
let to = |unless: Vec<Query>| CallQuery {
depth: 3,
virtual_calls: true,
unless,
..CallQuery::new(CallRoots::Matching(names::parse_rule("method:{literal:android_id}").unwrap()), Direction::Callers)
};
let t = engine.call_graph(&to(Vec::new())).unwrap();
assert!(t.lines().iter().any(|l| l.contains("Lp/Caller;->call")), "{:?}", t.lines());
let t = engine.call_graph(&to(vec![names::parse_rule("method:Lp/I;->leak").unwrap()])).unwrap();
assert!(!t.lines().iter().any(|l| l.contains("Lp/Caller;->call")), "pruned: {:?}", t.lines());
assert!(rows_and_notes(&engine, &names::parse_rule("method:{literal:android_id}").unwrap()).1.is_empty());
}
let _ = AtomicBool::new(false);
}
#[test]
fn a_supertype_with_many_implementers_is_noted_not_followed() {
let dir = program();
let cache_dir = tempfile::tempdir().unwrap();
let cached = || {
let disk = asc_engine::asc_cache::DiskCache::open(cache_dir.path().join("c")).with_capacity(1 << 30).with_min_dex_bytes(0);
Engine::new(dir.path()).with_threads(2).with_cache(CacheConfig { memory_bytes: 64 << 20, disk: Some(disk), ..Default::default() })
};
let want_note = format!(
"dispatch not followed via Lp/Many;->run()V ({} implementers): name it explicitly, or write method:*{{...}}, to include",
IMPLEMENTERS + 1
);
let call_row = "many.jar/classes.dex | Lp/ManyCaller;->go(Lp/Many;)V | Lp/Many;->run()V";
for engine in [Engine::new(dir.path()).with_threads(2), cached()] {
let (r, notes) = rows_and_notes(&engine, &names::parse_rule("method:{literal:serial}").unwrap());
assert!(r.is_empty(), "{r:?}");
assert_eq!(notes, std::slice::from_ref(&want_note));
let (r, notes) = rows_and_notes(&engine, &names::parse_rule("method:*{literal:serial}").unwrap());
assert_eq!(r, [format!("{call_row} (dispatch Lp/M0;)")]);
assert!(notes.is_empty(), "{notes:?}");
assert_eq!(rows(&engine, &names::parse_rule("method:Lp/Many;->run").unwrap()), [call_row]);
let mut n = 0;
let summary = engine
.find_refs_rules(
&[names::parse_rule("method:{literal:serial}").unwrap()],
Default::default(),
&AtomicBool::new(false),
&mut |_, b| n += b.results.len(),
)
.unwrap();
assert_eq!((n, summary.notes), (0, vec![want_note.clone()]));
let rule = names::parse_rule("method:{method:{literal:serial}}").unwrap();
let t = engine.call_graph(&CallQuery { depth: 1, ..CallQuery::new(CallRoots::Matching(rule), Direction::Callers) });
assert!(t.is_err());
engine.flush_cache_writes();
}
let engine = cached();
let (r, notes) = rows_and_notes(&engine, &names::parse_rule("method:{literal:serial}").unwrap());
assert!(r.is_empty() && notes == [want_note], "{r:?} {notes:?}");
assert_eq!(engine.cache_stats().inflates, 0);
}