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}