Skip to main content

asc_engine/names/
mappings.rs

1//! The names asc hands to decompiler backends (docs/decompile-backends.md
2//! §6.2, §7): new names for classes, fields and methods, keyed in asc's
3//! member notation, the one `findrefs` prints (`Lowner;->name:Type`,
4//! `Lowner;->name(args)Ret`).
5//!
6//! Keys are always the names as they are in the DEX (obfuscated, if the
7//! program is); values are the names to show instead, which come only from
8//! a mapping file the user supplies (R8 / ProGuard `mapping.txt`, see
9//! [`NameMappings::from_proguard`]). asc keeps no renames of its own and
10//! derives no deobfuscation aliases; without a mapping file every name stays
11//! as it is. Only decompiled source uses these names: query outputs
12//! (`findrefs`, smali, …) keep the DEX's own descriptors.
13
14use super::dalvik_types;
15use crate::Engine;
16use sha2::{Digest, Sha256};
17use std::collections::BTreeMap;
18use std::fmt;
19use std::sync::Arc;
20
21/// Why a mapping entry was refused.
22#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
23#[non_exhaustive]
24pub enum MappingError {
25    /// A key that is not a class descriptor.
26    #[error("not a class descriptor: {0:?} (want Lpkg/Name;)")]
27    Class(String),
28    /// A key that is not a field signature.
29    #[error("not a field signature: {0:?} (want Lowner;->name:Type)")]
30    Field(String),
31    /// A key that is not a method signature, or names a constructor.
32    #[error("not a method signature: {0:?} (want Lowner;->name(args)Ret)")]
33    Method(String),
34    /// A new member name that would not read as one identifier.
35    #[error("not a usable new name: {0:?}")]
36    NewName(String),
37    /// A new class name that is not a usable class descriptor.
38    #[error("not a usable new class name: {0:?} (want Lpkg/Name;)")]
39    NewClass(String),
40}
41
42/// A field as `Lowner;->name:Type`. Every part borrows from the signature.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44#[non_exhaustive]
45pub struct FieldRef<'a> {
46    /// The declaring class descriptor.
47    pub class: &'a str,
48    /// The field name.
49    pub name: &'a str,
50    /// The field's type descriptor.
51    pub ty: &'a str,
52}
53
54impl<'a> FieldRef<'a> {
55    /// Parse the canonical form exactly as written (no trimming, no other
56    /// spellings), so `parse(s).to_string() == s`.
57    ///
58    /// # Errors
59    ///
60    /// [`MappingError::Field`] when `signature` is not of that form.
61    pub fn parse(signature: &'a str) -> Result<FieldRef<'a>, MappingError> {
62        let bad = || MappingError::Field(signature.to_string());
63        let (class, member) = signature.split_once("->").ok_or_else(bad)?;
64        let (name, ty) = member.split_once(':').ok_or_else(bad)?;
65        let field_type = matches!(dalvik_types(ty).as_deref(), Some([t]) if *t != "V");
66        (is_class_descriptor(class) && is_simple_name(name) && field_type).then_some(FieldRef { class, name, ty }).ok_or_else(bad)
67    }
68}
69
70impl fmt::Display for FieldRef<'_> {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}->{}:{}", self.class, self.name, self.ty)
73    }
74}
75
76/// A method as `Lowner;->name(args)Ret`; `proto` is `(args)Ret`, which
77/// tells overloads apart. Every part borrows from the signature.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
79#[non_exhaustive]
80pub struct MethodRef<'a> {
81    /// The declaring class descriptor.
82    pub class: &'a str,
83    /// The method name (`<init>` / `<clinit>` included).
84    pub name: &'a str,
85    /// `(args)Ret`.
86    pub proto: &'a str,
87}
88
89impl<'a> MethodRef<'a> {
90    /// Parse the canonical form exactly as written, like [`FieldRef::parse`].
91    ///
92    /// # Errors
93    ///
94    /// [`MappingError::Method`] when `signature` is not of that form.
95    pub fn parse(signature: &'a str) -> Result<MethodRef<'a>, MappingError> {
96        let bad = || MappingError::Method(signature.to_string());
97        let (class, member) = signature.split_once("->").ok_or_else(bad)?;
98        let open = member.find('(').ok_or_else(bad)?;
99        let (name, proto) = member.split_at(open);
100        let (params, ret) = proto[1..].split_once(')').ok_or_else(bad)?;
101        let name_ok = is_simple_name(name) || name == "<init>" || name == "<clinit>";
102        let params_ok = params.is_empty() || dalvik_types(params).is_some_and(|ps| !ps.contains(&"V"));
103        let ret_ok = matches!(dalvik_types(ret).as_deref(), Some([_]));
104        (is_class_descriptor(class) && name_ok && params_ok && ret_ok).then_some(MethodRef { class, name, proto }).ok_or_else(bad)
105    }
106}
107
108impl fmt::Display for MethodRef<'_> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        write!(f, "{}->{}{}", self.class, self.name, self.proto)
111    }
112}
113
114/// `Lpkg/Name;`, `LName;`, `Lpkg/Outer$Inner;`: a class (not array or
115/// primitive) descriptor whose path segments are simple names.
116pub(super) fn is_class_descriptor(s: &str) -> bool {
117    s.strip_prefix('L').and_then(|d| d.strip_suffix(';')).is_some_and(|inner| inner.split('/').all(is_simple_name))
118}
119
120/// A DEX simple name as far as the notation needs: non-empty and free of
121/// the characters that delimit descriptors and signatures. Anything else
122/// (`ۢۤܺ`, `-`, digits first) is the obfuscator's business and allowed.
123fn is_simple_name(s: &str) -> bool {
124    !s.is_empty() && !s.contains(['/', ';', '.', '[', '(', ')', ':', '<', '>'])
125}
126
127/// A new name must read as one identifier in any target language's output
128/// once the backend has escaped it; the notation's delimiters, whitespace
129/// and control characters never do.
130fn is_new_name(s: &str) -> bool {
131    is_simple_name(s) && !s.chars().any(|c| c.is_whitespace() || c.is_control())
132}
133
134/// A new class name is a whole descriptor (the package may change too) whose
135/// every segment is a usable new name; `$` (nesting) is just a character.
136fn is_new_class(s: &str) -> bool {
137    s.strip_prefix('L').and_then(|d| d.strip_suffix(';')).is_some_and(|inner| inner.split('/').all(is_new_name))
138}
139
140/// Resolves the names decompiled source shows, in process (the embedded
141/// decompiler) or behind a protocol (external backends get [`NameMappings`]
142/// as JSON). Keys are the names in the DEX (`Lz/c;`, `Lz/c;->a(I)V`);
143/// `None` keeps the name the program has.
144pub trait NameResolver {
145    /// The full new descriptor of the class `descriptor`: `Lz/c;` →
146    /// `Lcom/example/Cart;`. The package may differ from the DEX's; for a
147    /// nested class the value is the whole binary name (`…/Cart$Line;`).
148    fn class(&self, descriptor: &str) -> Option<&str>;
149    /// The new name of the field `Lowner;->name:Type`.
150    fn field(&self, signature: &str) -> Option<&str>;
151    /// The new name of the method `Lowner;->name(args)Ret`.
152    fn method(&self, signature: &str) -> Option<&str>;
153}
154
155/// The §6.2 `mappings` object: `classes` maps a descriptor to the class's
156/// full new descriptor, `fields` and `methods` map a member signature to the
157/// member's new simple name. Keys use the DEX's own names throughout, so a
158/// member of a renamed class is still keyed by the old class descriptor. Keys are checked on insertion and
159/// kept sorted, so iteration, serialisation and [`NameMappings::hash`] do
160/// not depend on insertion order. The default (empty) value is the identity.
161#[derive(Debug, Clone, Default, PartialEq, Eq)]
162pub struct NameMappings {
163    classes: BTreeMap<String, String>,
164    fields: BTreeMap<String, String>,
165    methods: BTreeMap<String, String>,
166}
167
168/// SHA-256 of a [`NameMappings`]' contents, part of the decompile cache key
169/// (§8). Equal mappings hash equally whatever order they were built in.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
171pub struct MappingsHash([u8; 32]);
172
173impl MappingsHash {
174    /// The raw digest.
175    pub fn as_bytes(&self) -> &[u8; 32] {
176        &self.0
177    }
178}
179
180/// Lowercase hex.
181impl fmt::Display for MappingsHash {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        self.0.iter().try_for_each(|b| write!(f, "{b:02x}"))
184    }
185}
186
187impl NameMappings {
188    /// No renames: every name stays as it is.
189    pub fn new() -> NameMappings {
190        NameMappings::default()
191    }
192
193    /// Whether there are no renames at all.
194    pub fn is_empty(&self) -> bool {
195        self.classes.is_empty() && self.fields.is_empty() && self.methods.is_empty()
196    }
197
198    /// Rename the class `descriptor` to the descriptor `new_descriptor`
199    /// (`Lz/c;` → `Lcom/example/Cart;`); returns the name it replaced.
200    ///
201    /// # Errors
202    ///
203    /// [`MappingError::Class`] for a bad `descriptor`,
204    /// [`MappingError::NewClass`] for a bad `new_descriptor`.
205    pub fn insert_class(&mut self, descriptor: &str, new_descriptor: &str) -> Result<Option<String>, MappingError> {
206        if !is_class_descriptor(descriptor) {
207            return Err(MappingError::Class(descriptor.to_string()));
208        }
209        if !is_new_class(new_descriptor) {
210            return Err(MappingError::NewClass(new_descriptor.to_string()));
211        }
212        Ok(self.classes.insert(descriptor.to_string(), new_descriptor.to_string()))
213    }
214
215    /// Rename the field `signature` (`Lowner;->name:Type`); returns the name
216    /// it replaced.
217    ///
218    /// # Errors
219    ///
220    /// [`MappingError::Field`] for a bad `signature`,
221    /// [`MappingError::NewName`] for a bad `new_name`.
222    pub fn insert_field(&mut self, signature: &str, new_name: &str) -> Result<Option<String>, MappingError> {
223        FieldRef::parse(signature)?;
224        Ok(self.fields.insert(signature.to_string(), checked_new_name(new_name)?))
225    }
226
227    /// Rename the method `signature` (`Lowner;->name(args)Ret`); returns the
228    /// name it replaced. Constructors and static initialisers keep their
229    /// names, so `<init>` / `<clinit>` are refused.
230    ///
231    /// # Errors
232    ///
233    /// [`MappingError::Method`] for a bad `signature` or a constructor,
234    /// [`MappingError::NewName`] for a bad `new_name`.
235    pub fn insert_method(&mut self, signature: &str, new_name: &str) -> Result<Option<String>, MappingError> {
236        let method = MethodRef::parse(signature)?;
237        if method.name.starts_with('<') {
238            return Err(MappingError::Method(signature.to_string()));
239        }
240        Ok(self.methods.insert(signature.to_string(), checked_new_name(new_name)?))
241    }
242
243    /// Descriptor → new descriptor, sorted by the DEX's descriptor.
244    pub fn classes(&self) -> &BTreeMap<String, String> {
245        &self.classes
246    }
247
248    /// Field signature → new name, sorted by signature.
249    pub fn fields(&self) -> &BTreeMap<String, String> {
250        &self.fields
251    }
252
253    /// Method signature → new name, sorted by signature.
254    pub fn methods(&self) -> &BTreeMap<String, String> {
255        &self.methods
256    }
257
258    /// The entries a request needs: the classes `keep` accepts and the
259    /// members they own. For a slice, `keep` should accept every class the
260    /// slice refers to, not only its targets, since references to renamed
261    /// classes and members are renamed in the output too.
262    #[must_use]
263    pub fn restricted_to(&self, keep: impl Fn(&str) -> bool) -> NameMappings {
264        let owned_by_kept = |signature: &String| signature.split_once("->").is_some_and(|(class, _)| keep(class));
265        let filter = |map: &BTreeMap<String, String>, keep_key: &dyn Fn(&String) -> bool| -> BTreeMap<String, String> {
266            map.iter().filter(|(k, _)| keep_key(k)).map(|(k, v)| (k.clone(), v.clone())).collect()
267        };
268        NameMappings {
269            classes: filter(&self.classes, &|d| keep(d)),
270            fields: filter(&self.fields, &owned_by_kept),
271            methods: filter(&self.methods, &owned_by_kept),
272        }
273    }
274
275    /// The stable hash of the entries: sections in a fixed order, each entry
276    /// length-prefixed so no two different mappings feed the same bytes.
277    pub fn hash(&self) -> MappingsHash {
278        let mut h = Sha256::new();
279        h.update(b"asc-name-mappings/2");
280        for (tag, map) in [(b'c', &self.classes), (b'f', &self.fields), (b'm', &self.methods)] {
281            h.update([tag]);
282            h.update((map.len() as u64).to_le_bytes());
283            for (k, v) in map {
284                for s in [k, v] {
285                    h.update((s.len() as u64).to_le_bytes());
286                    h.update(s.as_bytes());
287                }
288            }
289        }
290        MappingsHash(h.finalize().into())
291    }
292}
293
294fn checked_new_name(new_name: &str) -> Result<String, MappingError> {
295    if is_new_name(new_name) { Ok(new_name.to_string()) } else { Err(MappingError::NewName(new_name.to_string())) }
296}
297
298impl NameResolver for NameMappings {
299    fn class(&self, descriptor: &str) -> Option<&str> {
300        self.classes.get(descriptor).map(String::as_str)
301    }
302
303    fn field(&self, signature: &str) -> Option<&str> {
304        self.fields.get(signature).map(String::as_str)
305    }
306
307    fn method(&self, signature: &str) -> Option<&str> {
308        self.methods.get(signature).map(String::as_str)
309    }
310}
311
312impl Engine {
313    /// Show the names of `mappings` in decompiled source (typically read
314    /// from the user's `mapping.txt` with [`NameMappings::from_proguard_file`]).
315    /// Shared, since one mapping file serves many engines.
316    pub fn with_name_mappings(mut self, mappings: impl Into<Arc<NameMappings>>) -> Engine {
317        self.name_mappings = mappings.into();
318        self
319    }
320
321    /// The names this program's classes and members go by in decompiled
322    /// output: the set given to [`Engine::with_name_mappings`], or the
323    /// identity (empty) when there is none, in which case every class and
324    /// member keeps its own name (backends still escape names their language
325    /// cannot spell). Narrow it to a request with [`NameMappings::restricted_to`].
326    pub fn name_mappings(&self) -> &NameMappings {
327        &self.name_mappings
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn signatures_round_trip_through_their_parts() {
337        let fields = ["Lbin/mt/json/JSONObject;->a:I", "Ll/ۢۤܺ;->ۢ:[Ljava/lang/String;", "LA$B$1;->this$0:LA$B;", "LC;->-x:J"];
338        for s in fields {
339            assert_eq!(FieldRef::parse(s).unwrap().to_string(), s, "{s}");
340        }
341        let f = FieldRef::parse("Lpkg/Outer$Inner;->val$x:Lpkg/Outer;").unwrap();
342        assert_eq!(f, FieldRef { class: "Lpkg/Outer$Inner;", name: "val$x", ty: "Lpkg/Outer;" });
343
344        let methods = ["Lc;->run()V", "Lc;->run(I)V", "Lc;-><init>(ILjava/lang/String;[[B)V", "La/b$c;->ۢۤ(La/b;)La/b$c;"];
345        for s in methods {
346            assert_eq!(MethodRef::parse(s).unwrap().to_string(), s, "{s}");
347        }
348        let m = MethodRef::parse("Lpkg/Outer$1;->access$000(Lpkg/Outer;)I").unwrap();
349        assert_eq!(m, MethodRef { class: "Lpkg/Outer$1;", name: "access$000", proto: "(Lpkg/Outer;)I" });
350    }
351
352    #[test]
353    fn non_canonical_signatures_are_refused() {
354        for s in ["Lc;->f", "Lc;->f:V", "Lc;->:I", "c->f:I", "[I->f:I", "Lc;->f:I ", " Lc;->f:I", "Lc;->f:Lx", "La//b;->f:I"] {
355            assert_eq!(FieldRef::parse(s), Err(MappingError::Field(s.into())), "{s}");
356        }
357        for s in ["Lc;->run", "Lc;->run()", "Lc;->run(V)V", "Lc;->(I)V", "Lc;->run(I)VV", "Lc;->a.b()V", "com.a.B->run()V"] {
358            assert_eq!(MethodRef::parse(s), Err(MappingError::Method(s.into())), "{s}");
359        }
360    }
361
362    #[test]
363    fn overloads_are_distinct_entries() {
364        let mut m = NameMappings::new();
365        m.insert_method("Lc;->a(I)V", "setCount").unwrap();
366        m.insert_method("Lc;->a(Ljava/lang/String;)V", "setName").unwrap();
367        m.insert_method("Lc;->a()I", "count").unwrap();
368        assert_eq!(m.method("Lc;->a(I)V"), Some("setCount"));
369        assert_eq!(m.method("Lc;->a(Ljava/lang/String;)V"), Some("setName"));
370        assert_eq!(m.method("Lc;->a()I"), Some("count"));
371        assert_eq!(m.method("Lc;->a(J)V"), None);
372        // A field and a method of one name are separate too.
373        m.insert_field("Lc;->a:I", "count").unwrap();
374        assert_eq!((m.field("Lc;->a:I"), m.field("Lc;->a:J")), (Some("count"), None));
375    }
376
377    #[test]
378    fn nested_classes_keep_their_descriptor() {
379        let mut m = NameMappings::new();
380        m.insert_class("Lbin/mt/json/JSONObject$Member;", "Lbin/mt/json/JSONObject$Entry;").unwrap();
381        m.insert_class("Ll/ۢۤܺ;", "Lbin/mt/json/LinkedTable;").unwrap();
382        assert_eq!(m.class("Lbin/mt/json/JSONObject$Member;"), Some("Lbin/mt/json/JSONObject$Entry;"));
383        assert_eq!(m.class("Lbin/mt/json/JSONObject;"), None);
384        // The package may change: mapping files name the original class in full.
385        assert_eq!(m.class("Ll/ۢۤܺ;"), Some("Lbin/mt/json/LinkedTable;"));
386    }
387
388    #[test]
389    fn bad_keys_and_names_are_refused() {
390        let mut m = NameMappings::new();
391        assert_eq!(m.insert_class("com.a.B", "LX;"), Err(MappingError::Class("com.a.B".into())));
392        assert_eq!(m.insert_class("[La;", "LX;"), Err(MappingError::Class("[La;".into())));
393        for bad in ["X", "com.a.X", "Lcom/a/X", "Lcom//X;", "L;", "Lcom/a b;", "[LX;", "I"] {
394            assert_eq!(m.insert_class("La;", bad), Err(MappingError::NewClass(bad.into())), "{bad:?}");
395        }
396        assert_eq!(m.insert_method("Lc;-><init>()V", "make"), Err(MappingError::Method("Lc;-><init>()V".into())));
397        for bad in ["", "a b", "a.b", "La;", "x\n"] {
398            assert_eq!(m.insert_field("Lc;->f:I", bad), Err(MappingError::NewName(bad.into())), "{bad:?}");
399        }
400        assert!(m.is_empty());
401        assert_eq!(m.insert_class("La;", "Lp/X;"), Ok(None));
402        assert_eq!(m.insert_class("La;", "Lq/Y$Z;"), Ok(Some("Lp/X;".into())));
403    }
404
405    fn sample(order: &[usize]) -> NameMappings {
406        let entries: [(&str, &str); 6] = [
407            ("c:Lb;", "Lcom/x/Bee;"),
408            ("c:La$1;", "Lcom/x/Aye$1;"),
409            ("f:Lb;->x:I", "size"),
410            ("f:La;->x:I", "count"),
411            ("m:La;->x(I)V", "put"),
412            ("m:La;->x()V", "clear"),
413        ];
414        let mut m = NameMappings::new();
415        for &i in order {
416            let (key, new) = entries[i];
417            match key.split_at(2) {
418                ("c:", d) => m.insert_class(d, new),
419                ("f:", s) => m.insert_field(s, new),
420                (_, s) => m.insert_method(s, new),
421            }
422            .unwrap();
423        }
424        m
425    }
426
427    #[test]
428    fn order_and_hash_do_not_depend_on_insertion() {
429        let (a, b) = (sample(&[0, 1, 2, 3, 4, 5]), sample(&[5, 3, 1, 4, 0, 2]));
430        assert_eq!(a, b);
431        assert_eq!(a.hash(), b.hash());
432        assert_eq!(a.classes().keys().collect::<Vec<_>>(), ["La$1;", "Lb;"]);
433        assert_eq!(a.fields().keys().collect::<Vec<_>>(), ["La;->x:I", "Lb;->x:I"]);
434        assert_eq!(a.methods().keys().collect::<Vec<_>>(), ["La;->x()V", "La;->x(I)V"]);
435    }
436
437    #[test]
438    fn hash_is_pinned_and_tells_contents_apart() {
439        // Pinned: the hash is part of on-disk cache keys, so changing the
440        // encoding must be deliberate (bump the `asc-name-mappings/N` tag).
441        assert_eq!(NameMappings::new().hash().to_string(), "eb2a94d4e5862de88b4a5b6162d60ce874a1b831bf01c074921ec976becdad4e");
442        assert_eq!(sample(&[0, 1, 2, 3, 4, 5]).hash().to_string(), "6eb831a95f17919d15479902c0e6f8d04471980d53733d9be3650064ef0cea5c");
443        // The same strings in another section, or split differently, differ.
444        let mut class = NameMappings::new();
445        class.insert_class("La;", "Lab;").unwrap();
446        let mut other = NameMappings::new();
447        other.insert_class("La;", "La;").unwrap();
448        assert_ne!(class.hash(), other.hash());
449        let mut field = NameMappings::new();
450        field.insert_field("La;->f:I", "x").unwrap();
451        let mut method = NameMappings::new();
452        method.insert_method("La;->f()I", "x").unwrap();
453        assert_ne!(field.hash(), method.hash());
454        assert_ne!(field.hash(), NameMappings::new().hash());
455    }
456
457    #[test]
458    fn restriction_keeps_the_classes_and_their_members() {
459        let m = sample(&[0, 1, 2, 3, 4, 5]);
460        let only_a = m.restricted_to(|c| c.starts_with("La"));
461        assert_eq!(only_a.classes().keys().collect::<Vec<_>>(), ["La$1;"]);
462        assert_eq!(only_a.fields().keys().collect::<Vec<_>>(), ["La;->x:I"]);
463        assert_eq!(only_a.methods().len(), 2);
464        assert!(m.restricted_to(|_| false).is_empty());
465        assert_eq!(m.restricted_to(|_| true), m);
466    }
467
468    #[test]
469    fn engine_mappings_are_the_identity_unless_supplied() {
470        let engine = Engine::new("unused.apk");
471        assert!(engine.name_mappings().is_empty());
472        assert_eq!(engine.name_mappings().class("La;"), None);
473        let supplied = Arc::new(sample(&[0, 1, 2, 3, 4, 5]));
474        let engine = Engine::new("unused.apk").with_name_mappings(Arc::clone(&supplied));
475        assert_eq!(engine.name_mappings(), &*supplied);
476        assert_eq!(engine.name_mappings().class("Lb;"), Some("Lcom/x/Bee;"));
477    }
478}