asc-engine 0.2.1

High-level asc-rs engine: parallel APK class lookup, cross-DEX reference search, single-class rebuild + disassembly, manifest decoding.
Documentation
//! `mapping.txt` from a real R8 run (tests/r8, regenerated by `gen.sh`)
//! against the DEX R8 wrote alongside it: every key must name a class or
//! member the DEX defines, with the DEX's own (obfuscated) descriptors.

use asc_dex::Dex;
use asc_engine::names::{NameMappings, NameResolver};
use std::collections::BTreeSet;

const DEX: &[u8] = include_bytes!("r8/classes.dex");
const MAPPING: &str = include_str!("r8/mapping.txt");

/// Classes, `Lc;->f:T` fields and `Lc;->m(args)Ret` methods the DEX defines.
#[derive(Default)]
struct Defined {
    classes: BTreeSet<String>,
    fields: BTreeSet<String>,
    methods: BTreeSet<String>,
}

fn defined(dex: &Dex<'_>) -> Defined {
    let mut d = Defined::default();
    for class in dex.class_defs() {
        let class = class.unwrap();
        d.classes.insert(dex.type_descriptor(class.class_idx).unwrap());
        if class.class_data_off == 0 {
            continue;
        }
        let data = dex.class_data(class.class_data_off).unwrap();
        for f in data.fields() {
            let id = dex.field_id(f.field_idx).unwrap();
            let owner = dex.type_descriptor(u32::from(id.class_idx)).unwrap();
            let ty = dex.type_descriptor(u32::from(id.type_idx)).unwrap();
            d.fields.insert(format!("{owner}->{}:{ty}", dex.string(id.name_idx).unwrap()));
        }
        for m in data.methods() {
            let id = dex.method_id(m.method_idx).unwrap();
            let owner = dex.type_descriptor(u32::from(id.class_idx)).unwrap();
            let proto = dex.proto_id(u32::from(id.proto_idx)).unwrap();
            let params: String = dex
                .proto_params(u32::from(id.proto_idx))
                .unwrap()
                .into_iter()
                .map(|t| dex.type_descriptor(u32::from(t)).unwrap())
                .collect();
            let ret = dex.type_descriptor(proto.return_type_idx).unwrap();
            d.methods.insert(format!("{owner}->{}({params}){ret}", dex.string(id.name_idx).unwrap()));
        }
    }
    d
}

#[test]
fn r8_mapping_keys_are_the_dex_names() {
    let parsed = NameMappings::from_proguard(MAPPING).unwrap();
    assert_eq!(parsed.warnings, []);
    let m = parsed.mappings;
    let dex = Dex::parse(DEX).unwrap();
    let d = defined(&dex);

    for key in m.classes().keys() {
        assert!(d.classes.contains(key), "class {key} not in the DEX: {:?}", d.classes);
    }
    for key in m.fields().keys() {
        assert!(d.fields.contains(key), "field {key} not in the DEX: {:?}", d.fields);
    }
    for key in m.methods().keys() {
        assert!(d.methods.contains(key), "method {key} not in the DEX: {:?}", d.methods);
    }

    // Every class R8 renamed, packages changed (`-repackageclasses z`).
    let classes: Vec<(&str, &str)> = m.classes().iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
    assert_eq!(
        classes,
        [
            ("Lz/a;", "Lcom/example/shop/model/Cart$1;"),
            ("Lz/b;", "Lcom/example/shop/model/Cart$Line$Tag;"),
            ("Lz/c;", "Lcom/example/shop/model/Cart$Line;"),
            ("Lz/d;", "Lcom/example/shop/model/Cart;"),
            ("Lz/e;", "Lcom/example/shop/model/Item;"),
            ("Lz/f;", "Lcom/example/shop/util/Pricing;"),
        ]
    );
    // Every member the DEX defines under a new name has one; only names R8
    // kept (`main`, `run`, `toString`, constructors) are absent.
    let unnamed: Vec<&String> = d.methods.iter().filter(|s| m.method(s).is_none()).filter(|s| !s.contains("-><")).collect();
    assert_eq!(unnamed, ["Lcom/example/shop/Main;->main([Ljava/lang/String;)V", "Lz/a;->run()V", "Lz/e;->toString()Ljava/lang/String;"]);
    assert!(d.fields.iter().all(|f| m.field(f).is_some()), "{:?}", d.fields);

    // Member types are the DEX's: R8's residual signatures where it changed
    // them (`List` → `ArrayList`, a dropped parameter), translated original
    // types elsewhere.
    assert_eq!(m.field("Lz/d;->a:Ljava/util/ArrayList;"), Some("items"));
    assert_eq!(m.field("Lz/c;->a:Lz/d;"), Some("owner"));
    assert_eq!(m.field("Lz/a;->a:[Ljava/lang/String;"), Some("val$args"));
    assert_eq!(m.method("Lz/c;->a([Lz/e;)J"), Some("cost"));
    assert_eq!(m.method("Lz/d;->a(Ljava/lang/String;I)V"), Some("add"));
    assert_eq!(m.method("Lz/d;->b(Lz/e;)V"), Some("add"));
    assert_eq!(m.method("Lz/d;->g()[Lz/c;"), Some("lines"));
    assert_eq!(m.method("Lz/f;->b([Lz/c;)J"), Some("total"));
    assert_eq!(m.method("Lz/f;->c([Lz/e;Z)J"), Some("total"));
    // Inlined frames (`Cart.access$000` inside `Cart$1.run`, a whole call
    // chain inside `Main.main`) rename nothing.
    assert!(m.methods().values().all(|name| !name.contains('.') && name != "access$000"));
}