Skip to main content

jdc_core/
rename.rs

1//! Case-collision class renames. Obfuscators emit class pairs differing
2//! only in letter case (`X/CUA` vs `X/Cua`); on case-insensitive
3//! filesystems both map to one physical file — one class's output was
4//! lost. The front-end driver computes a deterministic rename (suffix
5//! `_2`, `_3`, … on the simple name, first-in-sorted-group unchanged)
6//! and installs it here before workers spawn; declarations, references
7//! and file names then all agree.
8
9use std::borrow::Cow;
10use crate::fx::FxHashMap as HashMap;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::OnceLock;
13
14/// internal → display. Entries whose value equals the key mark
15/// file-level classes that were NOT renamed (they anchor the prefix
16/// walk below).
17static RENAMES: OnceLock<HashMap<String, String>> = OnceLock::new();
18static REVERSE: OnceLock<HashMap<String, String>> = OnceLock::new();
19
20/// Field display renames: owner → list of (name, type descriptor, display).
21/// Obfuscators rename a synthetic outer-reference field (`this$0`) to a
22/// one-char name that COLLIDES with a real field of the same class
23/// (`final a a;` beside `private Runnable a;`) — legal in bytecode (fields
24/// resolve by index), illegal in source. Every consumer of a field name —
25/// the declaration and every reference — funnels through here, so one
26/// registry keeps all sites consistent. Vec per owner: a class has a
27/// handful of collisions at most, and the borrow-based lookup avoids
28/// allocation on the (rare) collision-hit path.
29static FIELD_RENAMES: OnceLock<HashMap<std::sync::Arc<str>, Vec<FieldRename>>> = OnceLock::new();
30static FIELD_ACTIVE: AtomicBool = AtomicBool::new(false);
31/// Owners with at least one rename — hot paths (every lifted field/invoke
32/// ref) probe this set instead of rebuilding descriptors for the whole
33/// corpus when the registry is active for a handful of classes.
34static FIELD_OWNERS: OnceLock<std::collections::HashSet<std::sync::Arc<str>>> = OnceLock::new();
35
36/// One renamed field of one owner class.
37pub struct FieldRename {
38    pub name: std::sync::Arc<str>,
39    pub desc: std::sync::Arc<str>,
40    pub display: std::sync::Arc<str>,
41}
42
43/// Install the field rename registry (empty map = inactive, one relaxed
44/// atomic read per lookup on clean corpora).
45pub fn set_field_renames(map: HashMap<std::sync::Arc<str>, Vec<FieldRename>>) {
46    if !map.is_empty() {
47        FIELD_ACTIVE.store(true, Ordering::Relaxed);
48        let _ = FIELD_OWNERS.set(
49            map.iter()
50                .filter(|(_, v)| !v.is_empty())
51                .map(|(k, _)| k.clone())
52                .collect(),
53        );
54    }
55    let _ = FIELD_RENAMES.set(map);
56}
57
58/// Does this owner class have ANY renamed member? One hash probe; the
59/// lift-time field/method ref paths skip descriptor reconstruction
60/// entirely for the (overwhelming majority of) untouched owners.
61#[inline]
62pub fn owner_has_renames(owner: &str) -> bool {
63    FIELD_ACTIVE.load(Ordering::Relaxed)
64        && FIELD_OWNERS.get().is_some_and(|s| s.contains(owner))
65}
66
67/// Fast probe: is the member-rename registry non-empty? Callers on hot
68/// paths (every method/field reference) skip descriptor reconstruction
69/// entirely when it is not.
70#[inline]
71pub fn member_rename_active() -> bool {
72    FIELD_ACTIVE.load(Ordering::Relaxed)
73}
74
75/// Display name for a field, when it was renamed.
76#[inline]
77pub fn field_display(owner: &str, name: &str, desc: &str) -> Option<&'static str> {
78    if !FIELD_ACTIVE.load(Ordering::Relaxed) {
79        return None;
80    }
81    FIELD_RENAMES
82        .get()?
83        .get(owner)?
84        .iter()
85        .find(|fr| &*fr.name == name && &*fr.desc == desc)
86        .map(|fr| fr.display.as_ref())
87}
88static ACTIVE: AtomicBool = AtomicBool::new(false);
89
90/// Install the map (call once, before worker threads spawn). Identity
91/// entries for unrenamed file-level classes are expected.
92/// Read-access to the installed class rename map (None before install).
93/// Lets the front-end derive per-package aggregates ONCE instead of
94/// probing apply_class_rename per candidate (an O(files × package)
95/// hot loop — WhatsApp package X has ~10k classes).
96pub fn with_renames<R>(f: impl FnOnce(&HashMap<String, String>) -> R) -> Option<R> {
97    RENAMES.get().map(f)
98}
99
100pub fn set_class_renames(map: HashMap<String, String>) {
101    if !map.is_empty() {
102        ACTIVE.store(true, Ordering::Relaxed);
103    }
104    // Reverse index of real (non-identity) renames: a display name's
105    // `$`-prefixes must resolve for nested references (`x0$a$a2$a3` —
106    // the `x0$a$a2` prefix names no pool class, it is the display of
107    // one).
108    let rev: HashMap<String, String> = map
109        .iter()
110        .filter(|(k, v)| k != v)
111        .map(|(k, v)| (v.clone(), k.clone()))
112        .collect();
113    let _ = REVERSE.set(rev);
114    let _ = RENAMES.set(map);
115}
116
117/// True when `internal` is the DISPLAY form of a renamed class (the
118/// value side of a real rename). print_class_name uses this to keep
119/// dotting through renamed nesting prefixes.
120#[inline]
121pub fn is_renamed_display(internal: &str) -> bool {
122    REVERSE.get().is_some_and(|m| m.contains_key(internal))
123}
124
125/// The POOL-side (pre-rename) internal of a DISPLAY name — the mirror
126/// of `apply_class_rename` over the REVERSE index. Pool lookups fed
127/// with display names (the ddcroot root-package migration renames
128/// `Foo$Bar` to `ddcroot/Foo$Bar`) miss without this: jdc-core's
129/// nested_display then read the class as off-pool and dotted its `$`
130/// against a flat declaration (lark's R8-outer-deleted
131/// UserCustomStatusExtraParams$* enum family, "不可见" ×138).
132#[inline]
133pub fn unrename_class(internal: &str) -> Cow<'_, str> {
134    if !ACTIVE.load(Ordering::Relaxed) {
135        return Cow::Borrowed(internal);
136    }
137    let Some(map) = REVERSE.get() else {
138        return Cow::Borrowed(internal);
139    };
140    if let Some(o) = map.get(internal) {
141        return Cow::Owned(o.clone());
142    }
143    let mut s = internal;
144    while let Some(i) = s.rfind('$') {
145        if s.as_bytes().get(i + 1) == Some(&b'/') {
146            s = &s[..i];
147            continue;
148        }
149        s = &s[..i];
150        if let Some(o) = map.get(s) {
151            let suffix = &internal[s.len()..];
152            return Cow::Owned(format!("{o}{suffix}"));
153        }
154    }
155    Cow::Borrowed(internal)
156}
157
158/// The display form of an internal class name. Exact file-level match
159/// first; otherwise walk `$` boundaries from the right — a nested
160/// reference (`X/Cua$Inner`) follows its renamed file-level owner, and
161/// an unrenamed file-level prefix anchors the walk (identity).
162#[inline]
163pub fn apply_class_rename(internal: &str) -> Cow<'_, str> {
164    if !ACTIVE.load(Ordering::Relaxed) {
165        return Cow::Borrowed(internal);
166    }
167    let Some(map) = RENAMES.get() else {
168        return Cow::Borrowed(internal);
169    };
170    if let Some(d) = map.get(internal) {
171        return if d == internal {
172            Cow::Borrowed(internal)
173        } else {
174            Cow::Owned(d.clone())
175        };
176    }
177    let mut s = internal;
178    while let Some(i) = s.rfind('$') {
179        // A `$` followed by `/` is a PACKAGE-path dollar (desugar's
180        // `j$/util/...`), never a nesting boundary — matching the
181        // prefix as a class rewrote the whole desugar tree through a
182        // root-package class `j` once default-pkg relocation gave it a
183        // non-identity display (deepseek `ddcroot.j$.util` 程序包不存在
184        // ×8.6k).
185        if s.as_bytes().get(i + 1) == Some(&b'/') {
186            s = &s[..i];
187            continue;
188        }
189        s = &s[..i];
190        match map.get(s) {
191            Some(d) if d != s => {
192                let suffix = &internal[s.len()..];
193                return Cow::Owned(format!("{d}{suffix}"));
194            }
195            // An unrenamed file-level prefix owns this name: identity.
196            Some(_) => return Cow::Borrowed(internal),
197            None => continue,
198        }
199    }
200    Cow::Borrowed(internal)
201}