use std::path::PathBuf;
use droidsaw::commands;
use droidsaw::context::CrossLayerContext;
fn minimal_dex_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("worktree has parent")
.join("droidsaw-dex/tests/fixtures/classes.dex")
}
fn run_frida(regex: &str) -> serde_json::Value {
let path = minimal_dex_path();
let ctx = CrossLayerContext::parse(&path, None).expect("classes.dex parses");
commands::frida(&ctx, regex).expect("frida command runs")
}
#[test]
fn hello_string_emits_runnable_per_method_hook() {
let out = run_frida(r"^hello $");
let hooks = out
.get("hooks")
.and_then(|v| v.as_array())
.expect("hooks array");
assert_eq!(
hooks.len(),
1,
"expected exactly one DEX hook for `hello ` string; got {hooks:#?}"
);
let hook = &hooks[0];
assert_eq!(hook.get("layer").and_then(|v| v.as_str()), Some("dex1"));
assert_eq!(hook.get("function").and_then(|v| v.as_str()), Some("Minimal"));
assert_eq!(
hook.get("matched_string").and_then(|v| v.as_str()),
Some("hello ")
);
let js = hook
.get("hook")
.and_then(|v| v.as_str())
.expect("hook string");
assert!(
js.contains("Java.perform(function() {"),
"DEX hook must wrap in Java.perform; got:\n{js}"
);
assert!(
js.contains(r#"var cls = Java.use("Minimal");"#),
"DEX hook must bind cls via Java.use; got:\n{js}"
);
assert!(
js.contains("cls.hello.overload('int').implementation = function(arg0)"),
"DEX hook must overload-bind hello with the dotted-Java type name 'int'; got:\n{js}"
);
assert!(
js.contains("this.hello.overload('int').apply(this, arguments)"),
"DEX hook must re-dispatch via apply with the same overload args; got:\n{js}"
);
assert!(
!js.contains(".overload('I')"),
"DEX hook must NOT use single-letter JNI form (frida-java-bridge expects 'int' not 'I'); got:\n{js}"
);
assert!(
js.contains("console.log('hello(' + arg0 + ')')"),
"DEX hook must log the call with positional arg interpolation; got:\n{js}"
);
assert!(
js.contains("console.log('-> ' + ret)"),
"DEX hook must log the return value; got:\n{js}"
);
assert!(
!js.contains("// hook methods here"),
"old class-granularity placeholder must be gone; got:\n{js}"
);
}
#[test]
fn meta_hint_reflects_dex_runnable_hermes_reference_shape() {
let out = run_frida(r"hello");
let hint = out
.pointer("/_meta/hint")
.and_then(|v| v.as_str())
.expect("_meta.hint string");
assert!(
hint.contains("DEX hook lines are runnable Frida JS"),
"_meta.hint must call out the DEX-runnable shape; got: {hint}"
);
assert!(
hint.contains("Hermes entries are reference comments"),
"_meta.hint must call out the Hermes-reference shape; got: {hint}"
);
assert!(
!hint.contains("hook strings are templates"),
"old template-only hint must be gone; got: {hint}"
);
}
#[test]
fn no_interceptor_attach_in_hermes_layer_hooks() {
let out = run_frida(r"hello");
let hooks = out
.get("hooks")
.and_then(|v| v.as_array())
.expect("hooks array");
for h in hooks {
let layer = h.get("layer").and_then(|v| v.as_str()).unwrap_or("");
let body = h.get("hook").and_then(|v| v.as_str()).unwrap_or("");
if layer == "hbc" {
assert!(
!body.contains("Interceptor.attach"),
"hbc-layer hook must not name Interceptor.attach (silent-hook foot-gun); got:\n{body}"
);
}
}
}
#[test]
fn unmatchable_regex_emits_no_hooks() {
let out = run_frida(r"this_string_does_not_exist_anywhere_in_classes_dex_\d{42}");
let hooks = out
.get("hooks")
.and_then(|v| v.as_array())
.expect("hooks array");
assert!(
hooks.is_empty(),
"unmatchable regex should yield zero hooks; got {} hooks",
hooks.len()
);
}