use std::path::PathBuf;
use droidsaw::commands::{
classify_decompile_target, dex_decompile, normalize_dex_class_search, DecompileRoute,
};
use droidsaw::context::CrossLayerContext;
fn classes_dex_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("worktree has parent")
.join("droidsaw-dex/tests/fixtures/classes.dex")
}
fn parse_classes_dex() -> CrossLayerContext {
let path = classes_dex_path();
CrossLayerContext::parse(&path, None).expect("classes.dex parses")
}
#[test]
fn classify_dex_only_jvm_descriptor_routes_to_dex() {
let ctx = parse_classes_dex();
let route = classify_decompile_target(&ctx, "LMinimal;").expect("classify");
assert!(matches!(route, DecompileRoute::DexClass("LMinimal;")));
}
#[test]
fn classify_dex_only_java_fqcn_routes_to_dex() {
let ctx = parse_classes_dex();
let route = classify_decompile_target(&ctx, "com.foo.Bar").expect("classify");
assert!(matches!(route, DecompileRoute::DexClass("com.foo.Bar")));
}
#[test]
fn classify_dex_only_bare_name_routes_to_dex() {
let ctx = parse_classes_dex();
let route = classify_decompile_target(&ctx, "Minimal").expect("classify");
assert!(matches!(route, DecompileRoute::DexClass("Minimal")));
}
#[test]
fn classify_dex_only_numeric_routes_to_dex() {
let ctx = parse_classes_dex();
let route = classify_decompile_target(&ctx, "0").expect("classify");
assert!(matches!(route, DecompileRoute::DexClass("0")));
}
#[test]
fn dex_decompile_resolves_jvm_descriptor() {
let ctx = parse_classes_dex();
let value = dex_decompile(&ctx, None, Some("LMinimal;"))
.expect("dex_decompile resolves LMinimal;");
let classes = value.get("classes").and_then(|v| v.as_array()).expect("classes array");
assert_eq!(classes.len(), 1, "exactly one match for LMinimal;");
}
#[test]
fn dex_decompile_resolves_java_fqcn() {
let ctx = parse_classes_dex();
let value = dex_decompile(&ctx, None, Some("Minimal"))
.expect("dex_decompile resolves bare class name");
let classes = value.get("classes").and_then(|v| v.as_array()).expect("classes array");
assert_eq!(classes.len(), 1, "exactly one match for Minimal");
}
#[test]
fn normalize_inner_class_fqcn_matches_descriptor_literally() {
let r = normalize_dex_class_search("com.foo.Outer$Inner");
let re = regex::Regex::new(&r).expect("normalized regex compiles");
assert!(
re.is_match("Lcom/foo/Outer$Inner;"),
"regex {r:?} must match descriptor 'Lcom/foo/Outer$Inner;'"
);
assert!(
!re.is_match("Lcom/foo/OuterXInner;"),
"regex {r:?} must NOT coincidentally match 'Lcom/foo/OuterXInner;' (dot matched X)"
);
assert!(
!re.is_match("Lcom-foo-Outer$Inner;"),
"regex {r:?} must NOT coincidentally match 'Lcom-foo-Outer$Inner;' (dot matched -)"
);
}
#[test]
fn normalize_bare_inner_class_anchors_correctly() {
let r = normalize_dex_class_search("Outer$Inner");
let re = regex::Regex::new(&r).expect("normalized regex compiles");
assert!(
re.is_match("Lcom/foo/Outer$Inner;"),
"regex {r:?} must match 'Lcom/foo/Outer$Inner;'"
);
assert!(
re.is_match("LOuter$Inner;"),
"regex {r:?} must match 'LOuter$Inner;'"
);
assert!(
!re.is_match("LOuter$InnerX;"),
"regex {r:?} must NOT match a class whose name doesn't end at $Inner"
);
}
#[test]
fn normalize_jvm_descriptor_passes_through() {
let r = normalize_dex_class_search("Lcom/foo/Bar;");
assert_eq!(r, "Lcom/foo/Bar;", "JVM descriptors pass through verbatim");
}
#[test]
fn normalize_user_regex_passes_through() {
let r = normalize_dex_class_search("L(com|org)/foo/Bar;");
assert_eq!(r, "L(com|org)/foo/Bar;", "regex patterns pass through");
}
#[test]
fn dex_decompile_dotted_fqcn_normalizes_to_descriptor() {
let ctx = parse_classes_dex();
let err = dex_decompile(&ctx, None, Some("com.bogus.Minimal")).expect_err(
"dotted FQCN com.bogus.Minimal must NOT coincidentally match LMinimal;"
);
let msg = format!("{err}");
assert!(
msg.contains("no matching class"),
"expected no-match error, got: {msg}"
);
}