ddc_dec/lib.rs
1//! ddc-dec — DEX → Java decompiler front-end over `jdc-core`.
2//!
3//! Pipeline per method: DEX code units → CFG → per-block register→IR
4//! lifting → `jdc-core` structuring/conversion → refinement passes →
5//! emission. Class-level rendering mirrors jcdc's `classdec` but reads
6//! DEX metadata (no signatures, no generics).
7
8pub mod cfg;
9pub mod cure_pass;
10pub mod classdec;
11pub mod fwdb;
12
13pub use classdec::{sanitize_fq, ClassOptions};
14pub mod ctx;
15pub mod lift;
16pub mod method;
17pub mod passes;
18pub mod platform;
19mod refscan;
20pub mod ssa_census;
21
22use jdc_core::FxHashMap as HashMap;
23
24use ddc_dex::annotations::{self, EncodedValue};
25
26/// A resolved static-field initializer.
27#[derive(Debug, Clone)]
28pub enum StaticValue {
29 Int(i64),
30 Float(f32),
31 Double(f64),
32 Str(String),
33 Type(String),
34 Boolean(bool),
35 Null,
36 /// (declaring class, field name, field descriptor) — enum constants /
37 /// const field refs. The desc participates in the rename-registry key
38 /// (owner, name, desc) so the render site can consult field_display.
39 Field(String, String, String),
40 Other,
41}
42use ddc_dex::{ClassDef, DexFile};
43use jdc_core::types::{
44 parse_field_descriptor, parse_method_descriptor, JavaType, MethodDescriptor,
45};
46
47// ---------------------------------------------------------------------------
48// Access flags (Dalvik numbering, JVM-compatible subset).
49// ---------------------------------------------------------------------------
50pub mod access {
51 pub const ACC_PUBLIC: u32 = 0x1;
52 pub const ACC_PRIVATE: u32 = 0x2;
53 pub const ACC_PROTECTED: u32 = 0x4;
54 pub const ACC_STATIC: u32 = 0x8;
55 pub const ACC_FINAL: u32 = 0x10;
56 pub const ACC_SYNCHRONIZED: u32 = 0x20;
57 pub const ACC_BRIDGE: u32 = 0x40;
58 pub const ACC_VARARGS: u32 = 0x80;
59 pub const ACC_NATIVE: u32 = 0x100;
60 pub const ACC_INTERFACE: u32 = 0x200;
61 pub const ACC_ABSTRACT: u32 = 0x400;
62 pub const ACC_STRICT: u32 = 0x800;
63 pub const ACC_SYNTHETIC: u32 = 0x1000;
64 pub const ACC_ANNOTATION: u32 = 0x2000;
65 pub const ACC_ENUM: u32 = 0x4000;
66 pub const ACC_CONSTRUCTOR: u32 = 0x1_0000;
67 pub const ACC_DECLARED_SYNCHRONIZED: u32 = 0x2_0000;
68}
69
70/// One field of a pooled class.
71#[derive(Debug, Clone)]
72pub struct PoolField {
73 /// Shared with the dex string table (field names repeat massively
74 /// under R8's single-letter namespace — one Arc per unique name).
75 pub name: std::sync::Arc<str>,
76 /// Field descriptor (`I`, `Ljava/lang/String;`, ...) — shared per
77 /// unique descriptor through the same table.
78 pub desc: std::sync::Arc<str>,
79 pub access: u32,
80 pub is_static: bool,
81}
82
83/// One method of a pooled class.
84#[derive(Debug, Clone)]
85pub struct PoolMethod {
86 /// Shared with the dex string table (the idx IS the dedup key).
87 pub name: std::sync::Arc<str>,
88 /// Method descriptor, shared per proto (a dex's proto table is the
89 /// descriptor dedup layer — 1.9M methods on lark share ~1 proto
90 /// table's worth of unique descriptors).
91 pub desc: std::sync::Arc<str>,
92 pub access: u32,
93 pub code_off: u32,
94 pub debug_info_off: u32,
95 /// Which DEX image the body lives in.
96 pub dex_idx: usize,
97}
98
99impl PoolMethod {
100 pub fn parsed_desc(&self) -> Option<MethodDescriptor> {
101 parse_method_descriptor(&self.desc)
102 }
103 pub fn is_static(&self) -> bool {
104 self.access & access::ACC_STATIC != 0
105 }
106 pub fn is_abstract_or_native(&self) -> bool {
107 self.access & (access::ACC_ABSTRACT | access::ACC_NATIVE) != 0
108 }
109}
110
111/// One pool entry: either already built, or name-registered with its
112/// (dex index, class_def index) locator for on-demand construction.
113enum ClassEntry {
114 Eager(Box<PoolClass>),
115 Lazy {
116 at: (usize, usize),
117 pc: std::sync::OnceLock<Box<PoolClass>>,
118 },
119 /// Name registered without an image: used by the two-tier single-class
120 /// path to keep global name-dependent rules (case/package/deshadow)
121 /// identical while only the target's dependency closure is parsed.
122 NameOnly,
123}
124
125/// A class materialized from one `class_def_item` (with ids resolved to
126/// names and the class data expanded).
127#[derive(Debug, Clone)]
128pub struct PoolClass {
129 pub name: String,
130 pub access: u32,
131 /// `None` for `java/lang/Object` roots.
132 pub super_name: Option<String>,
133 pub interfaces: Vec<String>,
134 pub source_file: Option<String>,
135 pub static_fields: Vec<PoolField>,
136 pub instance_fields: Vec<PoolField>,
137 pub direct_methods: Vec<PoolMethod>,
138 pub virtual_methods: Vec<PoolMethod>,
139 /// Static initial values aligned with the head of `static_fields`.
140 pub static_values: Vec<StaticValue>,
141 /// Nesting evidence with annotation ids resolved to names.
142 pub nesting: ResolvedNesting,
143 /// Which DEX image the class body lives in (provenance header).
144 pub dex_idx: usize,
145}
146
147/// Nesting evidence, names resolved at pooling time.
148#[derive(Debug, Clone, Default)]
149pub struct ResolvedNesting {
150 pub enclosing_class: Option<String>,
151 /// (class internal name, method name) of the enclosing method.
152 pub enclosing_method: Option<(String, String)>,
153 pub member_classes: Vec<String>,
154}
155
156impl PoolClass {
157 pub fn is_interface(&self) -> bool {
158 self.access & access::ACC_INTERFACE != 0
159 }
160
161 pub fn is_enum(&self) -> bool {
162 self.access & access::ACC_ENUM != 0
163 }
164
165 pub fn is_synthetic(&self) -> bool {
166 self.access & access::ACC_SYNTHETIC != 0
167 }
168
169 pub fn is_static_nested(&self) -> bool {
170 self.access & access::ACC_STATIC != 0
171 }
172
173 /// All methods in declaration order.
174 pub fn all_methods(&self) -> impl Iterator<Item = &PoolMethod> {
175 self.direct_methods
176 .iter()
177 .chain(self.virtual_methods.iter())
178 }
179
180 /// Find a method by name + descriptor.
181 pub fn find_method(&self, name: &str, desc: &str) -> Option<&PoolMethod> {
182 self.all_methods()
183 .find(|m| &*m.name == name && &*m.desc == desc)
184 }
185
186 /// Constructors `<init>` matching `arity` descriptor arguments.
187 pub fn ctors_by_arity(&self, arity: usize) -> Vec<&PoolMethod> {
188 self.all_methods()
189 .filter(|m| &*m.name == "<init>")
190 .filter(|m| {
191 m.parsed_desc()
192 .map(|d| d.args.len() == arity)
193 .unwrap_or(false)
194 })
195 .collect()
196 }
197
198 pub fn field_flags_of(&self, name: &str) -> Option<u32> {
199 self.static_fields
200 .iter()
201 .chain(self.instance_fields.iter())
202 .find(|f| f.name.as_ref() == name)
203 .map(|f| f.access)
204 }
205}
206
207/// Multi-DEX class pool: classes from all images, first definition wins.
208/// One facade-map entry: the public base plus, for PARTIALLY covered
209/// parts, the exact static members the base declares (None = the whole
210/// part rewrites; Some = per-member gate). Single map = one hash probe
211/// per static reference (two maps measured +2.4s on weibo).
212type FacadeTarget = (
213 String,
214 Option<
215 std::sync::Arc<(
216 jdc_core::FxHashSet<(String, String)>,
217 jdc_core::FxHashSet<String>,
218 )>,
219 >,
220);
221
222pub struct DexPool {
223 /// Images as Arcs: `dex()` hands out snapshots whose borrows stay
224 /// valid for the Arc's lifetime (decompile workers hold them), while
225 /// `release_images` empties the image data through exclusive access
226 /// when all of an image's classes are done (full-decompile driver
227 /// only — progressive callers keep images whole).
228 dexes: Vec<std::sync::Arc<DexFile>>,
229 /// Human label per image ("weibo!classes.dex") for the provenance
230 /// header; defaults to "dex N" until the CLI names the inputs.
231 pub dex_labels: Vec<String>,
232 /// Name → class entry. Eager pools materialize everything at add
233 /// time (full decompile, tests); lazy pools register names only and
234 /// materialize on first `get` (progressive `getclass` — one class's
235 /// PoolClass costs annotation reads, 145k of them cost ~0.2s).
236 classes: HashMap<String, ClassEntry>,
237 /// Class names in insertion order (stable for output).
238 pub order: Vec<String>,
239 /// Per-image remaining class count once `arm_retirement` fires.
240 retire_counts: std::sync::Mutex<Vec<u64>>,
241 retire_armed: std::sync::atomic::AtomicBool,
242 /// name → outer (computed once; `$` heuristic + dalvik annotations).
243 outers: std::sync::OnceLock<HashMap<String, Option<String>>>,
244 /// outer → direct children (computed once).
245 children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
246 /// Per-image hot-reference interning (parallel to `dexes`).
247 ref_caches: Vec<DexRefCache>,
248 /// Set when the renderer materializes a NameOnly entry: the two-tier
249 /// single-class path then falls back to a full parse instead of
250 /// emitting degraded types.
251 name_only_missed: std::sync::atomic::AtomicBool,
252 /// First NameOnly class the renderer asked for (diagnostics/fallback).
253 name_only_miss: std::sync::Mutex<Option<String>>,
254 /// Synthetic-static accessor code snapshots keyed by (dex_idx,
255 /// code_off). `inline_accessors` reads accessor bodies ACROSS images
256 /// during worker runs; image retirement can release those bytes
257 /// mid-run, making the inline decision — and with it var numbering
258 /// downstream — dependent on worker completion interleaving
259 /// (observed: reqable enum-constant args flipping `var0`/`var1`
260 /// between identical runs; DDC_NORETIRE=1 stable). The full-decompile
261 /// driver fills this BEFORE `arm_retirement`; progressive/lazy pools
262 /// never retire and fall through to live reads.
263 accessor_code: std::sync::OnceLock<AccessorSnapshots>,
264 /// Cached package → direct-class simple names (import-collision gate).
265 pkg_simples:
266 std::sync::OnceLock<jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>>>,
267 /// First path segments of all packages (`v2` for `v2/n`): local or
268 /// field names equal to one capture package-qualified renders
269 /// (`v2.n.a`) — deshadow_locals consults this.
270 root_segs: std::sync::OnceLock<jdc_core::FxHashSet<String>>,
271 /// Partially-covered facade parts (see kotlin_facade_partial_map).
272 /// Kotlin multi-file facade parts → public facade (see
273 /// `kotlin_facade_map`).
274 kotlin_facades: std::sync::OnceLock<jdc_core::FxHashMap<String, FacadeTarget>>,
275 /// Per class internal name: the set of `<init>` ARGUMENT descriptors
276 /// (the `(..)` inner text) REFERENCED through that class anywhere in
277 /// the pool. Dex resolves a `<init>` method-ref up the superclass
278 /// chain, so a subclass `super(..)` may target `C.<init>(sig)` that C
279 /// does NOT declare (an ancestor does) — Java has no inherited ctors,
280 /// so C needs a bridge. Computed once from the method-id tables
281 /// (parsed, survive image retirement). See `inherited_ctor_bridges`.
282 ctor_refs: std::sync::OnceLock<jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>>,
283}
284
285/// Raw code_item bytes of every synthetic-static accessor, keyed by
286/// (dex_idx, code_off) — see `snapshot_accessor_code`.
287type AccessorSnapshots = HashMap<(usize, u32), std::sync::Arc<[u8]>>;
288
289/// Lazily-filled, thread-shared reference interning for one image.
290///
291/// The lifter used to rebuild class names, field/method names and WHOLE
292/// method descriptors per INSTRUCTION (`method_ref` alone: N param
293/// Strings → `format!` descriptor → re-parse — 6-10 allocations per
294/// invoke). Every entry here is built at most once per (image, table
295/// index); lifts and the endless IR clones downstream are refcount
296/// bumps. Slots are `OnceLock` so worker threads fill them race-free
297/// without a lock on the hot path.
298pub(crate) struct DexRefCache {
299 proto_descs: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::MethodDescriptor>>]>,
300 type_names: Box<[std::sync::OnceLock<std::sync::Arc<str>>]>,
301 type_javas: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::JavaType>>]>,
302}
303
304impl DexRefCache {
305 fn new(protos: usize, types: usize) -> Self {
306 DexRefCache {
307 proto_descs: (0..protos).map(|_| std::sync::OnceLock::new()).collect(),
308 type_names: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
309 type_javas: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
310 }
311 }
312}
313
314impl DexPool {
315 pub fn new() -> Self {
316 DexPool {
317 dexes: Vec::new(),
318 retire_armed: std::sync::atomic::AtomicBool::new(false),
319 dex_labels: Vec::new(),
320 classes: HashMap::default(),
321 order: Vec::new(),
322 retire_counts: std::sync::Mutex::new(Vec::new()),
323 outers: std::sync::OnceLock::new(),
324 children: std::sync::OnceLock::new(),
325 ref_caches: Vec::new(),
326 name_only_missed: std::sync::atomic::AtomicBool::new(false),
327 name_only_miss: std::sync::Mutex::new(None),
328 accessor_code: std::sync::OnceLock::new(),
329 pkg_simples: std::sync::OnceLock::new(),
330 root_segs: std::sync::OnceLock::new(),
331 kotlin_facades: std::sync::OnceLock::new(),
332 ctor_refs: std::sync::OnceLock::new(),
333 }
334 }
335
336 /// Referenced `<init>` argument-descriptor set for a class internal
337 /// name (see `ctor_refs`). Builds the whole index on first call by
338 /// scanning every image's method-id table for `<init>` refs — cheap
339 /// (flat table, integer name-idx compare against the interned
340 /// "<init>" string) and done once for the whole decompile.
341 pub fn ctor_ref_sigs(&self, class: &str) -> Option<&jdc_core::FxHashSet<String>> {
342 self.ctor_refs
343 .get_or_init(|| {
344 let mut map: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
345 jdc_core::FxHashMap::default();
346 for dex in &self.dexes {
347 let n = dex.method_count();
348 for i in 0..n {
349 let m = dex.method(i as u32);
350 if dex.string(m.name_idx) != "<init>" {
351 continue;
352 }
353 let desc = dex.proto_desc(m.proto_idx);
354 let (Some(lo), Some(hi)) = (desc.find('('), desc.find(')')) else {
355 continue;
356 };
357 map.entry(dex.class_name(m.class_idx))
358 .or_default()
359 .insert(desc[lo + 1..hi].to_string());
360 }
361 }
362 map
363 })
364 .get(class)
365 }
366
367 /// Shared `MethodDescriptor` of a proto (one parse per image).
368 pub fn proto_desc_parsed(
369 &self,
370 di: usize,
371 proto_idx: u32,
372 ) -> std::sync::Arc<jdc_core::types::MethodDescriptor> {
373 let Some(dex) = self.dexes.get(di) else {
374 return std::sync::Arc::new(jdc_core::types::MethodDescriptor {
375 args: Vec::new(),
376 ret: jdc_core::types::JavaType::Void,
377 });
378 };
379 let build = || {
380 std::sync::Arc::new(parse_proto_desc(dex, proto_idx))
381 };
382 match self
383 .ref_caches
384 .get(di)
385 .and_then(|c| c.proto_descs.get(proto_idx as usize))
386 {
387 Some(slot) => slot.get_or_init(build).clone(),
388 None => build(),
389 }
390 }
391
392 /// Shared internal class name of a type id (one strip+alloc per image).
393 pub fn type_name_arc(&self, di: usize, type_idx: u32) -> std::sync::Arc<str> {
394 let Some(dex) = self.dexes.get(di) else {
395 return std::sync::Arc::from("");
396 };
397 let build = || std::sync::Arc::from(dex.class_name(type_idx).as_str());
398 match self
399 .ref_caches
400 .get(di)
401 .and_then(|c| c.type_names.get(type_idx as usize))
402 {
403 Some(slot) => slot.get_or_init(build).clone(),
404 None => build(),
405 }
406 }
407
408 /// Shared parsed type of a type id.
409 pub fn type_java(&self, di: usize, type_idx: u32) -> std::sync::Arc<jdc_core::types::JavaType> {
410 let Some(dex) = self.dexes.get(di) else {
411 return std::sync::Arc::new(jdc_core::types::JavaType::Object(
412 std::sync::Arc::from("java/lang/Object"),
413 ));
414 };
415 let build = || std::sync::Arc::new(desc_type(dex.type_name(type_idx)));
416 match self
417 .ref_caches
418 .get(di)
419 .and_then(|c| c.type_javas.get(type_idx as usize))
420 {
421 Some(slot) => slot.get_or_init(build).clone(),
422 None => build(),
423 }
424 }
425
426 /// name → outer map, computed lazily once.
427 fn outer_map(&self) -> &HashMap<String, Option<String>> {
428 self.outers.get_or_init(|| {
429 self.order
430 .iter()
431 .map(|n| (n.clone(), find_outer_name(self, n)))
432 .collect()
433 })
434 }
435
436 /// The outer class of `name`, from the cached map (borrowed).
437 pub fn outer_of(&self, name: &str) -> Option<&str> {
438 self.outer_map().get(name).and_then(|o| o.as_deref())
439 }
440
441 /// Direct nested children of `internal` (any `$` depth 1), cached.
442 /// (Perf: returns a borrowed slice — the owned-Vec clone ran once per
443 /// class on 98k-class runs.)
444 pub fn children_of(&self, internal: &str) -> &[String] {
445 if self.children.get().is_none() {
446 // Build FIRST, set ONCE: the previous shape set an empty map
447 // to claim the OnceLock and then silently failed to store the
448 // real index (`let _ = set(...)` on an initialized lock) —
449 // children_of returned [] forever, so member nested classes
450 // were neither inlined nor emitted as files (Guard$Report
451 // vanished whole; every corpus run since the borrow refactor
452 // dropped them, invisible to the syntax-only gate).
453 let mut idx: HashMap<String, Vec<String>> = HashMap::default();
454 for name in &self.order {
455 if let Some(outer) = self.outer_of(name).map(str::to_string) {
456 idx.entry(outer).or_default().push(name.clone());
457 }
458 }
459 // A racing thread may have set an identical map first — the
460 // index is a pure function of `order`, so either copy is right.
461 let _ = self.children.set(idx);
462 }
463 self.children
464 .get()
465 .and_then(|m| m.get(internal))
466 .map(|v| v.as_slice())
467 .unwrap_or(&[])
468 }
469
470 /// Adds one image; duplicate class names keep their first definition.
471 /// Returns the image's index — call `set_dex_label` with it to give the
472 /// provenance header a real origin instead of the default "dex N".
473 pub fn add_dex(&mut self, dex: DexFile) -> usize {
474 let dex_idx = self.dexes.len();
475 // Pre-size the name map: 240k-entry growth rehashed repeatedly
476 // (reserve_rehash showed up in corpus profiles).
477 self.classes.reserve(dex.class_defs.len());
478 // The annotation reader borrows the image; pool classes are built
479 // before ownership moves into `self.dexes` (no full-image copy).
480 for cd in &dex.class_defs {
481 let name = dex.class_name(cd.class_idx);
482 if self.classes.contains_key(&name) {
483 continue;
484 }
485 let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
486 self.classes.insert(name.clone(), ClassEntry::Eager(Box::new(pc)));
487 self.order.push(name);
488 }
489 self.ref_caches
490 .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
491 self.dexes.push(std::sync::Arc::new(dex));
492 self.retire_counts.lock().unwrap().push(0);
493 self.dex_labels.push(format!("dex {}", dex_idx));
494 dex_idx
495 }
496
497 /// Registers class NAMES only; the PoolClass (annotation reads
498 /// included) is materialized on first `get`. Duplicate class names
499 /// keep their first definition, matching `add_dex`.
500 /// Register class names without any image. Later `add_dex_lazy` for a
501 /// real image keeps the first registration (first-wins, matching the
502 /// eager/lazy builders); names never materialize.
503 pub fn add_names(&mut self, names: impl IntoIterator<Item = String>) {
504 for name in names {
505 if self.classes.contains_key(&name) {
506 continue;
507 }
508 self.classes.insert(name.clone(), ClassEntry::NameOnly);
509 self.order.push(name);
510 }
511 }
512
513 pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
514 let dex_idx = self.dexes.len();
515 self.classes.reserve(dex.class_defs.len());
516 for (ci, cd) in dex.class_defs.iter().enumerate() {
517 let name = dex.class_name(cd.class_idx);
518 if self.classes.contains_key(&name) {
519 continue;
520 }
521 self.classes.insert(
522 name.clone(),
523 ClassEntry::Lazy {
524 at: (dex_idx, ci),
525 pc: std::sync::OnceLock::new(),
526 },
527 );
528 self.order.push(name);
529 }
530 self.ref_caches
531 .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
532 self.dexes.push(std::sync::Arc::new(dex));
533 self.retire_counts.lock().unwrap().push(0);
534 self.dex_labels.push(format!("dex {}", dex_idx));
535 dex_idx
536 }
537
538 /// Label the image at `idx` (see `add_dex`).
539 pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
540 if let Some(slot) = self.dex_labels.get_mut(idx) {
541 *slot = label.into();
542 }
543 }
544
545 pub fn len(&self) -> usize {
546 self.classes.len()
547 }
548
549 pub fn is_empty(&self) -> bool {
550 self.classes.is_empty()
551 }
552
553 pub fn get(&self, internal: &str) -> Option<&PoolClass> {
554 if matches!(self.classes.get(internal), Some(ClassEntry::NameOnly)) {
555 self.name_only_missed
556 .store(true, std::sync::atomic::Ordering::Relaxed);
557 let mut slot = self.name_only_miss.lock().unwrap();
558 if slot.is_none() {
559 *slot = Some(internal.to_string());
560 }
561 return None;
562 }
563 self.get_inner(internal, None)
564 }
565
566 /// Clear the NameOnly-materialization marker (call before rendering).
567 pub fn clear_name_only_miss(&self) {
568 self.name_only_missed
569 .store(false, std::sync::atomic::Ordering::Relaxed);
570 *self.name_only_miss.lock().unwrap() = None;
571 }
572
573 /// Name of the first NameOnly class the renderer requested.
574 pub fn name_only_miss_name(&self) -> Option<String> {
575 self.name_only_miss.lock().unwrap().clone()
576 }
577
578 /// True when rendering needed a class whose image was not parsed.
579 pub fn name_only_missed(&self) -> bool {
580 self.name_only_missed
581 .load(std::sync::atomic::Ordering::Relaxed)
582 }
583
584 /// `get` with optional pre-taken raw-image snapshots (one per
585 /// image). `materialize_all` hoists the `DexFile::raw()` lock out of
586 /// the per-class loop — the parallel materialize used to take the
587 /// same per-image lock once PER CLASS across all threads.
588 fn get_inner(&self, internal: &str, raws: Option<&[&[u8]]>) -> Option<&PoolClass> {
589 match self.classes.get(internal)? {
590 ClassEntry::NameOnly => None,
591 ClassEntry::Eager(pc) => Some(&**pc),
592 ClassEntry::Lazy { at, pc } => {
593 if let Some(built) = pc.get() {
594 return Some(&**built);
595 }
596 let (di, ci) = *at;
597 let dex = self.dexes.get(di)?;
598 let cd = dex.class_defs.get(ci)?;
599 let raw = match raws {
600 Some(r) => r.get(di).copied().unwrap_or(&[]),
601 None => dex.raw(),
602 };
603 let built = Box::new(pool_class_of(dex, raw, cd, di));
604 Some(&**pc.get_or_init(move || built))
605 }
606 }
607 }
608
609 /// Materialize every lazy entry (parallel across the given chunk
610 /// count). After this the pool is observationally identical to an
611 /// eagerly-built one — including the annotation-aware outer-map —
612 /// while the annotation reads ran on worker threads instead of the
613 /// serial pool build (~0.2s on weibo).
614 pub fn materialize_all(&self, threads: usize) {
615 let keys: Vec<String> = self
616 .classes
617 .iter()
618 .filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
619 .map(|(k, _)| k.clone())
620 .collect();
621 if keys.is_empty() {
622 return;
623 }
624 // One raw() snapshot per image for the whole sweep (see
625 // get_inner). Safe window: materialize_all completes before
626 // retirement is armed, so no image releases while these slices
627 // are alive.
628 let raws: Vec<&[u8]> = self.dexes.iter().map(|d| d.raw()).collect();
629 let threads = threads.max(1);
630 let next = std::sync::atomic::AtomicUsize::new(0);
631 std::thread::scope(|scope| {
632 for _ in 0..threads {
633 scope.spawn(|| loop {
634 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
635 let Some(k) = keys.get(i) else { break };
636 let _ = self.get_inner(k, Some(&raws));
637 });
638 }
639 });
640 }
641
642 /// `get` without materializing: None for not-yet-built lazy entries.
643 fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
644 match self.classes.get(internal)? {
645 ClassEntry::NameOnly => None,
646 ClassEntry::Eager(pc) => Some(&**pc),
647 ClassEntry::Lazy { pc, .. } => pc.get().map(|b| &**b),
648 }
649 }
650
651 /// Name registered (no materialization).
652 pub fn has_name(&self, internal: &str) -> bool {
653 self.classes.contains_key(internal)
654 }
655
656 /// One image as an Arc snapshot (borrows of the snapshot live as
657 /// long as the caller holds the Arc).
658 pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
659 self.dexes.get(idx).cloned()
660 }
661
662 pub fn dex_count(&self) -> usize {
663 self.dexes.len()
664 }
665
666 /// Raw image list (install-phase whole-program scans).
667 pub fn dexes(&self) -> &[std::sync::Arc<ddc_dex::DexFile>] {
668 &self.dexes
669 }
670
671 /// First path segments of every package in the pool (cached once).
672 pub fn root_pkg_segs(&self) -> &jdc_core::FxHashSet<String> {
673 self.root_segs.get_or_init(|| {
674 self.order
675 .iter()
676 .filter_map(|n| n.find('/').map(|i| n[..i].to_string()))
677 .collect()
678 })
679 }
680
681 /// Package → simple names of its direct classes, cached once per
682 /// pool (the import-collision gate consults it per class — the
683 /// uncached scan was O(classes²) and 10×'d weixin's wall time).
684 pub fn package_simples(
685 &self,
686 ) -> &jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>> {
687 // Arc'd sets: the per-file import machinery BORROWS a package's
688 // set (an Arc clone) instead of deep-copying it — WhatsApp's
689 // ~10k-class package paid two full copies per class (~200M
690 // String allocations), the #2/#3 CPU frames on its profile.
691 self.pkg_simples.get_or_init(|| {
692 let mut m: jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>> =
693 jdc_core::FxHashMap::default();
694 let mut raw: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
695 jdc_core::FxHashMap::default();
696 for n in &self.order {
697 if let Some((pkg, simple)) = n.rsplit_once('/') {
698 raw.entry(pkg.to_string())
699 .or_default()
700 .insert(simple.to_string());
701 }
702 }
703 m.extend(raw.into_iter().map(|(k, v)| (k, std::sync::Arc::new(v))));
704 m
705 })
706 }
707
708 /// Kotlin multi-file class facades: the compiler splits a file facade
709 /// (`kotlin.text.StringsKt`) into package-private parts
710 /// (`StringsKt__StringsKt`, `StringsKt__StringsJVMKt`) and call sites
711 /// target the PARTS directly. Java cannot access a package-private
712 /// class cross-package ("StringsKt__StringsKt在kotlin.text中不是公共的"
713 /// — 1.4k lark root errors), but the public facade EXTENDS the parts,
714 /// so static members resolve through it: rewrite the reference's owner
715 /// class to the facade. part → base, gated on base existing, being
716 /// public, and transitively extending the part (static inheritance is
717 /// the resolution guarantee).
718 pub fn kotlin_facade_map(&self) -> &jdc_core::FxHashMap<String, FacadeTarget> {
719 self.kotlin_facades.get_or_init(|| {
720 let t_fb = std::time::Instant::now();
721 let mut m: jdc_core::FxHashMap<String, FacadeTarget> =
722 jdc_core::FxHashMap::default();
723 let mut npartial = 0usize;
724 for name in &self.order {
725 let Some(i) = name.find("__") else { continue };
726 let base = &name[..i];
727 if base.is_empty() {
728 continue;
729 }
730 let Some(basec) = self.get(base) else {
731 continue;
732 };
733 if basec.access & crate::access::ACC_PUBLIC == 0 {
734 continue;
735 }
736 // base must transitively extend the part...
737 let mut cur = basec.super_name.clone();
738 let mut hops = 0u32;
739 let mut reaches = false;
740 while let Some(c) = cur {
741 if c == *name {
742 reaches = true;
743 break;
744 }
745 if hops >= 8 {
746 break;
747 }
748 hops += 1;
749 cur = self.get(&c).and_then(|cc| cc.super_name.clone());
750 }
751 // ...OR cover the part's static members: the Kotlin
752 // compiler MERGES every part's statics into the public
753 // facade, but the facade's super chain only includes ONE
754 // part — lark's `CollectionsKt extends
755 // CollectionsKt___CollectionsKt` leaves
756 // `CollectionsKt__CollectionsJVMKt` uncovered (463
757 // 不是公共的 errors). Member coverage (name+desc for
758 // methods, name for fields) is the precise rewrite
759 // condition: every reference through the facade then
760 // resolves exactly as through the part.
761 if reaches {
762 m.insert(name.clone(), (base.to_string(), None));
763 continue;
764 }
765 // Member-coverage fallback: the Kotlin compiler MERGES
766 // the parts' statics into the public facade, but R8 can
767 // sever the super chain (lark: `CollectionsKt extends
768 // CollectionsKt___CollectionsKt extends
769 // kotlin/collections/s` — `__CollectionsJVMKt` is off
770 // the chain, 463 不是公共的 errors) and may drop facade
771 // members no direct facade call needed. Map at (name,
772 // desc) granularity then: covered members rewrite, the
773 // rest keep the part owner (a latent error either way —
774 // Java cannot access the package-private part).
775 let Some(part) = self.get(name) else {
776 continue;
777 };
778 let base_methods: jdc_core::FxHashSet<(&str, &str)> = basec
779 .direct_methods
780 .iter()
781 .chain(basec.virtual_methods.iter())
782 .map(|mm| (&*mm.name, &*mm.desc))
783 .collect();
784 let base_fields: jdc_core::FxHashSet<&str> =
785 basec.static_fields.iter().map(|f| &*f.name).collect();
786 let mut methods: jdc_core::FxHashSet<(String, String)> =
787 jdc_core::FxHashSet::default();
788 let mut fields: jdc_core::FxHashSet<String> =
789 jdc_core::FxHashSet::default();
790 let mut all = true;
791 for mm in part
792 .direct_methods
793 .iter()
794 .chain(part.virtual_methods.iter())
795 .filter(|mm| mm.access & crate::access::ACC_STATIC != 0)
796 {
797 if base_methods.contains(&(&*mm.name, &*mm.desc)) {
798 methods.insert((mm.name.to_string(), mm.desc.to_string()));
799 } else {
800 all = false;
801 }
802 }
803 for pf in &part.static_fields {
804 if base_fields.contains(&*pf.name) {
805 fields.insert(pf.name.to_string());
806 } else {
807 all = false;
808 }
809 }
810 if all {
811 m.insert(name.clone(), (base.to_string(), None));
812 } else if !methods.is_empty() || !fields.is_empty() {
813 npartial += 1;
814 m.insert(
815 name.clone(),
816 (base.to_string(), Some(std::sync::Arc::new((methods, fields)))),
817 );
818 }
819 }
820 if std::env::var("DDC_STATS").is_ok() {
821 eprintln!(
822 "[renames] facade map: entries={} partial={} build={:?}",
823 m.len(),
824 npartial,
825 t_fb.elapsed()
826 );
827 }
828 m
829 })
830 }
831
832 pub fn class_names(&self) -> impl Iterator<Item = &str> {
833 self.order.iter().map(|s| s.as_str())
834 }
835
836 /// True when `sub` is assignable to `sup` (internal names), walking the
837 /// pool's hierarchy. Unknown classes are never assignable (conservative).
838 pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
839 if sub == sup {
840 return true;
841 }
842 // Arrays: covariance by element type when both are arrays.
843 if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
844 if sub_elem.starts_with('L')
845 && sup_elem.starts_with('L')
846 && sub_elem.ends_with(';')
847 && sup_elem.ends_with(';')
848 {
849 return self.is_subtype(
850 &sub_elem[1..sub_elem.len() - 1],
851 &sup_elem[1..sup_elem.len() - 1],
852 );
853 }
854 // Multi-dimensional: recurse on the element descriptors
855 // (`[[LSavedState;` <: `[[LObject;` peels one dimension at a
856 // time); the equality fallback below stays for primitive
857 // elements (`[I` vs `[J` are invariant).
858 if sub_elem.starts_with('[') && sup_elem.starts_with('[') {
859 return self.is_subtype(sub_elem, sup_elem);
860 }
861 return sub_elem == sup_elem;
862 }
863 // Everything reference-typed is Object-assignable (the pool walk
864 // used to fall off the chain end and answer false for
865 // String <: Object — the hand-written Object-formal special case
866 // in the ambiguity pin existed for exactly this hole).
867 if sup == "java/lang/Object" {
868 return !matches!(
869 sub,
870 "I" | "Z" | "B" | "S" | "C" | "J" | "F" | "D" | "V"
871 );
872 }
873 let mut cur = self.get(sub);
874 if cur.is_none() {
875 // `sub` is a framework (or phantom) type: the embedded API
876 // database decides (phantoms are absent there too → false).
877 return crate::fwdb::is_subtype(sub, sup);
878 }
879 let mut hops = 0;
880 while let Some(c) = cur {
881 hops += 1;
882 if hops > 64 {
883 return false;
884 }
885 for i in &c.interfaces {
886 if i == sup || self.is_subtype(i, sup) {
887 return true;
888 }
889 }
890 match &c.super_name {
891 Some(s) if s == sup => return true,
892 Some(s) if s != "java/lang/Object" => {
893 match self.get(s) {
894 Some(sc) => cur = Some(sc),
895 // Framework boundary: continue in the embedded DB.
896 None => return crate::fwdb::is_subtype(s, sup),
897 }
898 }
899 _ => return false,
900 }
901 }
902 false
903 }
904}
905
906impl Default for DexPool {
907 fn default() -> Self {
908 Self::new()
909 }
910}
911
912fn array_elem(desc: &str) -> Option<&str> {
913 desc.strip_prefix('[')
914}
915
916fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
917 match v {
918 EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
919 EncodedValue::Short(x) => StaticValue::Int(*x as i64),
920 EncodedValue::Char(x) => StaticValue::Int(*x as i64),
921 EncodedValue::Int(x) => StaticValue::Int(*x as i64),
922 EncodedValue::Long(x) => StaticValue::Int(*x),
923 EncodedValue::Float(x) => StaticValue::Float(*x),
924 EncodedValue::Double(x) => StaticValue::Double(*x),
925 EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
926 EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
927 EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
928 EncodedValue::Null => StaticValue::Null,
929 EncodedValue::Enum(i) | EncodedValue::Field(i) => {
930 let f = dex.field(*i);
931 StaticValue::Field(
932 dex.class_name(f.class_idx),
933 dex.string(f.name_idx).to_string(),
934 dex.type_name(f.type_idx).to_string(),
935 )
936 }
937 _ => StaticValue::Other,
938 }
939}
940
941fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
942 let name = dex.class_name(cd.class_idx);
943 let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
944 None
945 } else {
946 let s = dex.class_name(cd.superclass_idx);
947 if s.is_empty() || s == "java/lang/Object" {
948 None
949 } else {
950 Some(s)
951 }
952 };
953 let interfaces = dex
954 .interfaces_of(cd)
955 .into_iter()
956 .map(|t| dex.class_name(t))
957 .collect();
958 let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
959 None
960 } else {
961 Some(dex.string(cd.source_file_idx).to_string())
962 };
963
964 let data = dex.class_data(cd);
965 let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
966 let f = dex.field(ef.field_idx);
967 PoolField {
968 name: dex.string_arc(f.name_idx),
969 desc: dex.type_name_arc(f.type_idx),
970 access: ef.access_flags,
971 is_static,
972 }
973 };
974 let static_fields: Vec<PoolField> = data
975 .static_fields
976 .iter()
977 .map(|f| mk_field(f, true))
978 .collect();
979 let instance_fields: Vec<PoolField> = data
980 .instance_fields
981 .iter()
982 .map(|f| mk_field(f, false))
983 .collect();
984 let mk_method = |em: &ddc_dex::EncodedMethod| {
985 let m = dex.method(em.method_idx);
986 let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
987 Some(d) => (em.code_off, d),
988 None => (0, 0),
989 };
990 PoolMethod {
991 name: dex.string_arc(m.name_idx),
992 desc: dex.proto_desc(m.proto_idx),
993 access: em.access_flags,
994 code_off,
995 debug_info_off,
996 dex_idx,
997 }
998 };
999 let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
1000 let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
1001
1002 let static_values: Vec<StaticValue> = dex
1003 .static_values(cd.static_values_off)
1004 .into_iter()
1005 .map(|v| resolve_static_value(&v, dex))
1006 .collect();
1007 let anns = if cd.annotations_off != 0 {
1008 annotations::read_class_annotations(raw, cd.annotations_off)
1009 } else {
1010 Vec::new()
1011 };
1012 let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
1013 let nesting = ResolvedNesting {
1014 enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
1015 enclosing_method: raw.enclosing_method.map(|m| {
1016 let mid = dex.method(m);
1017 (
1018 dex.class_name(mid.class_idx),
1019 dex.string(mid.name_idx).to_string(),
1020 )
1021 }),
1022 member_classes: raw
1023 .member_classes
1024 .into_iter()
1025 .map(|t| dex.class_name(t))
1026 .collect(),
1027 };
1028
1029 PoolClass {
1030 name,
1031 access: cd.access_flags,
1032 super_name,
1033 interfaces,
1034 source_file,
1035 static_fields,
1036 instance_fields,
1037 direct_methods,
1038 virtual_methods,
1039 static_values,
1040 nesting,
1041 dex_idx,
1042 }
1043}
1044
1045/// JavaType for a field/method descriptor segment.
1046/// Parse a proto's descriptor into a MethodDescriptor straight from the
1047/// tables (no string round-trip).
1048fn parse_proto_desc(
1049 dex: &ddc_dex::DexFile,
1050 proto_idx: u32,
1051) -> jdc_core::types::MethodDescriptor {
1052 let proto = dex.proto(proto_idx);
1053 let args = dex
1054 .proto_params(proto_idx)
1055 .iter()
1056 .map(|&t| desc_type(dex.type_name(t)))
1057 .collect();
1058 jdc_core::types::MethodDescriptor {
1059 args,
1060 ret: desc_type(dex.type_name(proto.return_type_idx)),
1061 }
1062}
1063
1064pub fn desc_type(desc: &str) -> JavaType {
1065 parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
1066}
1067
1068/// Nesting evidence for `internal`: dalvik annotations first, then the
1069/// `$`-name heuristic against the pool.
1070pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
1071 // Annotation refinement only for ALREADY-MATERIALIZED classes: eager
1072 // pools (full decompile) behave exactly as before; lazy pools
1073 // (progressive getclass) fall straight to the `$` chain without
1074 // materializing the world.
1075 if let Some(pc) = pool.get_if_materialized(internal) {
1076 if let Some(enc) = &pc.nesting.enclosing_class {
1077 // The annotation outer must still EXIST: R8 deletes whole
1078 // outer classes and leaves the nestlings as flat `$` files
1079 // (weixin matrix JiffiesMonitorFeature deleted,
1080 // $JiffiesSnapshot survives) — an unresolved annotation
1081 // outer made refs render dotted THROUGH a missing type
1082 // ("是在不可访问的类或接口中定义的"). Fall through to the
1083 // existence-checked `$` walk.
1084 if pool.has_name(enc) {
1085 return Some(enc.clone());
1086 }
1087 }
1088 }
1089 let mut rest = internal;
1090 while let Some(d) = rest.rfind('$') {
1091 // A `$` followed by `/` is a PACKAGE-path dollar (desugar's
1092 // `j$/util/...`), never a nesting boundary: without this the
1093 // whole j$ tree attached to a root-package class `j` as family
1094 // members (deepseek's 62k-line j.java absorbing 549 desugar
1095 // classes, double-emitted against their flat j$/ files).
1096 if rest.as_bytes().get(d + 1) == Some(&b'/') {
1097 rest = &rest[..d];
1098 continue;
1099 }
1100 let cand = &rest[..d];
1101 if pool.has_name(cand) {
1102 return Some(cand.to_string());
1103 }
1104 rest = cand;
1105 }
1106 None
1107}
1108
1109/// True when the `$` tail names a plain member class (not anonymous/local/
1110/// lambda), i.e. the class renders inside its outer's compilation unit.
1111fn clean_member_tail(rest: &str) -> bool {
1112 if rest.starts_with('-') {
1113 return false;
1114 }
1115 // A member tail can never contain `/` — that is a package-path
1116 // fragment from a package-dollar name (`j$/util/X` stripped against
1117 // a root class `j`); defensive backstop for the find_outer_name
1118 // guard.
1119 if rest.contains('/') {
1120 return false;
1121 }
1122 // d8's synthetic outline/lambda/backport classes (`X$$ExternalSynthetic…`)
1123 // are TOP-LEVEL synthetics, not nested members: `$$` is d8's marker, and
1124 // find_outer_name's `$`-chain walk mistakes them for children of `X`.
1125 // Emitting them nested strands every cross-package reference on a bare
1126 // simple name ("找不到符号 变量 ExternalSyntheticBackportWithForwarding0",
1127 // weibo ×467). Targeted at `ExternalSynthetic` only — Kotlin's
1128 // `$$serializer` / `$$delegate` stay nested members.
1129 if rest.starts_with("$ExternalSynthetic") {
1130 return false;
1131 }
1132 let tail = rest.rsplit('$').next().unwrap_or(rest);
1133 if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
1134 return false;
1135 }
1136 true
1137}
1138
1139/// Classes to emit as their own compilation units: top-level classes plus
1140/// anonymous/local/lambda-shaped ones (clean members render inline).
1141impl DexPool {
1142 /// Arm image retirement (full-decompile driver only): count one
1143 /// pending class per image by pool ownership.
1144 /// Snapshot every synthetic-static accessor body (the exact predicate
1145 /// `inline_accessors` uses) as RAW code_item bytes, so cross-image
1146 /// accessor reads during worker runs survive image retirement.
1147 /// Call AFTER materialization and BEFORE `arm_retirement`; idempotent
1148 /// (first fill wins). Raw memcpy, no decode — accessors are numerous
1149 /// (weixin ~100k) but only a fraction are ever inlined, so decoding
1150 /// stays lazy at the use site. The slice bound covers the fixed
1151 /// header + insns + try items with generous handler slack (accessor
1152 /// tries are vanishingly rare; the snapshot is a deterministic
1153 /// function of the image either way).
1154 pub fn snapshot_accessor_code(&self) {
1155 let mut snap: AccessorSnapshots = HashMap::default();
1156 let mut bytes_total = 0usize;
1157 for name in &self.order {
1158 let Some(pc) = self.get_if_materialized(name) else {
1159 continue;
1160 };
1161 for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
1162 if m.code_off == 0
1163 || !m.is_static()
1164 || m.access & crate::access::ACC_SYNTHETIC == 0
1165 {
1166 continue;
1167 }
1168 if snap.contains_key(&(m.dex_idx, m.code_off)) {
1169 continue;
1170 }
1171 let Some(dex) = self.dex(m.dex_idx) else {
1172 continue;
1173 };
1174 let raw = dex.raw();
1175 let off = m.code_off as usize;
1176 if off + 16 > raw.len() {
1177 continue;
1178 }
1179 let u2 = |o: usize| u16::from_le_bytes([raw[o], raw[o + 1]]) as usize;
1180 let u4 = |o: usize| {
1181 u32::from_le_bytes([raw[o], raw[o + 1], raw[o + 2], raw[o + 3]]) as usize
1182 };
1183 let tries = u2(off + 6);
1184 let insns = u4(off + 12);
1185 let mut end = off + 16 + 2 * insns;
1186 if tries > 0 {
1187 // pad to 4-align + try_item[tries] + handler blobs
1188 // (uleb-coded; slack covers realistic accessor shapes).
1189 end = end.next_multiple_of(4) + 8 * tries + 256 + 64 * tries;
1190 }
1191 let end = end.min(raw.len());
1192 let slice: std::sync::Arc<[u8]> = raw[off..end].into();
1193 bytes_total += slice.len();
1194 snap.insert((m.dex_idx, m.code_off), slice);
1195 }
1196 }
1197 if std::env::var("DDC_WALL").is_ok() {
1198 eprintln!(
1199 "[wall] accessor snapshot: {} entries, {} KB",
1200 snap.len(),
1201 bytes_total / 1024
1202 );
1203 }
1204 let _ = self.accessor_code.set(snap);
1205 }
1206
1207 /// Snapshotted accessor code_item bytes, when the snapshot was taken.
1208 /// Parse with `CodeItem::parse(&bytes, 0)`.
1209 pub fn accessor_code(&self, dex_idx: usize, code_off: u32) -> Option<std::sync::Arc<[u8]>> {
1210 self.accessor_code.get()?.get(&(dex_idx, code_off)).cloned()
1211 }
1212
1213 pub fn arm_retirement(&self) {
1214 self.retire_armed
1215 .store(true, std::sync::atomic::Ordering::Release);
1216 let mut counts = self.retire_counts.lock().unwrap();
1217 for c in counts.iter_mut() {
1218 *c = 0;
1219 }
1220 for entry in self.classes.values() {
1221 let di = match entry {
1222 ClassEntry::Eager(pc) => pc.dex_idx,
1223 ClassEntry::Lazy { at, .. } => at.0,
1224 ClassEntry::NameOnly => continue,
1225 };
1226 if let Some(c) = counts.get_mut(di) {
1227 *c += 1;
1228 }
1229 }
1230 }
1231
1232 /// Report one class finished; Some(image) when it was the image's
1233 /// last class (driver batches these into release_images).
1234 pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
1235 if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
1236 return None;
1237 }
1238 let entry = self.classes.get(class_name)?;
1239 let di = match entry {
1240 ClassEntry::Eager(pc) => pc.dex_idx,
1241 ClassEntry::Lazy { at, .. } => at.0,
1242 ClassEntry::NameOnly => return None,
1243 };
1244 let mut counts = self.retire_counts.lock().unwrap();
1245 match counts.get_mut(di) {
1246 Some(c) => {
1247 *c = c.saturating_sub(1);
1248 if *c == 0 {
1249 Some(di)
1250 } else {
1251 None
1252 }
1253 }
1254 None => None,
1255 }
1256 }
1257
1258 /// Release the images' inflated bytes. Safe: the driver calls this
1259 /// only when every class of each image is emitted or failed; new
1260 /// `dex()` snapshots still work (tables valid, code accessors empty).
1261 pub fn release_images(&self, indexes: &[usize]) {
1262 let mut counts = self.retire_counts.lock().unwrap();
1263 for &i in indexes {
1264 if let Some(dex) = self.dexes.get(i) {
1265 // Mark the shared image: code accessors go empty
1266 // immediately; the bytes drop when the last snapshot
1267 // drops (workers hold snapshots only mid-class).
1268 dex.mark_released();
1269 }
1270 if let Some(c) = counts.get_mut(i) {
1271 *c = u64::MAX; // released marker
1272 }
1273 }
1274 }
1275}
1276
1277pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
1278 // Progressive (lazy) pools register 100k+ names but materialize only
1279 // the target family. `outer_of` would build the global outer map and
1280 // `get(outer)` would materialize every outer class — a single-class
1281 // query paid ~450MB and hundreds of ms for a boolean. The structural
1282 // decision needs neither: `find_outer_name` already resolves the
1283 // `$`-chain against registered names, and `has_name` proves existence
1284 // without materializing.
1285 let lazy = !pool_majority_materialized(pool);
1286 pool.class_names()
1287 .filter(|name| {
1288 let outer = if lazy {
1289 find_outer_name(pool, name)
1290 } else {
1291 pool.outer_of(name).map(str::to_string)
1292 };
1293 let Some(outer) = outer else {
1294 return true;
1295 };
1296 if !pool.has_name(&outer) {
1297 return true;
1298 }
1299 // The outer can come from an ANNOTATION (EnclosingClass)
1300 // with no naming relationship to this class — obfuscated
1301 // apps pair a 1-char name with a long enclosing descriptor
1302 // (weixin), which made the old blind slice panic. Only a
1303 // real `outer$tail` prefix yields a member tail; anything
1304 // else is a standalone unit.
1305 let rest = name
1306 .strip_prefix(outer.as_str())
1307 .and_then(|t| t.strip_prefix('$'))
1308 .unwrap_or("");
1309 !clean_member_tail(rest)
1310 })
1311 .map(|n| n.to_string())
1312 .collect()
1313}
1314
1315/// Sanitized form of one internal-name segment — the shared mapping the
1316/// declaration site (`print_class_name` → `sanitize_ref`), the CLASS
1317/// lookup and the CLI writer must all agree on. Exported so the writer
1318/// cannot drift from the declaration (see `sanitize_fq`).
1319pub fn sanitize_seg(seg: &str) -> String {
1320 classdec::sanitize_fq(seg)
1321}
1322
1323/// Sanitized internal PATH form of a class name: the exact on-disk
1324/// identity `source_path` writes and the declaration claims.
1325pub fn sanitize_internal(internal: &str) -> String {
1326 internal
1327 .split('/')
1328 .map(classdec::sanitize_fq)
1329 .collect::<Vec<_>>()
1330 .join("/")
1331}
1332
1333/// Deterministic case-collision renames over the FILE-emission set:
1334/// classes whose internal names differ only in letter case cannot share
1335/// one case-insensitive directory; the first (sorted) member of each
1336/// group keeps its name, the others gain `_2`, `_3`, … on the simple
1337/// segment. The map carries identity entries for unrenamed file-level
1338/// classes (they anchor nested prefix walks in apply_class_rename).
1339pub fn case_rename_map(pool: &DexPool) -> HashMap<String, String> {
1340 use std::collections::HashMap;
1341 let mut groups: HashMap<String, Vec<String>> = HashMap::default();
1342 for t in top_level_classes(pool) {
1343 groups.entry(t.to_lowercase()).or_default().push(t);
1344 }
1345 // No `folds` clone: the fold check consults `groups` itself (the old
1346 // full HashSet<String> of every lowercase key was a ~25-30MB pure
1347 // duplicate on the large corpora). Sort in place first, then an
1348 // IMMUTABLE walk keeps the keys queryable for the fold lookup.
1349 for v in groups.values_mut() {
1350 v.sort();
1351 }
1352 let mut map = HashMap::default();
1353 for members in groups.values() {
1354 for (i, m) in members.iter().enumerate() {
1355 if i == 0 {
1356 // Identity anchors are only needed for NESTED names: a
1357 // top-level name has no `$` prefix for apply_class_rename
1358 // to compose, so storing `m -> m` there is pure overhead
1359 // (118k entries on lark).
1360 if m.contains('$') {
1361 map.insert(m.clone(), m.clone());
1362 }
1363 continue;
1364 }
1365 // Suffix the simple segment; keep incrementing if the fold
1366 // of the suffixed name is already taken by a real class.
1367 let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
1368 let mut n = i + 1;
1369 loop {
1370 let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
1371 let fold = cand.to_lowercase();
1372 if !groups.contains_key(&fold) {
1373 map.insert(m.clone(), cand);
1374 break;
1375 }
1376 n += 1;
1377 }
1378 }
1379 }
1380 map
1381}
1382
1383/// Display-level renames for two nested-class shapes that render
1384/// uncompilable or inconsistent output:
1385///
1386/// 1. A member class whose simple tail equals its rendered parent's
1387/// tail (`x0$a$a` inside `x0$a`): javac rejects a member class with
1388/// the same simple name as its immediately enclosing class ("已在类
1389/// x0中定义了类 x0.a") — a shape Kotlin lambda families hit by the
1390/// thousand (lark alone: 22k). Bump the tail: `a` → `a2`, `a3`, …
1391/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent from the
1392/// pool): the member DECLARES as `b` inside `x0` but references
1393/// printed `x0.a$b` — a type that exists nowhere. Flatten the
1394/// display name to `x0$b` so every site agrees.
1395///
1396/// Both are pure display renames keyed by internal name; declarations,
1397/// ctor names, file names and every type reference funnel through
1398/// apply_class_rename, so one map keeps all sites consistent. Children
1399/// Display-level renames for nested member classes that render
1400/// uncompilable or inconsistent output:
1401///
1402/// 1. A member whose simple tail equals ANY enclosing class's simple
1403/// name in its nesting chain. javac rejects more than the immediate
1404/// parent (`class a { static class a2 { static class a {} } }` —
1405/// member `a` two levels under `a` — is "已在类 X中定义了类 X.a");
1406/// Kotlin lambda families (`x0$a$a` inside `x0$a`) hit it by the
1407/// thousand (lark alone: 22k). Bump the tail until it clears every
1408/// ancestor name: `a` → `a3`.
1409/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent): the
1410/// member DECLARES as `b` inside `x0` but references printed
1411/// `x0.a$b` — a type that exists nowhere. Flatten the display to
1412/// `x0$b` so every site agrees.
1413///
1414/// Candidates build on the parent's DISPLAY name (ancestors are
1415/// renamed first — ascending `$`-depth order), so declarations,
1416/// constructor names, file names and every type reference stay
1417/// consistent through apply_class_rename. All are pure display
1418/// renames keyed by internal name.
1419/// In-progress-map display composition, mirroring
1420/// `rename::apply_class_rename`'s ancestor walk (the registry is not
1421/// installed yet during map construction, so mint rules must compose
1422/// through renamed ancestors themselves). A nested name whose DIRECT
1423/// parent has no entry still inherits every renamed higher prefix:
1424/// grandparent `yN5` -> `yN5_3` must turn minted `yN5$a$a2` into
1425/// `yN5_3$a$a2` (xingye case-quads: the raw-prefix entry short-
1426/// circuited the render walk — 程序包ddcroot.yN5.a不存在 ×134-family).
1427fn display_of(name: &str, map: &HashMap<String, String>) -> String {
1428 // EXACT hit first — the walk truncates the full name before ever
1429 // consulting it, and an identity entry on an ancestor prefix
1430 // (case_rename_map seeds those for every fold-group first member)
1431 // would short-circuit to the raw name (lark aa2/d$c17 minted, then
1432 // aa2/d$c$a composed against the identity of aa2/d — the child
1433 // stranded on the dead `c` prefix, 找不到符号 类 a ×1,874).
1434 if let Some(d) = map.get(name) {
1435 return d.clone();
1436 }
1437 let mut s = name;
1438 while let Some(i) = s.rfind('$') {
1439 s = &s[..i];
1440 match map.get(s) {
1441 Some(d) if d.as_str() != s => return format!("{d}{}", &name[s.len()..]),
1442 Some(_) => return name.to_string(),
1443 None => continue,
1444 }
1445 }
1446 map.get(name).cloned().unwrap_or_else(|| name.to_string())
1447}
1448
1449fn nested_collision_renames(
1450 pool: &DexPool,
1451 map: &mut HashMap<String, String>,
1452 fam_segs: &SegMap,
1453) {
1454 // Ancestors before descendants: a parent's rename must be in the
1455 // map when its children compute their display chains.
1456 let mut names: Vec<&String> = pool.order.iter().collect();
1457 names.sort_by_key(|n| n.matches('$').count());
1458 let mut assigned: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1459 // Top-level package segments. A nested member type DISPLAYED as `a2`
1460 // shadows package `a2` for every qualified `a2.x` reference in its
1461 // file (member types outrank package names in class scope), so a
1462 // renamed tail must not become a package shadow — round 47's failure
1463 // mode at nested level: candidate `a2` (for j2/g$a) broke the SAME
1464 // file's `extends a2.a` ("找不到符号 类 a" + a 9450-error Object
1465 // cascade as every dependent of the corrupt head failed too).
1466 let mut pkg_segments: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1467 for n in &pool.order {
1468 if let Some((seg, _)) = n.split_once('/') {
1469 pkg_segments.insert(seg.to_string());
1470 }
1471 }
1472 // LOCAL-OK gate (round-59 Design B): a field-obscuring rename may
1473 // only fire when every class referencing the candidate lives inside
1474 // the root file's family, so the rename perturbs exactly ONE rendered
1475 // file. Ungated (Design A) the rule netted reqable −1653 / lark −5221
1476 // but weibo +36916: renames there flipped ambiguous type-vs-package
1477 // resolutions across thousands of pre-existing conflict families and
1478 // javac's error recovery turned "missing supertype, body suppressed"
1479 // files into full Object cascades. Lazy (progressive-browse) pools
1480 // skip the gate AND the rule — the multi-second image scan must not
1481 // stall single-class queries; the full-decompile pipeline always
1482 // materializes everything before installing renames.
1483 // Round-59 Design B stands: ungated field-clash renames were
1484 // retried AFTER the package-obscuring rules landed (weibo gate,
1485 // DDC_FC_UNGATED A/B) and still exploded to the 500k error cap —
1486 // fifth falsification of the ungated rename route.
1487 let local_ok: jdc_core::FxHashSet<String> = if pool_majority_materialized(pool) {
1488 refscan::local_ok(&pool.dexes, &field_clash_cands(pool, map))
1489 } else {
1490 jdc_core::FxHashSet::default()
1491 };
1492 let mut taken_sibling_displays: jdc_core::FxHashSet<String> =
1493 map.values().cloned().collect();
1494 // Stage-2 queue: nested tails equal to a top-level package segment
1495 // whose family showed NO descriptor-level trigger — the victim ref
1496 // may live in method bodies only (weixin e32/a2's `a.b.c()` bound
1497 // to its own nested `class a`, 位置: 类 a ×169).
1498 let mut body_cands: Vec<(String, String, String)> = Vec::new();
1499 for name in names {
1500 if !name.contains('$') || map.contains_key(name) {
1501 continue;
1502 }
1503 let Some(parent) = find_outer_name(pool, name) else {
1504 continue;
1505 };
1506 // Only a REAL `parent$rest` name renders as an inline member;
1507 // anonymous/local/lambda tails keep their flat own-file names.
1508 let Some(rest) = name
1509 .strip_prefix(parent.as_str())
1510 .and_then(|t| t.strip_prefix('$'))
1511 else {
1512 continue;
1513 };
1514 if !clean_member_tail(rest) {
1515 continue;
1516 }
1517 // Display simple names of every enclosing level, bottom-up; `cur`
1518 // ends at the ROOT (the top-level class whose file renders this
1519 // nested type — the scope the renamed tail lives in).
1520 let mut chain: Vec<String> = Vec::new();
1521 let mut cur = parent.clone();
1522 loop {
1523 let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
1524 let simple = disp.rsplit('/').next().unwrap_or(&disp);
1525 chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
1526 match find_outer_name(pool, &cur) {
1527 Some(o) if o != cur => cur = o,
1528 _ => break,
1529 }
1530 }
1531 let root: &str = &cur;
1532 let disp_parent = display_of(&parent, map);
1533 let tail = rest.rsplit('$').next().unwrap_or(rest);
1534 let orphan = rest.contains('$');
1535 // Field obscuring (JLS 6.4.2): a nested type whose simple name
1536 // equals an enclosing-class field is OBSCURED by it in a
1537 // `<Parent>.<tail>` qualified reference — `io/flutter/view/g$i`
1538 // (a `static enum i`) renders `g.i.t`, but g also declares an
1539 // instance field `i`, so javac resolves `g.i` to the field:
1540 // "无法从静态上下文中引用非静态 变量 i" (975 of 1140 reqable
1541 // static-context errors). The fix renames the NESTED TYPE (the
1542 // registry carries every render path: shorten, inner_simple —
1543 // whose bypass was the round-55 blowup, fixed in jdc-core —
1544 // classdec declarations), gated to LOCAL-OK candidates by the
1545 // refscan above. PoC: in-file rename of flutter g's `enum i`
1546 // took the class from 2452 errors to 1.
1547 let field_clash = local_ok.contains(name.as_str());
1548 // Package shadow (JLS 6.4.2 at nested level): a nested type
1549 // DISPLAYED with a simple name equal to the first segment of a
1550 // package its FAMILY references binds every `s.x` qualified ref
1551 // in the root file to the member type — the family's own outer
1552 // rename can EXPOSE this (e/b → e/b2 un-collided the nested `b`,
1553 // which then hijacked `b.b2` — 循环继承). The round-47 candidate
1554 // guard only kept RENAMED tails off package segments; pre-
1555 // existing shadows need the rename too, family-gated: fire only
1556 // when this family's own descriptors reference the segment.
1557 // Orphans INCLUDED: an `Outer$$x` orphan normalizes to the
1558 // nested display `x` (k=1 invisible rename), and a clean tail
1559 // renders BARE in the family file — same package shadow as a
1560 // true nested (weixin li/u0$$r → `class r` hijacked `r.a` for
1561 // root package r).
1562 let pkg_shadow = fam_segs
1563 .get(root)
1564 .is_some_and(|segs| segs.contains(tail));
1565 let clash = chain.iter().any(|c| c == tail) || field_clash || pkg_shadow;
1566 // Stage-2 queue: NO descriptor trigger, but the tail equals a
1567 // top-level package segment — the victim ref may be body-level
1568 // only. ORPHANS included: their pass-1 k=1 mint is the invisible
1569 // normalization (`a$$a` → display `a`), which leaves the package
1570 // shadow fully alive (weixin e32/a$$a hijacking `a.b.c()` ×169).
1571 if !clash && pkg_segments.contains(tail) {
1572 body_cands.push((name.clone(), root.to_string(), tail.to_string()));
1573 }
1574 if !orphan && !clash {
1575 continue;
1576 }
1577 // The enclosing class's field names (sanitized like the renderer)
1578 // — only needed once a rename actually fires, to keep the new
1579 // tail from being obscured again. Computed lazily so progressive
1580 // pools don't materialize parents for rules that don't rename.
1581 let mut enc_fields: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1582 if field_clash || pkg_shadow {
1583 if let Some(pc) = pool.get(&parent) {
1584 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
1585 enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
1586 }
1587 }
1588 }
1589 let mut k = 0u32;
1590 loop {
1591 k += 1;
1592 let cand_tail = if k == 1 {
1593 tail.to_string()
1594 } else {
1595 format!("{tail}{k}")
1596 };
1597 // The candidate must clear every ancestor display name (javac
1598 // checks the whole chain) AND every enclosing field name (else
1599 // the renamed type is just obscured again). Sibling nested
1600 // tails are covered by the pool.has_name check below.
1601 if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
1602 continue;
1603 }
1604 // Package shadow: the renamed tail becomes a member type of
1605 // the root file's class, outranking any package of the same
1606 // name for qualified refs in that file. Same-package top-level
1607 // classes likewise outrank-then-break in-file simple refs.
1608 // BOTH checks apply to field-clash renames only: for the
1609 // orphan rule the k=1 candidate is the natural display name
1610 // (`g0$$r` → `g0$r` renders identically — an invisible rename),
1611 // and rejecting it forced weixin's whole `g0$$X` Runnable
1612 // family to visible `X17` renames, breaking every lucky
1613 // `g0.r`-style resolution corpus-wide (+2911 `变量 r` — the
1614 // orphan rule is ungated by design, predating LOCAL-OK).
1615 if field_clash || pkg_shadow {
1616 if pkg_segments.contains(&cand_tail) {
1617 continue;
1618 }
1619 if pkg_shadow
1620 && fam_segs
1621 .get(root)
1622 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1623 {
1624 continue;
1625 }
1626 if let Some((root_pkg, _)) = root.rsplit_once('/') {
1627 let sib = format!("{root_pkg}/{cand_tail}");
1628 if pool.has_name(&sib) || taken_sibling_displays.contains(&sib)
1629 {
1630 // A top-level sibling exists — or an EARLIER rule
1631 // minted that display (obscuring_class_renames
1632 // moved ssosdk/b → ssosdk/b2 while this loop was
1633 // about to mint nested $b → b2: two `b2`s in one
1634 // scope, member type outranks the package sibling
1635 // and hijacked every bare `b2.b(..)` call).
1636 continue;
1637 }
1638 taken_sibling_displays.insert(sib);
1639 }
1640 }
1641 let cand = format!("{disp_parent}${cand_tail}");
1642 if pool.has_name(&cand) || assigned.contains(&cand) {
1643 continue;
1644 }
1645 assigned.insert(cand.clone());
1646 map.insert(name.clone(), cand);
1647 break;
1648 }
1649 }
1650 // Stage 2 (body-gated package shadow): one body-level image scan
1651 // over the queued families (family_ref_segments — the field-
1652 // deshadow mechanism; instruction-level, cross-package refs), then
1653 // mint for the families whose members actually reference the
1654 // segment. Mirrors obscuring_class_renames' two-stage design at
1655 // family level. Lazy pools skip (same gate as stage 1's scan).
1656 let mut body_renamed = 0usize;
1657 if !body_cands.is_empty() && pool_majority_materialized(pool) {
1658 let roots: jdc_core::FxHashSet<&str> =
1659 body_cands.iter().map(|(_, r, _)| r.as_str()).collect();
1660 let mut members: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1661 for n in &pool.order {
1662 let r = match n.find('$') {
1663 Some(i) => &n[..i],
1664 None => n.as_str(),
1665 };
1666 if roots.contains(r) {
1667 members.insert(n.clone());
1668 }
1669 }
1670 let t0 = std::time::Instant::now();
1671 let (by_referrer, _) = refscan::family_ref_segments(&pool.dexes, &members);
1672 // Referrer-keyed → family-root-keyed: every member renders in
1673 // the root file's scope.
1674 let mut root_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
1675 jdc_core::FxHashMap::default();
1676 for (referrer, segs) in &by_referrer {
1677 let r = referrer.split('$').next().unwrap_or(referrer);
1678 root_segs.entry(r).or_default().extend(segs.iter().map(|s| s.as_str()));
1679 }
1680 if std::env::var("DDC_STATS").is_ok() {
1681 eprintln!(
1682 "[renames] nested body-shadow scan: cands={} families={} scan={:?}",
1683 body_cands.len(),
1684 root_segs.len(),
1685 t0.elapsed()
1686 );
1687 }
1688 for (name, root, tail) in &body_cands {
1689 if !root_segs
1690 .get(root.as_str())
1691 .is_some_and(|s| s.contains(tail.as_str()))
1692 {
1693 continue;
1694 }
1695 if let Some(d) = map.get(name) {
1696 // Pass-1 orphan mints are invisible (display tail ==
1697 // tail) — those get re-minted visibly here. Anything
1698 // already visibly renamed stays as the earlier rule set it.
1699 let cur_tail = d.rsplit(['/', '$']).next().unwrap_or("");
1700 if cur_tail != tail.as_str() {
1701 continue;
1702 }
1703 }
1704 // Fresh display context: stage-1 renames may have moved the
1705 // parent/chain since this candidate was queued.
1706 let Some(parent) = find_outer_name(pool, name) else {
1707 continue;
1708 };
1709 let Some(rest) = name
1710 .strip_prefix(parent.as_str())
1711 .and_then(|t| t.strip_prefix('$'))
1712 else {
1713 continue;
1714 };
1715 if !clean_member_tail(rest) {
1716 continue;
1717 }
1718 let mut chain: Vec<String> = Vec::new();
1719 let mut cur = parent.clone();
1720 loop {
1721 let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
1722 let simple = disp.rsplit('/').next().unwrap_or(&disp);
1723 chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
1724 match find_outer_name(pool, &cur) {
1725 Some(o) if o != cur => cur = o,
1726 _ => break,
1727 }
1728 }
1729 let disp_parent = display_of(&parent, map);
1730 let mut enc_fields: jdc_core::FxHashSet<String> =
1731 jdc_core::FxHashSet::default();
1732 if let Some(pc) = pool.get(&parent) {
1733 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
1734 enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
1735 }
1736 }
1737 let mut k = 0u32;
1738 loop {
1739 k += 1;
1740 let cand_tail = if k == 1 {
1741 tail.clone()
1742 } else {
1743 format!("{tail}{k}")
1744 };
1745 if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
1746 continue;
1747 }
1748 // The renamed tail stays a member type of the root
1749 // file — it must not shadow ANOTHER package segment the
1750 // family references (descriptor OR body level) nor a
1751 // same-package top-level sibling.
1752 if pkg_segments.contains(&cand_tail) {
1753 continue;
1754 }
1755 if fam_segs
1756 .get(root.as_str())
1757 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1758 {
1759 continue;
1760 }
1761 if root_segs
1762 .get(root.as_str())
1763 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1764 {
1765 continue;
1766 }
1767 if let Some((root_pkg, _)) = root.rsplit_once('/') {
1768 let sib = format!("{root_pkg}/{cand_tail}");
1769 if pool.has_name(&sib) || taken_sibling_displays.contains(&sib) {
1770 continue;
1771 }
1772 taken_sibling_displays.insert(sib);
1773 }
1774 let cand = format!("{disp_parent}${cand_tail}");
1775 if pool.has_name(&cand) || assigned.contains(&cand) {
1776 continue;
1777 }
1778 // Repair pass-1 mints whose display chain ran through
1779 // THIS name's old display: stage 2 renames ancestors
1780 // after descendants were minted (weibo mobileads k$a →
1781 // a5 left pass-1's k$a$b$a → "k$a$b$a2" pointing through
1782 // the dead `a` — 找不到符号 类 a ×48). Ancestor-first
1783 // within each pass, but stage 2 as a whole runs last.
1784 let old_disp = format!("{disp_parent}${tail}");
1785 if old_disp != cand {
1786 for (mk, mv) in map.iter_mut() {
1787 if mk == name {
1788 continue;
1789 }
1790 if let Some(rest) = mv.strip_prefix(old_disp.as_str()) {
1791 if rest.starts_with('$') {
1792 let repaired = format!("{cand}{rest}");
1793 assigned.insert(repaired.clone());
1794 *mv = repaired;
1795 }
1796 }
1797 }
1798 }
1799 assigned.insert(cand.clone());
1800 map.insert(name.clone(), cand);
1801 body_renamed += 1;
1802 break;
1803 }
1804 }
1805 }
1806 if body_renamed > 0 && std::env::var("DDC_STATS").is_ok() {
1807 eprintln!("[renames] nested body-shadow renames: {body_renamed}");
1808 }
1809}
1810
1811/// Field-obscuring rename candidates: nested member types whose simple
1812/// name equals a field of the enclosing class, mapped to the top-level
1813/// class whose file renders them (the LOCAL-OK family root). Mirrors the
1814/// skip conditions of `nested_collision_renames` (same-name entries must
1815/// agree, or the gate proves locality for a rename that never fires — or
1816/// worse, skips one that does).
1817fn field_clash_cands(pool: &DexPool, map: &HashMap<String, String>) -> HashMap<String, String> {
1818 let mut out: HashMap<String, String> = HashMap::default();
1819 for name in &pool.order {
1820 if !name.contains('$') || map.contains_key(name) {
1821 continue;
1822 }
1823 let Some(parent) = find_outer_name(pool, name) else {
1824 continue;
1825 };
1826 let Some(rest) = name
1827 .strip_prefix(parent.as_str())
1828 .and_then(|t| t.strip_prefix('$'))
1829 else {
1830 continue;
1831 };
1832 if !clean_member_tail(rest) {
1833 continue;
1834 }
1835 let tail = rest.rsplit('$').next().unwrap_or(rest);
1836 let Some(pc) = pool.get(&parent) else {
1837 continue;
1838 };
1839 let clash = pc
1840 .static_fields
1841 .iter()
1842 .chain(pc.instance_fields.iter())
1843 .any(|f| crate::classdec::java_ident(&f.name).as_ref() == tail);
1844 if !clash {
1845 continue;
1846 }
1847 let mut root = parent.clone();
1848 loop {
1849 match find_outer_name(pool, &root) {
1850 Some(o) if o != root => root = o,
1851 _ => break,
1852 }
1853 }
1854 out.insert(name.clone(), root);
1855 }
1856 out
1857}
1858
1859/// Whether the pool is (majority-)materialized: full-decompile pipelines
1860/// call `materialize_all` before installing renames, progressive browse
1861/// pools materialize a handful of classes on demand. The refscan gate
1862/// must not run (or stall startup) in the progressive case.
1863fn pool_majority_materialized(pool: &DexPool) -> bool {
1864 let (mut mat, mut lazy) = (0usize, 0usize);
1865 for e in pool.classes.values() {
1866 match e {
1867 ClassEntry::Eager(_) => mat += 1,
1868 ClassEntry::Lazy { pc, .. } => {
1869 if pc.get().is_some() {
1870 mat += 1;
1871 } else {
1872 lazy += 1;
1873 }
1874 }
1875 ClassEntry::NameOnly => lazy += 1,
1876 }
1877 }
1878 lazy == 0 || mat >= lazy
1879}
1880
1881/// Compute and install the registry (call before worker threads spawn).
1882/// CURRENT resident size (MB) — ru_maxrss is monotone (historical peak),
1883/// so purge effects are invisible there; live-vs-retained attribution
1884/// needs the instantaneous figure (macOS mach task_info).
1885#[cfg(target_vendor = "apple")]
1886fn current_rss_mb() -> f64 {
1887 #[repr(C)]
1888 struct TaskBasicInfo {
1889 virtual_size: u64,
1890 resident_size: u64,
1891 resident_size_max: u64,
1892 // time_value_t = {i32 secs, i32 usec} = 8B each.
1893 total_user_time: u64,
1894 total_sys_time: u64,
1895 policy: i32,
1896 suspend_count: i32,
1897 }
1898 extern "C" {
1899 fn mach_task_self() -> u32;
1900 fn task_info(
1901 target_port: u32,
1902 flavor: i32,
1903 task_info_out: *mut TaskBasicInfo,
1904 task_info_out_cnt: *mut u32,
1905 ) -> i32;
1906 }
1907 let mut info = TaskBasicInfo {
1908 virtual_size: 0,
1909 resident_size: 0,
1910 resident_size_max: 0,
1911 total_user_time: 0,
1912 total_sys_time: 0,
1913 policy: 0,
1914 suspend_count: 0,
1915 };
1916 // count is in natural_t (4B) units; a smaller-than-kernel version
1917 // still fills the prefix fields we read.
1918 let mut cnt = (std::mem::size_of::<TaskBasicInfo>() / 4) as u32;
1919 let kr = unsafe { task_info(mach_task_self(), 20, &mut info, &mut cnt) };
1920 if kr == 0 {
1921 info.resident_size as f64 / (1024.0 * 1024.0)
1922 } else {
1923 0.0
1924 }
1925}
1926#[cfg(not(target_vendor = "apple"))]
1927fn current_rss_mb() -> f64 {
1928 0.0
1929}
1930
1931/// ru_maxrss peak so far (MB). macOS reports bytes, Linux KB; Windows
1932/// has no getrusage (the old not(apple) fallback referenced it and
1933/// failed the MSVC link — LNK2019 — in the v0.1.18 CI).
1934#[cfg(unix)]
1935fn peak_rss_mb() -> f64 {
1936 #[repr(C)]
1937 struct Rusage {
1938 pad: [i64; 4],
1939 maxrss: i64,
1940 // Oversized on purpose: the kernel writes sizeof(struct rusage)
1941 // (~144B on macOS) regardless of what we read back.
1942 tail: [i64; 16],
1943 }
1944 extern "C" {
1945 fn getrusage(who: i32, r: *mut Rusage) -> i32;
1946 }
1947 let mut r = Rusage {
1948 pad: [0; 4],
1949 maxrss: 0,
1950 tail: [0; 16],
1951 };
1952 unsafe { getrusage(0, &mut r) };
1953 #[cfg(target_vendor = "apple")]
1954 let scale = 1024.0 * 1024.0;
1955 #[cfg(not(target_vendor = "apple"))]
1956 let scale = 1024.0;
1957 r.maxrss as f64 / scale
1958}
1959#[cfg(not(unix))]
1960fn peak_rss_mb() -> f64 {
1961 0.0
1962}
1963
1964/// DDC_STATS phase attribution of the RSS high-water mark: peak AND
1965/// instantaneous live (ru_maxrss is monotone, so purge effects are
1966/// invisible there).
1967pub(crate) fn stats_rss(phase: &str) {
1968 if std::env::var("DDC_STATS").is_ok() {
1969 eprintln!(
1970 "[rss] {phase}: peak {:.0} MB, live {:.0} MB",
1971 peak_rss_mb(),
1972 current_rss_mb()
1973 );
1974 }
1975}
1976
1977pub fn install_case_renames(pool: &DexPool) {
1978 let dbg = std::env::var("DDC_DBG_REN").is_ok();
1979 let mut tick = std::time::Instant::now();
1980 let mut lap = |tag: &str| {
1981 if dbg {
1982 eprintln!("[ren] {:?} {}", tick.elapsed(), tag);
1983 tick = std::time::Instant::now();
1984 }
1985 };
1986 let mut map = case_rename_map(pool);
1987 lap("case_rename_map");
1988 crate::stats_rss(" case_rename_map");
1989 // Cross-package reference first segments (descriptor level). Lazy
1990 // (progressive-browse) pools skip the scan — materializing every
1991 // class must not stall single-class queries; the obscuring rules
1992 // then simply never fire (browse mode has no whole-program javac).
1993 let (pkg_segs, fam_segs) = if pool_majority_materialized(pool) {
1994 ref_segments(pool)
1995 } else {
1996 (
1997 jdc_core::FxHashMap::default(),
1998 jdc_core::FxHashMap::default(),
1999 )
2000 };
2001 // Package-leaf shadows FIRST: the nested-collision renames compute
2002 // display chains off the (renamed) parent display names — running
2003 // them before the parent rename left both rules minting the same
2004 // display (`a2` twice in weibo's AIDL families).
2005 crate::stats_rss(" ref_segments");
2006 lap("ref_segments");
2007 pkg_leaf_shadow_renames(pool, &mut map);
2008 lap("pkg_leaf_shadow");
2009 class_pkg_collision_renames(pool, &mut map);
2010 lap("class_pkg_collision");
2011 obscuring_class_renames(pool, &mut map, &pkg_segs);
2012 lap("obscuring");
2013 nested_collision_renames(pool, &mut map, &fam_segs);
2014 lap("nested_collision");
2015 // LAST: lossy-sanitize collisions key on the display form every rule
2016 // above has already settled (`l.᩻ܶ` → `l/__.java`, 22,636 classes onto
2017 // 3 paths on bin.mt.plus — the silent-overwrite data loss).
2018 crate::stats_rss(" shadow+collision+obscuring+nested rules");
2019 lap("rules");
2020 lossy_sanitize_renames(pool, &mut map);
2021 // Default-package relocation keys on the settled displays too (it
2022 // PREFIXES them with the synthetic package) — runs after every
2023 // simple-name rule has minted.
2024 root_pkg_relocation(pool, &mut map);
2025 fw_shadow_relocation(pool, &mut map);
2026 crate::stats_rss(" lossy+root-pkg rules");
2027 lap("lossy+root");
2028 jdc_core::rename::set_class_renames(map);
2029 crate::stats_rss("class-renames installed");
2030 lap("set_class_renames");
2031 // Field-rename registry build and the access-widening image scan are
2032 // independent: both only READ the pool, they install into disjoint
2033 // statics (FIELD_RENAMES vs WIDEN), the WIDEN accessors are
2034 // is_some_and-guarded with no reader before render time, and lazy
2035 // materialization is OnceLock-safe. Run them concurrently — the 1.6s
2036 // QQ widening scan fully overlaps the field-rename build.
2037 if pool_majority_materialized(pool) {
2038 std::thread::scope(|s| {
2039 let h = s.spawn(|| crate::classdec::install_access_widening(pool));
2040 jdc_core::rename::set_field_renames(combined_field_renames(pool));
2041 let _ = h.join();
2042 });
2043 crate::stats_rss("field-renames + widening installed");
2044 lap("field+widen");
2045 } else {
2046 jdc_core::rename::set_field_renames(combined_field_renames(pool));
2047 lap("field_renames(lazy)");
2048 }
2049}
2050
2051/// A top-level class `P/s` whose simple name equals the FIRST PACKAGE
2052/// SEGMENT of types that other classes in P reference cross-package.
2053/// Such a reference renders as the FQN `s.rest...`, and javac binds the
2054/// first segment to the nearest type in scope — the same-package class
2055/// `P/s` (JLS 6.4.2): `n91.f.a(x)` inside pc5/bd looks up member `f` OF
2056/// CLASS pc5.n91 (weixin's n0/n91/uc6/vc6 protobuf cluster: ~12k root
2057/// files, ~90k cascade lines; weibo's `a.g.b.a.b` chains root the same
2058/// way — class a beside subpackage a/ inside package a/g/b). No Java
2059/// syntax escapes an obscured first segment (imports can bypass it, but
2060/// single-type imports are blocked exactly where obfuscated leaf names
2061/// collide with same-package siblings), so the class moves: the r66
2062/// rename mechanism at a new trigger. Gated on DESCRIPTOR-level refs of
2063/// the candidate's own package (supers/fields/protos) — body-only refs
2064/// are missed by design; the import layer covers unblocked leaves there.
2065/// Descriptor-level cross-package reference first segments, aggregated
2066/// per PACKAGE (top-level obscurers: every file in P shares the scope)
2067/// and per FAMILY root (nested obscurers: a nested simple name is in
2068/// scope only inside its own root file). Raw `L..;` scans — no JavaType
2069/// parsing. Body-level refs are missed by design (the import layer
2070/// catches unblocked leaves there); descriptors cover supers, field
2071/// types and method protos — the header positions whose breakage starts
2072/// the error-type cascades.
2073/// Package (or family) → first segments of the cross-package types its
2074/// classes reference at descriptor level. Obscuring-rename trigger sets.
2075type SegMap<'a> = jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>;
2076
2077fn ref_segments(pool: &DexPool) -> (SegMap<'_>, SegMap<'_>) {
2078 fn pkg_of(n: &str) -> &str {
2079 match n.rsplit_once('/') {
2080 Some((p, _)) => p,
2081 None => "",
2082 }
2083 }
2084 fn note<'a>(
2085 pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
2086 fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
2087 pkg: &'a str,
2088 fam: &'a str,
2089 t: &'a str,
2090 ) {
2091 if !t.contains('/') {
2092 return; // root-package target: unreferenceable from named pkgs
2093 }
2094 let _ = pkg; // same-package refs COUNT: ddc renders headers/field
2095 // types fully qualified (print_class_name), so `a.g.b.a.b`
2096 // inside package a/g/b is obscured by class a/g/b/a just
2097 // like a cross-package FQN (weibo's whole `a`-tree family).
2098 let seg = t.split('/').next().unwrap_or("");
2099 if !seg.is_empty() {
2100 pkg_segs.entry(pkg).or_default().insert(seg);
2101 fam_segs.entry(fam).or_default().insert(seg);
2102 }
2103 }
2104 fn scan<'a>(
2105 pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
2106 fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
2107 pkg: &'a str,
2108 fam: &'a str,
2109 desc: &'a str,
2110 ) {
2111 let b = desc.as_bytes();
2112 let mut i = 0usize;
2113 while i < b.len() {
2114 if b[i] == b'L' {
2115 if let Some(end) = desc[i + 1..].find(';') {
2116 note(pkg_segs, fam_segs, pkg, fam, &desc[i + 1..i + 1 + end]);
2117 i += end + 2;
2118 continue;
2119 }
2120 }
2121 i += 1;
2122 }
2123 }
2124 let mut pkg_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
2125 jdc_core::FxHashMap::default();
2126 let mut fam_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
2127 jdc_core::FxHashMap::default();
2128 for name in &pool.order {
2129 let Some(c) = pool.get(name) else { continue };
2130 let pkg = pkg_of(name);
2131 let fam = match name.find('$') {
2132 Some(i) => &name[..i],
2133 None => name.as_str(),
2134 };
2135 if let Some(sup) = &c.super_name {
2136 note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
2137 }
2138 for sup in c.interfaces.iter() {
2139 note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
2140 }
2141 for f in c.static_fields.iter().chain(c.instance_fields.iter()) {
2142 scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &f.desc);
2143 }
2144 for m in c.direct_methods.iter().chain(c.virtual_methods.iter()) {
2145 scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &m.desc);
2146 }
2147 }
2148 (pkg_segs, fam_segs)
2149}
2150
2151/// A top-level class `P/s` whose simple name equals the FIRST PACKAGE
2152/// SEGMENT of types that classes in P reference cross-package. Such a
2153/// reference renders as the FQN `s.rest...`, and javac binds the first
2154/// segment to the nearest type in scope — the same-package class `P/s`
2155/// (JLS 6.4.2, no package fallback — verified): `n91.f.a(x)` inside
2156/// pc5/bd looks up member `f` OF CLASS pc5.n91 (weixin's n0/n91/uc6/vc6
2157/// protobuf cluster: ~12k root files, ~90k cascade lines; weibo's
2158/// `a.g.b.a.b` chains root the same way). No Java syntax escapes an
2159/// obscured first segment, so the class moves: the r66 rename mechanism
2160/// at a new trigger, gated on DESCRIPTOR-level refs of the candidate's
2161/// package (body-only refs go to the import layer). Nested obscurers
2162/// (a nested simple name shadowing a package inside its family file)
2163/// are the companion trigger in nested_collision_renames.
2164fn obscuring_class_renames(
2165 pool: &DexPool,
2166 map: &mut HashMap<String, String>,
2167 pkg_segs: &SegMap,
2168) {
2169 fn pkg_of(n: &str) -> &str {
2170 match n.rsplit_once('/') {
2171 Some((p, _)) => p,
2172 None => "",
2173 }
2174 }
2175 // Root package first segments (segments some class lives under).
2176 let mut root_segs: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
2177 for n in &pool.order {
2178 if let Some(i) = n.find('/') {
2179 root_segs.insert(&n[..i]);
2180 }
2181 }
2182 let mut cands: Vec<&String> = Vec::new();
2183 for name in &pool.order {
2184 let simple = name.rsplit('/').next().unwrap_or(name);
2185 let pkg = pkg_of(name);
2186 if simple.is_empty() || simple.contains('$') || pkg.is_empty() {
2187 continue;
2188 }
2189 if !root_segs.contains(simple) {
2190 continue;
2191 }
2192 if map.get(name).is_some_and(|v| v != name) {
2193 continue; // an earlier rule moved it
2194 }
2195 // NB: NO "class lives under the s tree" exclusion. weibo's
2196 // a/a/b/c/l/a shadowed the FQN refs `a.a.a.a2` that its own
2197 // package files render for cross-subpackage targets (the
2198 // starts_with("a/") skip left the WHOLE a-tree unrenamed —
2199 // 找不到符号 类 a chains). The two-stage refsegs gates below
2200 // require an actual cross-package FQN ref for the trigger.
2201 cands.push(name);
2202 }
2203 let ncands = cands.len();
2204 if std::env::var("DDC_STATS").is_ok() {
2205 eprintln!(
2206 "[renames] obscuring candidates={ncands} pkgs-with-segs={}",
2207 pkg_segs.len()
2208 );
2209 }
2210 if cands.is_empty() {
2211 return;
2212 }
2213 // Fast collision oracle: sorted names + prefix range scan (the r66
2214 // candidate loop's `order.iter().any(starts_with)` is O(n) per
2215 // candidate — this rule can mint tens of thousands of renames).
2216 let mut sorted: Vec<&String> = pool.order.iter().collect();
2217 sorted.sort_unstable();
2218 let prefix_free = |cand: &str| -> bool {
2219 // No class named cand, nothing nested under cand$..., no package cand/...
2220 let idx = sorted.partition_point(|n| n.as_str() < cand);
2221 !sorted[idx..].first().is_some_and(|n| n.starts_with(cand))
2222 };
2223 let pkg_simples = pool.package_simples();
2224 let mut taken_displays: jdc_core::FxHashSet<String> = map.values().cloned().collect();
2225 let mut renamed = 0usize;
2226 // Stage 1: descriptor-level hits rename now; the rest queue for the
2227 // BODY-level image scan (weixin pc5's `invoke-static Ln91/f;.a` —
2228 // 7.8k+6.6k error lines the descriptor gate cannot see).
2229 let mut stage2: Vec<&String> = Vec::new();
2230 for name in cands {
2231 let pkg = pkg_of(name);
2232 let simple = name.rsplit('/').next().unwrap_or(name);
2233 if !pkg_segs.get(pkg).is_some_and(|segs| segs.contains(simple)) {
2234 stage2.push(name);
2235 continue;
2236 }
2237 let siblings = pkg_simples.get(pkg);
2238 let mut k = 1u32;
2239 loop {
2240 k += 1;
2241 let new_simple = format!("{simple}{k}");
2242 let cand = if pkg.is_empty() {
2243 new_simple.clone()
2244 } else {
2245 format!("{pkg}/{new_simple}")
2246 };
2247 // Exact/prefix pool clash, another rule's display target, the
2248 // package's own referenced segments (the new name must not
2249 // obscure ANOTHER package the same files reference), or a
2250 // case-insensitive clash with a same-package sibling
2251 // (case-insensitive filesystems fold the files).
2252 let clash = !prefix_free(&cand)
2253 || taken_displays.contains(&cand)
2254 || pkg_segs
2255 .get(pkg)
2256 .is_some_and(|segs| segs.contains(new_simple.as_str()))
2257 || siblings.is_some_and(|s| {
2258 s.iter()
2259 .any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
2260 });
2261 if !clash {
2262 taken_displays.insert(cand.clone());
2263 map.insert(name.clone(), cand);
2264 renamed += 1;
2265 break;
2266 }
2267 }
2268 }
2269 let mut body_renamed = 0usize;
2270 if !stage2.is_empty() && pool_majority_materialized(pool) {
2271 let stage2_pkgs: jdc_core::FxHashSet<String> =
2272 stage2.iter().map(|n| pkg_of(n).to_string()).collect();
2273 let body_segs = crate::refscan::body_ref_segments(&pool.dexes, &stage2_pkgs);
2274 for name in stage2 {
2275 let pkg = pkg_of(name);
2276 let simple = name.rsplit('/').next().unwrap_or(name);
2277 if !body_segs
2278 .get(pkg)
2279 .is_some_and(|segs| segs.contains(simple))
2280 {
2281 continue;
2282 }
2283 let siblings = pkg_simples.get(pkg);
2284 let mut k = 1u32;
2285 loop {
2286 k += 1;
2287 let new_simple = format!("{simple}{k}");
2288 let cand = format!("{pkg}/{new_simple}");
2289 let clash = !prefix_free(&cand)
2290 || taken_displays.contains(&cand)
2291 || pkg_segs
2292 .get(pkg)
2293 .is_some_and(|segs| segs.contains(new_simple.as_str()))
2294 || body_segs
2295 .get(pkg)
2296 .is_some_and(|segs| segs.contains(new_simple.as_str()))
2297 || siblings.is_some_and(|s| {
2298 s.iter()
2299 .any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
2300 });
2301 if !clash {
2302 taken_displays.insert(cand.clone());
2303 map.insert(name.clone(), cand);
2304 renamed += 1;
2305 body_renamed += 1;
2306 break;
2307 }
2308 }
2309 }
2310 }
2311 if std::env::var("DDC_STATS").is_ok() {
2312 eprintln!(
2313 "[renames] obscuring-class renames: {renamed} (body-gated {body_renamed}; pkgs-with-segs={}, cands={ncands})",
2314 pkg_segs.len()
2315 );
2316 }
2317}
2318
2319/// Class-level counterpart of the non-ASCII rule already applied to
2320/// MEMBERS by `member_collision_renames`, kept as the SAFETY NET behind
2321/// the injective `sanitize_fq`.
2322///
2323/// `sanitize_fq` escapes non-identifier characters as `_u<hex>`, so two
2324/// distinct classes normally reach two distinct file names. It cannot be
2325/// injective against *lookalikes*, because ident-safe ASCII must pass
2326/// through unchanged: a literal class named `_u1a7b` collides with the
2327/// class whose single character is U+1A7B, and the pre-existing keyword
2328/// repair makes `_do` collide with `do`. Without this pass those pairs
2329/// repeat the original disaster — the writer opens with `create_new`,
2330/// takes EEXIST, unlinks and rewrites, so one class is destroyed with
2331/// exit code 0 (on bin.mt.plus the lossy predecessor of this rule hid
2332/// 22,633 of 30,768 classes exactly this way).
2333///
2334/// Groups are keyed by the SANITIZED path — the exact string both the
2335/// declaration site (`sanitize_ref`) and the writer (`source_path`)
2336/// produce. The first (sorted) member of each group keeps its name; the
2337/// rest become `<sanitized>_2`, `<sanitized>_3`, … Pure ASCII, so a
2338/// minted name is a fixed point of the sanitizer: it cannot silently
2339/// join a second collision group, and the declaration, every reference
2340/// and the file name agree by construction through `apply_class_rename`.
2341///
2342/// Costs nothing on a healthy pool: one pass over the emission set, and
2343/// it returns before touching the registry when every key is unique.
2344/// Runs LAST in `install_case_renames`: it keys on the display form the
2345/// earlier rules have already settled.
2346fn lossy_sanitize_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
2347 let names = top_level_classes(pool);
2348 // Current display of an emission-set name. `case_rename_map` has
2349 // already inserted an entry for every one of these (identity or
2350 // renamed) and an exact registry hit short-circuits
2351 // `apply_class_rename`, so `map` already holds the final display —
2352 // no `$`-prefix walk is needed here.
2353 fn key_of(map: &HashMap<String, String>, n: &str) -> String {
2354 let disp = map.get(n).map(|s| s.as_str()).unwrap_or(n);
2355 sanitize_internal(disp)
2356 }
2357 let mut counts: HashMap<String, u32> = HashMap::default();
2358 for n in &names {
2359 *counts.entry(key_of(map, n)).or_insert(0) += 1;
2360 }
2361 if counts.values().all(|c| *c == 1) {
2362 return;
2363 }
2364 // Every sanitized path in the POOL is off-limits for a minted name,
2365 // not just the emission set: an inlined nested class still owns the
2366 // display it renders under inside its outer's file.
2367 let mut taken: jdc_core::FxHashSet<String> = pool
2368 .order
2369 .iter()
2370 .map(|n| sanitize_internal(map.get(n).map(|s| s.as_str()).unwrap_or(n)))
2371 .collect();
2372 let mut sorted: Vec<&String> = names.iter().collect();
2373 sorted.sort();
2374 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2375 let mut renamed = 0usize;
2376 for n in sorted {
2377 let key = key_of(map, n);
2378 if counts.get(&key).copied().unwrap_or(0) < 2 {
2379 continue;
2380 }
2381 if seen.insert(key.clone()) {
2382 continue; // first (sorted) member of the group keeps its name
2383 }
2384 let (pkg, simple) = match key.rsplit_once('/') {
2385 Some((p, s)) => (p, s),
2386 None => ("", key.as_str()),
2387 };
2388 let mut i = 1u32;
2389 loop {
2390 i += 1;
2391 let cand = if pkg.is_empty() {
2392 format!("{simple}_{i}")
2393 } else {
2394 format!("{pkg}/{simple}_{i}")
2395 };
2396 if !taken.insert(cand.clone()) {
2397 continue; // already owned by a real class or another mint
2398 }
2399 // The minted display IS the sanitized name, so
2400 // sanitize_internal(cand) == cand and the writer's file name
2401 // matches the declaration it writes into it.
2402 map.insert(n.clone(), cand);
2403 renamed += 1;
2404 break;
2405 }
2406 }
2407 if std::env::var("DDC_STATS").is_ok() {
2408 eprintln!("[renames] lossy-sanitize collisions renamed={renamed}");
2409 }
2410}
2411
2412/// A class named `P/s` while OTHER classes live under `P/s/` — the dex
2413/// namespace is flat, so obfuscators freely mint a class whose simple
2414/// name equals a subpackage of its own package (`ptr.a` +
2415/// `ptr.a.a`, weibo rsplay/ptr families). That namespace is
2416/// unrepresentable in Java: the declaration itself is
2417/// "类 a与带有相同名称的程序包冲突" and every qualified reference
2418/// through the segment becomes unresolvable (the 53.8k weibo
2419/// missing-class errors root here, with Object/getClass cascades on
2420/// top). Rename the class — the registry carries declarations and
2421/// references — with a candidate that cannot recreate the shape (not a
2422/// class, not a package, not a nested-class prefix, not another rule's
2423/// target).
2424fn class_pkg_collision_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
2425 // Every package that exists in the pool — ALL ancestor prefixes of
2426 // every class path, not just direct parents: Java sees package
2427 // `z` as existing when any class lives at `z/a/b` (the weibo
2428 // `com.sina.weibo.z` interface collides with a subpackage-only
2429 // `z/` that has no directly-resident class).
2430 // Borrowed slices: the owned set allocated a fresh String per
2431 // ancestor prefix of every class (~600k allocations on weibo).
2432 let mut pkgs: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
2433 for n in &pool.order {
2434 let mut rest = n.as_str();
2435 while let Some(i) = rest.rfind('/') {
2436 rest = &rest[..i];
2437 pkgs.insert(rest);
2438 }
2439 }
2440 // Exact-class and minted-display sets make the clash checks O(1):
2441 // the old `order.iter().any(starts_with(&cand))` scanned every name
2442 // per candidate per k-step (weibo: the second quadratic of the
2443 // single-class getclass path, ~1s) — and its prefix match also
2444 // counted SIBLING classes (`a/b2` matching `a/b23`) that map to
2445 // different files and never collide.
2446 let taken: jdc_core::FxHashSet<&str> = pool.order.iter().map(|s| s.as_str()).collect();
2447 let mut used: jdc_core::FxHashSet<String> = map.values().cloned().collect();
2448 for name in &pool.order {
2449 let Some((pkg, simple)) = name.rsplit_once('/') else {
2450 continue;
2451 };
2452 if simple.is_empty() || pkg.is_empty() || simple.contains('$') {
2453 // Nested classes render dotted (`Outer.a`) — no package
2454 // effect from their `$` tail.
2455 continue;
2456 }
2457 if !pkgs.contains(name.as_str()) || map.get(name).is_some_and(|v| v != name) {
2458 continue; // no subpackage under it, or an earlier rule moved it
2459 }
2460 let mut k = 1u32;
2461 loop {
2462 k += 1;
2463 let cand = format!("{pkg}/{simple}{k}");
2464 let clash = pkgs.contains(cand.as_str())
2465 || taken.contains(cand.as_str())
2466 || used.contains(&cand);
2467 if !clash {
2468 used.insert(cand.clone());
2469 map.insert(name.clone(), cand);
2470 break;
2471 }
2472 }
2473 }
2474}
2475
2476/// Obfuscators can name a class after its own package leaf (`package k;`
2477/// `class k extends EditText`). Every SAME-PACKAGE reference to `k.Anything`
2478/// then resolves the first segment to the CLASS, not the package —
2479/// javac looks for `Anything` as a member of class k ("找不到符号 class p2,
2480/// location: class k"). Renaming the class (display-level, the registry
2481/// carries declarations and references) makes the package the only
2482/// thing the leaf name resolves to.
2483fn pkg_leaf_shadow_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
2484 let taken: jdc_core::FxHashSet<&str> = pool.order.iter().map(|s| s.as_str()).collect();
2485 let mut used: jdc_core::FxHashSet<String> = map.values().cloned().collect();
2486 // Classes per package in ONE pass — the per-candidate
2487 // `order.iter().filter(starts_with).count()` was a quadratic scan
2488 // on obfuscated corpora (weibo: 2124 leaf-shadow candidates × 98k
2489 // names ≈ 2×10⁸ memcmp — the dominant single-class getclass cost,
2490 // and paid again by every full run).
2491 let mut under_pkg: jdc_core::FxHashMap<&str, usize> = jdc_core::FxHashMap::default();
2492 for n in &pool.order {
2493 if let Some(i) = n.rfind('/') {
2494 *under_pkg.entry(&n[..i]).or_default() += 1;
2495 }
2496 }
2497 for name in &pool.order {
2498 let Some((pkg, simple)) = name.rsplit_once('/') else {
2499 continue;
2500 };
2501 if simple.is_empty() || pkg.is_empty() {
2502 continue;
2503 }
2504 let leaf = pkg.rsplit('/').next().unwrap_or(pkg);
2505 // class name == package leaf, and the package really exists
2506 // (more classes than just this one live under it).
2507 if simple != leaf {
2508 continue;
2509 }
2510 let siblings = under_pkg.get(pkg).copied().unwrap_or(0).saturating_sub(1);
2511 if siblings == 0 || map.get(name).is_some_and(|v| v != name) {
2512 continue; // already renamed by an earlier rule; identity anchors are fine to overwrite
2513 }
2514 let mut k = 1u32;
2515 loop {
2516 k += 1;
2517 let cand = format!("{pkg}/{simple}{k}");
2518 if !taken.contains(cand.as_str()) && !used.contains(&cand) {
2519 used.insert(cand.clone());
2520 map.insert(name.clone(), cand);
2521 break;
2522 }
2523 }
2524 }
2525}
2526
2527/// Member-level collision renames (fields AND methods of one class).
2528/// Two sources collapse onto one display name: obfuscators renaming a
2529/// synthetic outer reference (`this$0`) onto a real field's name, and
2530/// the identifier sanitizer mapping non-ASCII to `_` (whole
2531/// `ERROR_中文` families render one `ERROR________`).
2532/// Both are legal in bytecode (members resolve by index) and both are
2533/// javac "already defined" errors in source. Policy per colliding group
2534/// (fields keep the first non-synthetic name; a synthetic outer
2535/// reference restores `this$0`; everything else gets `2`/`3` suffixes;
2536/// method keys include erased parameter types):
2537/// Keys stay ORIGINAL (name, descriptor) so references resolve exactly;
2538/// only the rendered identifier changes.
2539/// Descriptor type string → internal class name (arrays/primitives →
2540/// None).
2541/// Can a method returning descriptor `m_ret` override an ancestor
2542/// declaration returning `r`? Equal, or a covariant subtype. ARRAY
2543/// returns must reach is_subtype in DESCRIPTOR form (it peels `[` for
2544/// element covariance): the old `strip L…;`-only comparison returned
2545/// false for `SavedState[] <: Object[]`, so Parcelable.Creator's real
2546/// `newArray(I)[LSavedState;` was judged an IMPOSTOR and renamed
2547/// (newArray3 — m_taken already held newArray + the bridge's newArray2),
2548/// breaking the interface override: "X$1不是抽象的, 并且未覆盖…
2549/// newArray(int)" (weibo ×504, battery ~789, rimet/news/uuyc ~655).
2550/// Object returns keep the internal-name form is_subtype's pool/fwdb
2551/// walk expects; an array m_ret against an object r resolves through
2552/// is_subtype's `sup == Object` / fwdb path (arrays ARE Objects).
2553fn ret_satisfies(pool: &DexPool, m_ret: &str, r: &str) -> bool {
2554 if m_ret == r {
2555 return true;
2556 }
2557 if m_ret.starts_with('[') {
2558 let sup: std::borrow::Cow<str> = if r.starts_with('[') {
2559 std::borrow::Cow::Borrowed(r)
2560 } else {
2561 match r.strip_prefix('L').and_then(|x| x.strip_suffix(';')) {
2562 Some(rc) => std::borrow::Cow::Borrowed(rc),
2563 None => return false, // primitive sup: only equality (above)
2564 }
2565 };
2566 return pool.is_subtype(m_ret, &sup);
2567 }
2568 if r.starts_with('[') {
2569 return false; // nothing but an array satisfies an array return
2570 }
2571 match (
2572 r.strip_prefix('L').and_then(|x| x.strip_suffix(';')),
2573 m_ret.strip_prefix('L').and_then(|x| x.strip_suffix(';')),
2574 ) {
2575 (Some(rc), Some(mc)) => pool.is_subtype(mc, rc),
2576 _ => false,
2577 }
2578}
2579
2580/// Raw name → (argsig, ret) JVM-descriptor slices of one class's own
2581/// virtual methods — the key space the collision queries use.
2582type AncMethods = jdc_core::FxHashMap<
2583 std::sync::Arc<str>,
2584 Vec<(std::sync::Arc<str>, std::sync::Arc<str>)>,
2585>;
2586
2587/// One provenance bucket of an analyzed class's ancestor chain (cached
2588/// layers, no merge — see `layer_rets`).
2589type AncLayers = Vec<std::sync::Arc<AncEntry>>;
2590
2591/// Parsed direct virtual methods of one ancestor class + its chain
2592/// edges, cached per class name for the member-collision walk. The
2593/// framework chains (View/Activity/Fragment…) and pool hub supers repeat
2594/// across thousands of analyzed classes — parsing each ancestor's
2595/// descriptors ONCE per run (instead of per analyzed subclass) removes
2596/// the dominant main-loop cost.
2597struct AncEntry {
2598 methods: AncMethods,
2599 /// Provenance bucket: FRAMEWORK ancestors (fwdb classes AND
2600 /// desugared `java.*`/`javax.*` pool copies — javac compiles against
2601 /// the android.jar originals, the covariant source truth) vs POOL
2602 /// ancestors (rendered RAW — the erased bridge is what satisfies the
2603 /// raw interface). The keeper preference flips on this axis (weibo
2604 /// Function0.invoke bridge must keep the name; WhatsApp SortedMap
2605 /// .keySet real override must).
2606 is_fw: bool,
2607 /// super + direct interfaces (chain continuation), Object-stripped.
2608 supers: Vec<std::sync::Arc<str>>,
2609}
2610
2611fn parse_anc_entry(pool: &DexPool, cn: &str) -> std::sync::Arc<AncEntry> {
2612 let mut methods: AncMethods = jdc_core::FxHashMap::default();
2613 let mut supers: Vec<std::sync::Arc<str>> = Vec::new();
2614 let is_fw;
2615 if let Some(ac) = pool.get_if_materialized(cn) {
2616 // Desugared `java.*`/`javax.*` copies in the dex are
2617 // pool-materialized, but javac compiles against the android.jar
2618 // ORIGINALS (covariant generics truth) — their declarations
2619 // belong to the fw bucket (WhatsApp bundles desugared
2620 // java.util.SortedMap; the pool bucket kept the
2621 // flattened-erasure bridge and renamed the real SortedSet
2622 // keySet).
2623 is_fw = cn.starts_with("java/") || cn.starts_with("javax/");
2624 for m in ac.all_methods() {
2625 if m.is_static() || &*m.name == "<init>" || &*m.name == "<clinit>"
2626 {
2627 continue;
2628 }
2629 let d: &str = &m.desc;
2630 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2631 let hi = d.find(')').unwrap_or(d.len());
2632 methods
2633 .entry(m.name.clone())
2634 .or_default()
2635 .push((
2636 std::sync::Arc::from(&d[lo..hi]),
2637 std::sync::Arc::from(&d[hi + 1..]),
2638 ));
2639 }
2640 if let Some(s) = &ac.super_name {
2641 if s != "java/lang/Object" {
2642 supers.push(std::sync::Arc::from(s.as_str()));
2643 }
2644 }
2645 supers.extend(ac.interfaces.iter().map(|i| std::sync::Arc::from(i.as_str())));
2646 } else {
2647 // FRAMEWORK ancestors bind names too: a group member
2648 // implementing a framework declaration must keep the original
2649 // name (renaming WhatsApp's Map.keySet-style overrides broke
2650 // the interface chain — cannot-find:变量 ×18.6k). fwdb
2651 // descriptors are erased JVM form — directly comparable with
2652 // dex descs. A class that is NEITHER pool nor fwdb (phantom)
2653 // parses to an empty entry — a chain dead-end, same as before.
2654 is_fw = true;
2655 crate::fwdb::for_each_method(cn, |d, f| {
2656 if f & crate::fwdb::MF_STATIC != 0 {
2657 return;
2658 }
2659 let Some(open) = d.find('(') else {
2660 return;
2661 };
2662 let lo = open + 1;
2663 let hi = d.find(')').unwrap_or(0);
2664 // NOTE: hi == lo is the zero-arg case, not malformed; and
2665 // the name slice ends at `open` (a `d[..lo]` cut kept the
2666 // `(` — every fw key missed its gkey lookup).
2667 if hi == 0 || hi + 1 >= d.len() {
2668 return;
2669 }
2670 methods
2671 .entry(std::sync::Arc::from(&d[..open]))
2672 .or_default()
2673 .push((
2674 std::sync::Arc::from(&d[lo..hi]),
2675 std::sync::Arc::from(&d[hi + 1..]),
2676 ));
2677 });
2678 if let Some(sup) = crate::fwdb::super_of(cn) {
2679 if sup != "java/lang/Object" {
2680 supers.push(std::sync::Arc::from(sup));
2681 }
2682 }
2683 crate::fwdb::for_each_interface(cn, |i| supers.push(std::sync::Arc::from(i)));
2684 }
2685 std::sync::Arc::new(AncEntry {
2686 methods,
2687 is_fw,
2688 supers,
2689 })
2690}
2691
2692/// Rets declared for `(name, argsig)` by any layer of a chain — lazy,
2693/// no merge (every consumer is an "any ancestor declares this" query,
2694/// so a per-layer scan replaces the old per-class eager map build).
2695/// Alloc-free: `Arc<str>: Borrow<str>` keys the hash lookup by `&str`.
2696fn layer_rets<'a>(
2697 layers: &'a [std::sync::Arc<AncEntry>],
2698 name: &'a str,
2699 argsig: &'a str,
2700) -> impl Iterator<Item = &'a str> {
2701 layers
2702 .iter()
2703 .filter_map(move |e| e.methods.get(name))
2704 .flatten()
2705 .filter(move |(a, _)| &**a == argsig)
2706 .map(|(_, r)| &**r)
2707}
2708
2709fn member_collision_renames(
2710 pool: &DexPool,
2711) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
2712 let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
2713 HashMap::default();
2714 // Direct nested tails per base class (`C$a` → a for base C), for
2715 // the companion-field rule below.
2716 let mut child_tails: jdc_core::FxHashMap<&str, Vec<&str>> =
2717 jdc_core::FxHashMap::default();
2718 // Per-package RENAMED class display heads: a MINTED field display
2719 // equal to one of these shadows same-package qualified type refs
2720 // (`a2.changeQuickRedirect` binds to the field when the class a2's
2721 // display is a2 — the inherited rule minting field a→a2 ONTO the
2722 // pkg-renamed class display created exactly this: weibo +194
2723 // cannot-find). Keyed on the POST-rename display (what refs render),
2724 // covering each class's own display too.
2725 let root_segs = pool.root_pkg_segs();
2726 // Lazy single-class pools build the global tables from MATERIALIZED
2727 // classes only (the target + its on-demand family): unmateralized
2728 // siblings matter to whole-program javac, which a one-class stdout
2729 // dump does not have — the same reduced-fidelity contract the
2730 // obscuring rules already document for lazy pools. The walk over
2731 // every registered name was the dominant field-rename cost of
2732 // getclass (~0.5s on weibo for one class).
2733 let lazy = !pool_majority_materialized(pool);
2734 let mut pkg_displays: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<String>> =
2735 jdc_core::FxHashMap::default();
2736 for n in &pool.order {
2737 if lazy && pool.get_if_materialized(n).is_none() {
2738 continue;
2739 }
2740 let renamed = crate::apply_class_rename(n);
2741 let (pkg, simple) = match renamed.rfind('/') {
2742 Some(i) => (renamed[..i].to_string(), renamed[i + 1..].to_string()),
2743 None => (String::new(), renamed.to_string()),
2744 };
2745 let head = simple.split('$').next().unwrap_or(&simple).to_string();
2746 if !head.is_empty() {
2747 let pkg_key = n.rfind('/').map(|i| &n[..i]).unwrap_or("");
2748 let _ = pkg;
2749 pkg_displays.entry(pkg_key).or_default().insert(head);
2750 }
2751 }
2752 // Reverse super map — the obscuring rename must register under every
2753 // transitive subclass (dex refs of an inherited field may name any
2754 // hierarchy class as owner).
2755 let mut subs: jdc_core::FxHashMap<&str, Vec<&str>> = jdc_core::FxHashMap::default();
2756 for n in &pool.order {
2757 if lazy && pool.get_if_materialized(n).is_none() {
2758 continue;
2759 }
2760 // EVERY `$` boundary: each prefix is an owner and the segment
2761 // after it one of its direct member-type tails. The old
2762 // first-segment-only map left NESTED owners (ta8/t$a, androidx
2763 // i0$e) with no member displays — their fields kept or minted
2764 // names colliding with their own member types, and in qualified
2765 // chains a VARIABLE OBSCURES a member type (JLS 6.4.2):
2766 // `ta8.t.a.a100.a` (Kotlin-object singleton sget, nova
2767 // static-ctx ×1,281) and `i0.e.b.ADDING` (lark ×145) bound to
2768 // the field instead of the nested class.
2769 for (i, _) in n.match_indices('$') {
2770 let owner = &n[..i];
2771 let rest = &n[i + 1..];
2772 let tail = match rest.find('$') {
2773 Some(j) => &rest[..j],
2774 None => rest,
2775 };
2776 if owner.is_empty() || tail.is_empty() {
2777 continue;
2778 }
2779 let v = child_tails.entry(owner).or_default();
2780 if !v.contains(&tail) {
2781 v.push(tail);
2782 }
2783 }
2784 if let Some(pc) = pool.get_if_materialized(n) {
2785 if let Some(sup) = &pc.super_name {
2786 if sup != "java/lang/Object" {
2787 subs.entry(sup.as_str()).or_default().push(n.as_str());
2788 }
2789 }
2790 }
2791 }
2792 // Per-ancestor parse cache (function-local: lives exactly as long
2793 // as this rename install, so materialization state is consistent by
2794 // construction; freed on return — no cross-run staleness, no RSS
2795 // tail).
2796 let mut anc_cache: jdc_core::FxHashMap<
2797 std::sync::Arc<str>,
2798 std::sync::Arc<AncEntry>,
2799 > = jdc_core::FxHashMap::default();
2800 for name in &pool.order {
2801 let Some(pc) = pool.get_if_materialized(name) else {
2802 continue;
2803 };
2804 let outer_ref_desc = find_outer_name(pool, name)
2805 .filter(|o| o != name)
2806 .map(|o| format!("L{o};"));
2807 // ---- fields: one shared namespace (static + instance) ----
2808 let fields: Vec<&PoolField> = pc
2809 .static_fields
2810 .iter()
2811 .chain(pc.instance_fields.iter())
2812 .collect();
2813 let fgroups: HashMap<String, Vec<&PoolField>> = HashMap::default();
2814 let mut fgroups = fgroups;
2815 for f in &fields {
2816 let disp = crate::classdec::java_ident(&f.name).into_owned();
2817 fgroups.entry(disp).or_default().push(f);
2818 }
2819 let mut f_taken: jdc_core::FxHashSet<String> = fields
2820 .iter()
2821 .map(|f| crate::classdec::java_ident(&f.name).into_owned())
2822 .collect();
2823 // Displays already registered under THIS class by ancestor
2824 // subs-propagation (an ancestor processed earlier in pool.order
2825 // pushed its renames down): the own mints must avoid them or
2826 // two fields in one class render the same display
2827 // ("已在类中定义了变量 a2", weibo bx$b: subs-pushed bv→a2 beside
2828 // its own String→a2).
2829 if let Some(v) = out.get(&std::sync::Arc::from(name.as_str())) {
2830 for fr in v {
2831 f_taken.insert(fr.display.to_string());
2832 }
2833 }
2834 for group in fgroups.values() {
2835 if group.len() < 2 {
2836 continue;
2837 }
2838 let keeper_pos = group
2839 .iter()
2840 .position(|f| f.access & crate::access::ACC_SYNTHETIC == 0)
2841 .unwrap_or(0);
2842 for (gi, f) in group.iter().enumerate() {
2843 if gi == keeper_pos {
2844 continue;
2845 }
2846 let base = crate::classdec::java_ident(&f.name).into_owned();
2847 let display: String = match &outer_ref_desc {
2848 Some(ord)
2849 if f.access & crate::access::ACC_SYNTHETIC != 0
2850 && !f.is_static
2851 && *ord == *f.desc =>
2852 {
2853 let mut k = 0u32;
2854 loop {
2855 let cand = if k == 0 {
2856 "this$0".to_string()
2857 } else {
2858 format!("this$0{k}")
2859 };
2860 if f_taken.insert(cand.clone()) {
2861 break cand;
2862 }
2863 k += 1;
2864 }
2865 }
2866 _ => suffix_unique(&base, &mut f_taken),
2867 };
2868 // Register with subclass propagation: dex refs of an
2869 // inherited field may name ANY hierarchy class as owner
2870 // (R8 writes the referrer), and without the propagation
2871 // the declaring class's rename does not reach the
2872 // subclass-keyed lookups — the ref renders the RAW name
2873 // against a renamed declaration (bx$b read its inherited
2874 // `a:Lcom/xiaomi/push/bv;` as `this.a` while bx$a
2875 // declared it `a2`). Mirrors the method-side hierarchy
2876 // consistency rule.
2877 register_field_rename_with_subs(
2878 pool, &subs, &mut out, name, &f.name, &f.desc, &display,
2879 );
2880 }
2881 }
2882 // ---- fields obscuring nested types (JLS 6.4.2) ----
2883 // `C.b.a(..)` with BOTH a field `b` and a member class `C$b`
2884 // binds to the FIELD — the qualified type reading is obscured
2885 // ("无法从静态上下文中引用非静态 变量 b" + the paired cannot-find
2886 // cascade; lark LKEvent/MotionLayout family, static-ctx ×1,154).
2887 // The CHILD-side rule (nested_collision_renames) already renames
2888 // colliding children when its local_ok gate passes (`a$a` renders
2889 // as `a2`); this field-side rule covers the gate-REJECTED residual
2890 // (child keeps display `b`, program-wide refs block its rename).
2891 // A field's refs all funnel through field_display (lift call
2892 // sites, declarations, const inits), so the field side needs no
2893 // locality gate. TWO invariants the first attempt violated (lark
2894 // +8,129): fire on the child's FINAL DISPLAY last segment, not
2895 // the raw tail (a child the child-side rule already renamed does
2896 // NOT obscure), and mint avoiding every child display (`a`→`a2`
2897 // landed on the existing `interface a2`, minting a fresh
2898 // obscuring pair).
2899 {
2900 let tails: &[&str] = child_tails
2901 .get(name.as_str())
2902 .map(|v| v.as_slice())
2903 .unwrap_or(&[]);
2904 let mut member_displays: jdc_core::FxHashSet<String> =
2905 jdc_core::FxHashSet::default();
2906 let mut obscuring: jdc_core::FxHashSet<String> =
2907 jdc_core::FxHashSet::default();
2908 for t in tails {
2909 let full = format!("{name}${t}");
2910 let renamed = crate::apply_class_rename(&full);
2911 let last = renamed
2912 .rsplit('/')
2913 .next()
2914 .unwrap_or(&renamed)
2915 .rsplit('$')
2916 .next()
2917 .unwrap_or(t)
2918 .to_string();
2919 member_displays.insert(last.clone());
2920 // Only an INLINE-rendered member is reachable by the bare
2921 // simple name (own-file children are referenced by their
2922 // flat `$` form, which fields cannot obscure).
2923 let member_rendered = clean_member_tail(t)
2924 && crate::find_outer_name(pool, &full).as_deref()
2925 == Some(name.as_str());
2926 if member_rendered {
2927 obscuring.insert(last);
2928 }
2929 }
2930 // The class's OWN simple display name joins the obscuring
2931 // set: a field named like its class captures every
2932 // qualified self-static ref from nested scopes
2933 // (`a2.changeQuickRedirect` bound to the String FIELD a2 —
2934 // weibo robust refs ×317; bare refs survive via the
2935 // variable namespace, the qualified form does not).
2936 let self_renamed = crate::apply_class_rename(name);
2937 let self_simple = self_renamed
2938 .rsplit('/')
2939 .next()
2940 .unwrap_or(&self_renamed)
2941 .rsplit('$')
2942 .next()
2943 .unwrap_or("")
2944 .to_string();
2945 if !self_simple.is_empty() {
2946 obscuring.insert(crate::classdec::java_ident(&self_simple).into_owned());
2947 }
2948 let self_pkg = name.rfind('/').map(|i| &name[..i]).unwrap_or("");
2949 let pkg_names = pkg_displays.get(self_pkg);
2950 for f in &fields {
2951 let fdisp = crate::classdec::java_ident(&f.name).into_owned();
2952 if fdisp.starts_with("this$") || !obscuring.contains(fdisp.as_str()) {
2953 continue;
2954 }
2955 // Already renamed by the same-class duplicate-group rule?
2956 // Its display no longer equals the obscuring simple name,
2957 // so this rule's target collision does not exist — and
2958 // minting a SECOND entry for the same (name, desc) with
2959 // subs-propagation put a CONFLICTING display under every
2960 // subclass: refs naming the subclass resolved to one name
2961 // while the declaring class's declaration rendered the
2962 // first entry's (weibo bx$b read its inherited
2963 // `a:Lcom/xiaomi/push/bv;` as `this.a8` while bx$a
2964 // declared it `a2` — the R8 same-name-field family).
2965 let already = out
2966 .get(&std::sync::Arc::from(name.as_str()))
2967 .is_some_and(|v| {
2968 v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
2969 });
2970 if already {
2971 continue;
2972 }
2973 let mut k = 1u32;
2974 let display = loop {
2975 k += 1;
2976 let cand = format!("{fdisp}{k}");
2977 // root_segs: a minted display equal to a PACKAGE
2978 // first segment captures every qualified ref to
2979 // that package inside the family's file (field e2
2980 // ate `e2.c.b(..)` — lark pq/e "变量 c" ×47). The
2981 // deshadow builder cannot repair this after the
2982 // fact: it scans RAW names, and the collision is
2983 // born here in display space.
2984 if !f_taken.contains(&cand)
2985 && !member_displays.contains(&cand)
2986 && !root_segs.contains(&cand)
2987 && !pkg_names.is_some_and(|p| p.contains(cand.as_str()))
2988 {
2989 f_taken.insert(cand.clone());
2990 member_displays.insert(cand.clone());
2991 break cand;
2992 }
2993 };
2994 register_field_rename_with_subs(
2995 pool, &subs, &mut out, name, &f.name, &f.desc, &display,
2996 );
2997 if std::env::var("DDC_OBSCURE_STATS").is_ok() {
2998 eprintln!("[obscure] {name} field {} -> {}", f.name, display);
2999 }
3000 }
3001 }
3002 // ---- companion-holder fields vs nested types (JLS 6.4.2 in
3003 // expression context): Kotlin compiles `object`/companion
3004 // holders as a static field whose name EQUALS the nested class
3005 // it instances (`static final j$a a`), and static members of the
3006 // nested are invoked as `j.a.c(..)` — but `j.a` in an EXPRESSION
3007 // resolves to the FIELD (variables outrank member types), so the
3008 // call becomes a member lookup on the instance type... which is
3009 // the same class, yet javac reports 找不到符号 for statics
3010 // reached through the shadowed path, and the whole file
3011 // error-types (weibo feed/business/j: 654 errors + closure
3012 // cascade; the nested-side rename for this is the LOCAL-OK-gated
3013 // rule that explodes ungated — the FIELD side is safe because
3014 // dex field refs always carry the declaring class, so this
3015 // registry keys every consumer). Gate: the nested class has
3016 // static members (companion shape) and the field's descriptor
3017 // is exactly the nested type.
3018 if let Some(tails) = child_tails.get(name.as_str()) {
3019 for tail in tails {
3020 let nested = format!("{name}${tail}");
3021 let Some(nc) = pool.get_if_materialized(&nested) else {
3022 continue;
3023 };
3024 // Only CLEAN tails render dotted (`j.a`); flat-rendered
3025 // tails (`j$1`) carry the `$` and never clash.
3026 if !clean_member_tail(tail) {
3027 continue;
3028 }
3029 let has_statics = nc
3030 .static_fields
3031 .iter()
3032 .chain(nc.instance_fields.iter())
3033 .any(|f| f.is_static)
3034 || nc.all_methods().any(|m| m.access & crate::access::ACC_STATIC != 0);
3035 if !has_statics {
3036 continue;
3037 }
3038 // STRICT companion shape: the field's descriptor IS the
3039 // nested type (`static final j$a a`). The relaxed form
3040 // (any same-named field beside a statics-bearing nested)
3041 // minted thousands of renames on weibo and exploded the
3042 // battery to the 500k cap — the field funnel does not
3043 // hold at that scale (改名面 vs 引用覆盖面, again).
3044 let want_desc = format!("L{nested};");
3045 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
3046 if f.desc.as_ref() != want_desc.as_str() {
3047 continue;
3048 }
3049 let disp = crate::classdec::java_ident(&f.name).into_owned();
3050 if disp != crate::classdec::java_ident(tail) {
3051 continue;
3052 }
3053 // Already renamed by the duplicate-group rule above?
3054 let already = out
3055 .get(&std::sync::Arc::from(name.as_str()))
3056 .is_some_and(|v| {
3057 v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
3058 });
3059 if already {
3060 continue;
3061 }
3062 let display = suffix_unique(&disp, &mut f_taken);
3063 f_taken.insert(display.clone());
3064 out.entry(std::sync::Arc::from(name.as_str()))
3065 .or_default()
3066 .push(jdc_core::rename::FieldRename {
3067 name: f.name.clone(),
3068 desc: f.desc.clone(),
3069 display: std::sync::Arc::from(display.as_str()),
3070 });
3071 }
3072 }
3073 }
3074 // ---- methods: display key = sanitized name + erased params ----
3075 let methods: Vec<&PoolMethod> = pc.all_methods().collect();
3076 // Ancestor virtual-method returns per (name, argsig) — one
3077 // chain walk per non-interface class, shared by the
3078 // covariant-keeper selection and the impostor-override
3079 // detection below. Layers stay PER-ANCESTOR (cached Arcs, no
3080 // merge): every consumer is an "any ancestor declares
3081 // (name, argsig) → ret" query, so a per-layer scan replaces
3082 // the old per-class eager map build. Provenance splits the
3083 // chain into fw / pool buckets (see AncEntry.is_fw).
3084 let mut anc_chain: Option<(AncLayers, AncLayers)> = None;
3085 if pc.access & crate::access::ACC_INTERFACE == 0 {
3086 let mut fw_layers: AncLayers = Vec::new();
3087 let mut pool_layers: AncLayers = Vec::new();
3088 let mut stack: Vec<std::sync::Arc<str>> = Vec::new();
3089 if let Some(sup) = &pc.super_name {
3090 if sup != "java/lang/Object" {
3091 stack.push(std::sync::Arc::from(sup.as_str()));
3092 }
3093 }
3094 stack.extend(
3095 pc.interfaces
3096 .iter()
3097 .map(|i| std::sync::Arc::from(i.as_str())),
3098 );
3099 let mut seen_cls: jdc_core::FxHashSet<std::sync::Arc<str>> =
3100 jdc_core::FxHashSet::default();
3101 while let Some(cn) = stack.pop() {
3102 if !seen_cls.insert(cn.clone()) {
3103 continue;
3104 }
3105 let entry = match anc_cache.get(cn.as_ref()) {
3106 Some(e) => e.clone(),
3107 None => {
3108 let e = parse_anc_entry(pool, &cn);
3109 anc_cache.insert(cn.clone(), e.clone());
3110 e
3111 }
3112 };
3113 if !entry.methods.is_empty() {
3114 if entry.is_fw {
3115 fw_layers.push(entry.clone());
3116 } else {
3117 pool_layers.push(entry.clone());
3118 }
3119 }
3120 stack.extend(entry.supers.iter().cloned());
3121 }
3122 anc_chain = Some((fw_layers, pool_layers));
3123 }
3124 let mut m_taken: jdc_core::FxHashSet<String> = methods
3125 .iter()
3126 .map(|m| crate::classdec::java_ident(&m.name).into_owned())
3127 .collect();
3128 let mut mgroups: HashMap<(String, String), Vec<&PoolMethod>> = HashMap::default();
3129 for m in &methods {
3130 let d: &str = &m.desc;
3131 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3132 let hi = d.find(')').unwrap_or(d.len());
3133 let key = (
3134 crate::classdec::java_ident(&m.name).into_owned(),
3135 d[lo..hi].to_string(),
3136 );
3137 mgroups.entry(key).or_default().push(m);
3138 }
3139 for ((base, _), group) in &mgroups {
3140 if group.len() < 2 {
3141 continue;
3142 }
3143 // R8 return-type specialization: a non-isolated class carries
3144 // `M(args)void` (the real logic) beside the interface bridge
3145 // `M(args)R` (unbox → call void → return Unit). Same erasure,
3146 // so Java source can't hold both, and `void` never satisfies
3147 // the interface's `R` — the claim map keeps the void side and
3148 // drops the bridge, yielding "X不是抽象的, 并且未覆盖…invoke"
3149 // (Kotlin suspend lambdas, weixin/lark ×1.1k). This is NOT the
3150 // covariant-override shape below (void has no subtype
3151 // relation): keep the BRIDGE under the original name so it
3152 // satisfies the interface, and rename the void specialization
3153 // so both render and the bridge's call to it resolves to the
3154 // renamed method.
3155 // `)Ljava/lang/Void;` counts as void-like: a Kotlin `Nothing`
3156 // function (an unsupported-op that always throws) compiles to
3157 // the Void CLASS return, not the `V` primitive, but it IS the
3158 // real-logic side of the same specialization pair. Missing it
3159 // sent the group down the covariant path, which kept the Void
3160 // method and renamed the bridge, then the impostor pass renamed
3161 // the Void method too — leaving NO `addAll` to satisfy Set
3162 // (weibo compose SnapshotMapEntrySet "未覆盖Set中的addAll").
3163 let ret_void = |m: &PoolMethod| {
3164 m.desc.ends_with(")V") || m.desc.ends_with(")Ljava/lang/Void;")
3165 };
3166 let isolated = pc
3167 .super_name
3168 .as_ref()
3169 .is_none_or(|s| s == "java/lang/Object")
3170 && pc.interfaces.is_empty();
3171 let has_void = group.iter().any(|m| ret_void(m) && &*m.name != "<init>");
3172 let has_nonvoid = group.iter().any(|m| !ret_void(m));
3173 if !isolated && has_void && has_nonvoid {
3174 let keeper_desc: Option<&str> = group
3175 .iter()
3176 .copied()
3177 .find(|m| {
3178 m.access & crate::access::ACC_BRIDGE != 0
3179 && !ret_void(m)
3180 && &*m.name != "<init>"
3181 })
3182 .or_else(|| {
3183 group
3184 .iter()
3185 .copied()
3186 .find(|m| !ret_void(m) && &*m.name != "<init>")
3187 })
3188 .map(|m| &*m.desc);
3189 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
3190 jdc_core::FxHashSet::default();
3191 for m in group.iter().copied() {
3192 if keeper_desc == Some(&*m.desc) {
3193 seen_orig.insert((&*m.name, &*m.desc));
3194 continue; // bridge keeps the interface name
3195 }
3196 if !seen_orig.insert((&*m.name, &*m.desc)) {
3197 continue; // exact duplicate: emitter drops it
3198 }
3199 let display = suffix_unique(base, &mut m_taken);
3200 out.entry(std::sync::Arc::from(name.as_str()))
3201 .or_default()
3202 .push(jdc_core::rename::FieldRename {
3203 name: m.name.clone(),
3204 desc: m.desc.clone(),
3205 display: std::sync::Arc::from(display.as_str()),
3206 });
3207 }
3208 continue;
3209 }
3210 // A COVARIANT override pair (same ORIGINAL name, different
3211 // descriptors — the compiler's bridge shape: the interface's
3212 // `deserialize(e)` plus the narrower implementation). Renaming
3213 // either side breaks @Override ("is not abstract and does not
3214 // override abstract method deserialize(e)", ~2.2k on reqable);
3215 // the emitter's claim logic already renders only the
3216 // non-bridge member of the pair. An ISOLATED class (extends
3217 // Object, implements nothing — d8's ExternalSyntheticApiModel
3218 // Outline shells) overrides nothing: its same-(name,params)
3219 // pairs differing only by RETURN type are dex-legal distinct
3220 // methods, and dropping one strands its callers on the
3221 // survivor (`HalfKt$$…0.m()Class` vs `.m()V` — "void无法
3222 // 转换为Class", weibo ×85+).
3223 let covariant = !isolated
3224 && group
3225 .iter()
3226 .any(|m| group.iter().any(|o| o.name == m.name && o.desc != m.desc));
3227 if covariant {
3228 // Return-type-only clash in a class WITH a hierarchy:
3229 // Java cannot declare both, and the claim logic would
3230 // keep an arbitrary one — when that is NOT the member
3231 // an ancestor declares, every concrete subclass fails
3232 // "X不是抽象的, 并且未覆盖…" (weibo ×590: viewmodel.a
3233 // kept getUsageInfo()e while interface d declares
3234 // getUsageInfo()g; the kept side also strands the
3235 // dropped side's callers). Keep the ancestor-declared
3236 // member under the original name and rename the rest —
3237 // the registry carries every call site, and the
3238 // propagation below shares the display down the family.
3239 // 0 or 2+ ancestor-matched members = the shape Java
3240 // cannot express at all (two interfaces demanding
3241 // different returns): keep the old claim behavior.
3242 let keepers: Vec<&&PoolMethod> = group
3243 .iter()
3244 .filter(|m| {
3245 let d: &str = &m.desc;
3246 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3247 let hi = d.find(')').unwrap_or(d.len());
3248 if let Some((fwl, pll)) = &anc_chain {
3249 layer_rets(fwl, &m.name, &d[lo..hi])
3250 .chain(layer_rets(pll, &m.name, &d[lo..hi]))
3251 .any(|r| r == &d[hi + 1..])
3252 } else {
3253 false
3254 }
3255 })
3256 .collect();
3257 if keepers.len() >= 2 {
3258 // The shape Java cannot express at all (2+
3259 // ancestors demanding different returns): no
3260 // rename target — keep the claim behavior.
3261 continue;
3262 }
3263 // fwdb flattens JDK covariant overrides to the erased
3264 // declaring return (SortedMap.keySet records Set, the
3265 // source return is SortedSet) — the sole "keeper" can
3266 // be the synthetic BRIDGE while the real override is
3267 // the non-bridge sibling whose return is a subtype of
3268 // the recorded one. Keeping the bridge renamed the
3269 // real method and broke the interface chain (WhatsApp
3270 // X/Jl2 SortedMap, cannot-find:变量 ×18.6k). Prefer the
3271 // non-bridge in that shape: the claim map dropped the
3272 // bridge before this machinery existed anyway.
3273 // Keeper selection by ancestor PROVENANCE:
3274 // - POOL ancestor matches (raw-rendered): the member
3275 // whose return EXACTLY matches a pool erasure that
3276 // is NOT purely framework-sourced keeps the name —
3277 // the erased bridge is what satisfies the raw
3278 // interface (weibo Function0.invoke / GlideRequest
3279 // .load; renaming the real one instead cost +375).
3280 // - FRAMEWORK-only match: javac reads the compiled
3281 // covariant truth (SortedMap.keySet→SortedSet) while
3282 // fwdb records the flattened erasure (→Set) that the
3283 // BRIDGE matches — prefer the non-bridge sibling
3284 // whose return is a subtype (WhatsApp X/Jl2).
3285 // - no ancestor: first non-bridge (the old claim
3286 // survivor), else dex order.
3287 fn ret_desc(m: &PoolMethod) -> &str {
3288 let d: &str = &m.desc;
3289 &d[d.find(')').map(|i| i + 1).unwrap_or(d.len())..]
3290 }
3291 let gkey = {
3292 let d: &str = &group[0].desc;
3293 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3294 let hi = d.find(')').unwrap_or(d.len());
3295 (
3296 crate::classdec::java_ident(&group[0].name).into_owned(),
3297 d[lo..hi].to_string(),
3298 )
3299 };
3300 // fw match: a NON-BRIDGE member whose return is
3301 // assignable to a framework ret (covariant source
3302 // truth — it satisfies the raw pool ancestor too,
3303 // overrides being covariant-legal in Java).
3304 let fw_i = anc_chain.as_ref().and_then(|(fwl, _)| {
3305 let fws: Vec<&str> =
3306 layer_rets(fwl, &gkey.0, &gkey.1).collect();
3307 if fws.is_empty() {
3308 return None;
3309 }
3310 group.iter().position(|m| {
3311 m.access & crate::access::ACC_BRIDGE == 0
3312 && fws.iter().any(|fr| {
3313 // ret_satisfies: descriptor-level, so
3314 // ARRAY covariant returns match too (the
3315 // old ret_internal strip returned None
3316 // for `[LX;` and fell back to descriptor
3317 // equality — Creator newArray's real
3318 // `[LSavedState;` never matched fw's
3319 // `[LObject;`).
3320 ret_satisfies(pool, ret_desc(m), fr)
3321 })
3322 })
3323 });
3324 let pool_i = anc_chain.as_ref().and_then(|(_, pll)| {
3325 let rets: Vec<&str> =
3326 layer_rets(pll, &gkey.0, &gkey.1).collect();
3327 if rets.is_empty() {
3328 return None;
3329 }
3330 group
3331 .iter()
3332 .position(|m| rets.iter().any(|r| *r == ret_desc(m)))
3333 });
3334 let keeper_idx = match (fw_i, pool_i) {
3335 (Some(fi), _) => fi,
3336 (None, Some(pi)) => pi,
3337 (None, None) => group
3338 .iter()
3339 .position(|m| m.access & crate::access::ACC_BRIDGE == 0)
3340 .unwrap_or(0),
3341 };
3342 // keepers==1: the ancestor-declared member keeps the
3343 // original name. keepers==0: NOTHING in the POOL binds
3344 // the name — keep the member the old claim map would
3345 // have kept (first NON-BRIDGE, else dex order) and
3346 // rename the rest through the registry: letting the
3347 // claim map silently drop a side stranded its callers
3348 // on the survivor's incompatible return (weibo xiaomi
3349 // push gk/dq/hj/hf/gt: `boolean b()Z` dropped beside
3350 // `String b()` — every `boolean v = this.b()` bound to
3351 // the String twin, "String无法转换为boolean" ×300+ in
3352 // the push package alone). NON-bridge preference is
3353 // load-bearing: a framework-ancestor covariant pair
3354 // (emoji2 n extends SpannableStringBuilder, Editable-
3355 // bridge beside the real override) satisfies the
3356 // ancestor through the REAL method — bridge-preference
3357 // renamed it and broke the override chain (reqable +8
3358 // "无法覆盖…返回类型不兼容").
3359 let keep_desc: &str = &group[keeper_idx].desc;
3360 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
3361 jdc_core::FxHashSet::default();
3362 for m in group.iter().copied() {
3363 if m.desc.as_ref() == keep_desc {
3364 seen_orig.insert((&*m.name, &*m.desc));
3365 continue;
3366 }
3367 if !seen_orig.insert((&*m.name, &*m.desc)) {
3368 continue;
3369 }
3370 let display = suffix_unique(base, &mut m_taken);
3371 out.entry(std::sync::Arc::from(name.as_str()))
3372 .or_default()
3373 .push(jdc_core::rename::FieldRename {
3374 name: m.name.clone(),
3375 desc: m.desc.clone(),
3376 display: std::sync::Arc::from(display.as_str()),
3377 });
3378 }
3379 continue;
3380 }
3381 // Real re-declarations (same original name AND descriptor —
3382 // R8 duplicates) are already deduped by the emitter's claim
3383 // logic; only SANITIZER collapses reach here and every one of
3384 // them is a distinct method.
3385 let mut base = base.clone();
3386 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
3387 jdc_core::FxHashSet::default();
3388 for m in group {
3389 if !seen_orig.insert((&m.name, &m.desc)) {
3390 continue; // exact duplicate: emitter drops it
3391 }
3392 let display = suffix_unique(&base, &mut m_taken);
3393 out.entry(std::sync::Arc::from(name.as_str()))
3394 .or_default()
3395 .push(jdc_core::rename::FieldRename {
3396 name: m.name.clone(),
3397 desc: m.desc.clone(),
3398 display: std::sync::Arc::from(display.as_str()),
3399 });
3400 // The base stays the sanitized ORIGINAL so the first
3401 // occurrence keeps the plain name.
3402 let _ = &mut base;
3403 }
3404 }
3405 // ---- impostor overrides (R8 generic specialization) ----
3406 // A class method whose (name, argsig) matches an ancestor
3407 // declaration but whose return type satisfies NONE of the
3408 // ancestor returns is not an override Java can express: the
3409 // pair shares an erasure, so the class cannot declare both, and
3410 // ART never resolved it either (interface dispatch matches
3411 // name+descriptor — the missing proto throws AbstractMethodError
3412 // at runtime; weibo IStreamPresenter.getView()IStreamView vs the
3413 // impl's getView()IPageView with no subtype edge, ×258 across
3414 // getView/getWidget/getListView families). Rename the impostor
3415 // (the registry carries every caller and the propagation below
3416 // carries subclasses) and let synth_missing_interface_stubs add
3417 // the ART-faithful stub — rename-aware provision makes the
3418 // requirement missing again.
3419 if let Some((fwl, pll)) = &anc_chain {
3420 for m in &methods {
3421 if m.is_static()
3422 || &*m.name == "<init>"
3423 || &*m.name == "<clinit>"
3424 {
3425 continue;
3426 }
3427 let d: &str = &m.desc;
3428 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3429 let hi = d.find(')').unwrap_or(d.len());
3430 let m_ret = &d[hi + 1..];
3431 // Lazy per-layer scan: `found` replaces the old
3432 // `map.get(key)` presence gate (no ancestor declares
3433 // (name, argsig) ⇒ nothing to violate ⇒ skip).
3434 let mut found = false;
3435 let mut satisfied = false;
3436 'lay: for e in fwl.iter().chain(pll.iter()) {
3437 let Some(rets) = e.methods.get(m.name.as_ref()) else {
3438 continue;
3439 };
3440 for (a, r) in rets {
3441 if **a != d[lo..hi] {
3442 continue;
3443 }
3444 found = true;
3445 // ret_satisfies handles ARRAY covariance in
3446 // descriptor form — the old L…;-strip match
3447 // answered false for `SavedState[] <: Object[]`
3448 // and renamed the REAL Parcelable.Creator
3449 // newArray as an impostor (newArray3), breaking
3450 // the override ("不是抽象的…未覆盖 newArray(int)",
3451 // weibo ×504 / battery ~789).
3452 if ret_satisfies(pool, m_ret, r) {
3453 satisfied = true;
3454 break 'lay;
3455 }
3456 }
3457 }
3458 if !found || satisfied {
3459 continue;
3460 }
3461 // Already renamed by a clash-group rule above?
3462 let already = out
3463 .get(&std::sync::Arc::from(name.as_str()))
3464 .is_some_and(|v| {
3465 v.iter().any(|fr| *fr.name == *m.name && *fr.desc == *m.desc)
3466 });
3467 if already {
3468 continue;
3469 }
3470 let base = crate::classdec::java_ident(&m.name).into_owned();
3471 let display = suffix_unique(&base, &mut m_taken);
3472 out.entry(std::sync::Arc::from(name.as_str()))
3473 .or_default()
3474 .push(jdc_core::rename::FieldRename {
3475 name: m.name.clone(),
3476 desc: m.desc.clone(),
3477 display: std::sync::Arc::from(display.as_str()),
3478 });
3479 }
3480 }
3481 }
3482 // ---- method-rename propagation down the hierarchy ----
3483 // A renamed method must keep ONE display across its whole override
3484 // family: the parent's abstract declaration and every subclass
3485 // implementation/caller render through the registry, and a missed
3486 // link re-breaks the override ("未覆盖" at each concrete subclass)
3487 // or the call site (cannot-find). Register each method entry on the
3488 // full transitive subclass + implementer closure — a subclass that
3489 // does NOT declare the method still serves as a dex ref owner for
3490 // the inherited method, so it needs the entry too (same reasoning
3491 // as register_field_rename_with_subs for fields). Ancestor-forced
3492 // displays win; a colliding local entry re-mints off the original
3493 // method name and re-propagates.
3494 {
3495 let mut down: jdc_core::FxHashMap<&str, Vec<&str>> =
3496 jdc_core::FxHashMap::default();
3497 for (sup, children) in subs.iter() {
3498 down.entry(*sup).or_default().extend(children.iter().copied());
3499 }
3500 for n in &pool.order {
3501 if let Some(pc) = pool.get_if_materialized(n) {
3502 for i in pc.interfaces.iter() {
3503 down.entry(i.as_str()).or_default().push(n.as_str());
3504 }
3505 }
3506 }
3507 type PropEntry = (
3508 std::sync::Arc<str>,
3509 std::sync::Arc<str>,
3510 std::sync::Arc<str>,
3511 std::sync::Arc<str>,
3512 );
3513 let mut queue: std::collections::VecDeque<PropEntry> =
3514 std::collections::VecDeque::new();
3515 {
3516 // Sorted seed: `out` is a std HashMap (RandomState) — an
3517 // unsorted seed would make conflicting ancestor forces
3518 // resolve in per-process order (display names flipping
3519 // run-to-run).
3520 let mut seed: Vec<PropEntry> = Vec::new();
3521 for (cls, entries) in out.iter() {
3522 for fr in entries {
3523 if fr.desc.contains('(') {
3524 seed.push((
3525 cls.clone(),
3526 fr.name.clone(),
3527 fr.desc.clone(),
3528 fr.display.clone(),
3529 ));
3530 }
3531 }
3532 }
3533 seed.sort();
3534 for t in seed {
3535 queue.push_back(t);
3536 }
3537 }
3538 let mut guard = 0usize;
3539 while let Some((cls, n, d, disp)) = queue.pop_front() {
3540 guard += 1;
3541 if guard > 2_000_000 {
3542 break;
3543 }
3544 let Some(children) = down.get(cls.as_ref()).cloned() else {
3545 continue;
3546 };
3547 for child in children {
3548 let mut changed = false;
3549 let mut reminted: Vec<PropEntry> = Vec::new();
3550 {
3551 let child_key: std::sync::Arc<str> = std::sync::Arc::from(child);
3552 let entries = out.entry(child_key.clone()).or_default();
3553 // A raw method of the child literally named `disp`
3554 // with a different signature blocks the force —
3555 // renaming that raw method instead is the over-reach
3556 // class (500k blowup history); skip this child.
3557 let raw_blocked = pool
3558 .get_if_materialized(child)
3559 .is_some_and(|pc| {
3560 pc.all_methods().any(|m| {
3561 crate::classdec::java_ident(&m.name) == disp.as_ref()
3562 && m.desc != d
3563 })
3564 });
3565 if raw_blocked {
3566 continue;
3567 }
3568 match entries
3569 .iter_mut()
3570 .find(|fr| *fr.name == *n && *fr.desc == *d)
3571 {
3572 Some(e) if e.display == disp => {}
3573 Some(e) => {
3574 e.display = disp.clone();
3575 changed = true;
3576 }
3577 None => {
3578 entries.push(jdc_core::rename::FieldRename {
3579 name: n.clone(),
3580 desc: d.clone(),
3581 display: disp.clone(),
3582 });
3583 changed = true;
3584 }
3585 }
3586 if changed {
3587 // Display collisions inside the child: the
3588 // ancestor-forced entry keeps `disp`; others
3589 // re-mint from their original method name.
3590 let mut taken: jdc_core::FxHashSet<String> =
3591 jdc_core::FxHashSet::default();
3592 if let Some(pc) = pool.get_if_materialized(child) {
3593 for m in pc.all_methods() {
3594 taken.insert(
3595 crate::classdec::java_ident(&m.name).into_owned(),
3596 );
3597 }
3598 }
3599 for e in entries.iter() {
3600 taken.insert(e.display.to_string());
3601 }
3602 let mut seen: jdc_core::FxHashSet<String> =
3603 jdc_core::FxHashSet::default();
3604 seen.insert(disp.to_string());
3605 for e in entries.iter_mut() {
3606 // FIELD entries never re-mint here: this is
3607 // the METHOD-name propagation, and a field
3608 // display colliding with a forced method
3609 // display is resolved at the field's own mint
3610 // (f_taken seeding above), not by renaming the
3611 // field out from under its subs-registered
3612 // references (bx$b's inherited bv→a2 was
3613 // reminted to a7 while every ref kept a2).
3614 if !e.desc.contains('(') {
3615 continue;
3616 }
3617 if *e.name == *n && *e.desc == *d {
3618 continue;
3619 }
3620 if seen.insert(e.display.to_string()) {
3621 continue;
3622 }
3623 let base =
3624 crate::classdec::java_ident(&e.name).into_owned();
3625 let nd: std::sync::Arc<str> = std::sync::Arc::from(
3626 suffix_unique(&base, &mut taken).as_str(),
3627 );
3628 e.display = nd.clone();
3629 reminted.push((
3630 child_key.clone(),
3631 e.name.clone(),
3632 e.desc.clone(),
3633 nd,
3634 ));
3635 }
3636 }
3637 }
3638 if changed {
3639 let child_key: std::sync::Arc<str> = std::sync::Arc::from(child);
3640 queue.push_back((child_key.clone(), n.clone(), d.clone(), disp.clone()));
3641 for r in reminted {
3642 queue.push_back(r);
3643 }
3644 }
3645 }
3646 }
3647 }
3648 inherited_obscuring_renames(pool, &mut out, &pkg_displays);
3649 out
3650}
3651
3652/// Inherited-field variant of the obscuring rule: class C renders a
3653/// member nested type `d` while INHERITING a field `d` from a pool
3654/// superclass — `C.d.b(..)` binds to the inherited variable (fields
3655/// obscure types, JLS 6.4.2) and fails ("无法从静态上下文中引用非静态
3656/// 变量 d" + "无法取消引用int" pairs, LynxBaseScrollViewDragging ×84 —
3657/// latent behind the broken-superclass hub until the same-class rule
3658/// healed it). The rename lands on the DECLARING class (one declaration
3659/// to re-render) and is registered under every transitive subclass as
3660/// well, because dex field refs may name any subclass as owner and the
3661/// lift-time consult keys on the ref's owner.
3662/// Register a field rename under the declaring class AND every transitive
3663/// subclass: dex field refs for an inherited access may name ANY class in
3664/// the hierarchy as owner (`iput Lh$a;->a` inside the constant-subclass
3665/// ctor of `h`), and the lift-time consult keys on the ref's owner — a
3666/// declaring-class-only entry leaves those refs stale (org/a/c/h ×3).
3667/// Subclasses that REDECLARE (name, desc) hide the field — their own
3668/// field is distinct and must keep its name (and their own obscuring,
3669/// if any, is handled when the loop reaches them).
3670fn register_field_rename_with_subs(
3671 pool: &DexPool,
3672 subs: &jdc_core::FxHashMap<&str, Vec<&str>>,
3673 out: &mut HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>>,
3674 owner: &str,
3675 fname: &str,
3676 fdesc: &str,
3677 display: &str,
3678) {
3679 let mut queue: Vec<&str> = vec![owner];
3680 let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
3681 seen.insert(owner);
3682 let mut first = true;
3683 while let Some(c) = queue.pop() {
3684 let redeclares = if first {
3685 first = false;
3686 false // the declaring class itself: always register
3687 } else {
3688 pool.get_if_materialized(c)
3689 .map(|pc| {
3690 pc.static_fields
3691 .iter()
3692 .chain(pc.instance_fields.iter())
3693 .any(|f| f.name.as_ref() == fname && f.desc.as_ref() == fdesc)
3694 })
3695 .unwrap_or(false)
3696 };
3697 if redeclares {
3698 continue; // hidden below this point: its subtree refs bind to its own field
3699 }
3700 out.entry(std::sync::Arc::from(c))
3701 .or_default()
3702 .push(jdc_core::rename::FieldRename {
3703 name: std::sync::Arc::from(fname),
3704 desc: std::sync::Arc::from(fdesc),
3705 display: std::sync::Arc::from(display),
3706 });
3707 if let Some(children) = subs.get(c) {
3708 for ch in children {
3709 if seen.insert(*ch) {
3710 queue.push(*ch);
3711 }
3712 }
3713 }
3714 }
3715}
3716
3717fn inherited_obscuring_renames(
3718 pool: &DexPool,
3719 out: &mut HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>>,
3720 pkg_displays: &jdc_core::FxHashMap<&str, jdc_core::FxHashSet<String>>,
3721) {
3722 let root_segs = pool.root_pkg_segs();
3723 // Reverse super map over materialized classes.
3724 let mut subs: jdc_core::FxHashMap<&str, Vec<&str>> = jdc_core::FxHashMap::default();
3725 for n in &pool.order {
3726 let Some(pc) = pool.get_if_materialized(n) else {
3727 continue;
3728 };
3729 if let Some(sup) = &pc.super_name {
3730 if sup != "java/lang/Object" {
3731 subs.entry(sup.as_str()).or_default().push(n.as_str());
3732 }
3733 }
3734 }
3735 let mut renamed: jdc_core::FxHashSet<(String, String, String)> =
3736 jdc_core::FxHashSet::default();
3737 for n in &pool.order {
3738 let Some(pc) = pool.get_if_materialized(n) else {
3739 continue;
3740 };
3741 if pc.is_interface() {
3742 continue;
3743 }
3744 // Member-rendered child displays of THIS class.
3745 let mut child_displays: Vec<String> = Vec::new();
3746 for child in pool.children_of(n.as_str()) {
3747 let Some(rest) = child
3748 .strip_prefix(n.as_str())
3749 .and_then(|t| t.strip_prefix('$'))
3750 else {
3751 continue;
3752 };
3753 if !clean_member_tail(rest) {
3754 continue;
3755 }
3756 if crate::find_outer_name(pool, child).as_deref() != Some(n.as_str()) {
3757 continue;
3758 }
3759 let renamed_disp = crate::apply_class_rename(child);
3760 let last = renamed_disp
3761 .rsplit('/')
3762 .next()
3763 .unwrap_or(&renamed_disp)
3764 .rsplit('$')
3765 .next()
3766 .unwrap_or(rest)
3767 .to_string();
3768 child_displays.push(last);
3769 }
3770 if child_displays.is_empty() {
3771 continue;
3772 }
3773 // Walk the super chain for an inherited field whose display
3774 // collides; own fields are the same-class rule's job.
3775 let own: jdc_core::FxHashSet<String> = pc
3776 .static_fields
3777 .iter()
3778 .chain(pc.instance_fields.iter())
3779 .map(|f| crate::classdec::java_ident(&f.name).into_owned())
3780 .collect();
3781 let mut sup = pc.super_name.clone();
3782 while let Some(sname) = sup {
3783 if sname == "java/lang/Object" {
3784 break;
3785 }
3786 let Some(sc) = pool.get_if_materialized(&sname) else {
3787 break;
3788 };
3789 for f in sc.static_fields.iter().chain(sc.instance_fields.iter()) {
3790 let fdisp = crate::classdec::java_ident(&f.name).into_owned();
3791 if fdisp.starts_with("this$") || !child_displays.contains(&fdisp) {
3792 continue;
3793 }
3794 if !renamed.insert((sname.clone(), f.name.to_string(), f.desc.to_string())) {
3795 continue;
3796 }
3797 // Mint in the DECLARING class's namespace: avoid its
3798 // fields + its own member-rendered child displays.
3799 let mut avoid: jdc_core::FxHashSet<String> = sc
3800 .static_fields
3801 .iter()
3802 .chain(sc.instance_fields.iter())
3803 .map(|g| crate::classdec::java_ident(&g.name).into_owned())
3804 .collect();
3805 // The declaring class's PACKAGE class displays (post-
3806 // rename): minting onto one shadows its qualified refs
3807 // from inside the class (the a→a2-onto-display-a2 bug).
3808 if let Some(names) = pkg_displays
3809 .get(sname.rfind('/').map(|i| &sname[..i]).unwrap_or(""))
3810 {
3811 avoid.extend(names.iter().cloned());
3812 }
3813 for child in pool.children_of(sname.as_str()) {
3814 let Some(rest) = child
3815 .strip_prefix(sname.as_str())
3816 .and_then(|t| t.strip_prefix('$'))
3817 else {
3818 continue;
3819 };
3820 if !clean_member_tail(rest) {
3821 continue;
3822 }
3823 let rd = crate::apply_class_rename(child);
3824 avoid.insert(
3825 rd.rsplit('/')
3826 .next()
3827 .unwrap_or(&rd)
3828 .rsplit('$')
3829 .next()
3830 .unwrap_or(rest)
3831 .to_string(),
3832 );
3833 }
3834 let mut k = 1u32;
3835 let display = loop {
3836 k += 1;
3837 let cand = format!("{fdisp}{k}");
3838 // root_segs: see the own-field mint site — a
3839 // package first segment must never be minted.
3840 if !avoid.contains(&cand) && !root_segs.contains(&cand) {
3841 break cand;
3842 }
3843 };
3844 register_field_rename_with_subs(
3845 pool, &subs, out, &sname, &f.name, &f.desc, &display,
3846 );
3847 if std::env::var("DDC_OBSCURE_STATS").is_ok() {
3848 eprintln!("[obscure-inh] {sname} field {} -> {display} (child of {n})", f.name);
3849 }
3850 }
3851 sup = sc.super_name.clone();
3852 }
3853 let _ = &own;
3854 }
3855}
3856
3857fn suffix_unique(base: &str, taken: &mut jdc_core::FxHashSet<String>) -> String {
3858 let mut k = 1u32;
3859 loop {
3860 k += 1;
3861 let cand = format!("{base}{k}");
3862 if taken.insert(cand.clone()) {
3863 return cand;
3864 }
3865 }
3866}
3867
3868/// member_collision_renames + field_deshadow_renames, collision-registry
3869/// entries winning per (owner, name, desc).
3870fn combined_field_renames(
3871 pool: &DexPool,
3872) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
3873 let t = std::time::Instant::now();
3874 let mut base = member_collision_renames(pool);
3875 if std::env::var("DDC_DBG_REN").is_ok() {
3876 eprintln!("[ren] {:?} member_collision_renames", t.elapsed());
3877 }
3878 let t = std::time::Instant::now();
3879 crate::stats_rss(" member_collision_renames");
3880 let extra = field_deshadow_renames(pool);
3881 if std::env::var("DDC_DBG_REN").is_ok() {
3882 eprintln!("[ren] {:?} field_deshadow_renames", t.elapsed());
3883 }
3884 let mut merged = 0usize;
3885 for (owner, v) in extra {
3886 let e = base.entry(owner).or_default();
3887 for fr in v {
3888 if !e.iter().any(|x| x.name == fr.name && x.desc == fr.desc) {
3889 e.push(fr);
3890 merged += 1;
3891 }
3892 }
3893 }
3894 if std::env::var("DDC_STATS").is_ok() {
3895 eprintln!("[renames] field-deshadow renames: {merged}");
3896 }
3897 crate::stats_rss(" field_deshadow merged");
3898 base
3899}
3900
3901/// Field deshadow (JLS 6.4.2, expression position): a FIELD whose display
3902/// name equals a root package first segment captures package-qualified
3903/// renders inside its family's file — androidx z2's field `j` binds the
3904/// `j` of `j.a.a` (找不到符号 变量 a). Rename through the field registry
3905/// (declaration and every reference funnel through field_display).
3906/// Gate: only when the family actually references that segment
3907/// (family_ref_segments body scan) or the segment is the family's OWN
3908/// package root (same-package refs fall back to `root.x` FQN renders) —
3909/// ungated field renames are the six-times-falsified blast radius.
3910fn field_deshadow_renames(
3911 pool: &DexPool,
3912) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
3913 let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
3914 HashMap::default();
3915 let root_segs = pool.root_pkg_segs();
3916 if root_segs.is_empty() {
3917 return out;
3918 }
3919 // Candidate fields per family (display, original name, desc).
3920 let mut cand: HashMap<
3921 String,
3922 Vec<(String, String, String, u32)>,
3923 > = HashMap::default();
3924 for name in &pool.order {
3925 let Some(pc) = pool.get_if_materialized(name) else {
3926 continue;
3927 };
3928 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
3929 let disp = crate::classdec::java_ident(&f.name).into_owned();
3930 if root_segs.contains(&disp) {
3931 cand
3932 .entry(name.clone())
3933 .or_default()
3934 .push((disp, f.name.to_string(), f.desc.to_string(), f.access));
3935 }
3936 }
3937 }
3938 if cand.is_empty() {
3939 return out;
3940 }
3941 let cand_set: jdc_core::FxHashSet<String> = cand.keys().cloned().collect();
3942 let t0 = std::time::Instant::now();
3943 let (fam_segs, fam_nest) =
3944 crate::refscan::family_ref_segments(&pool.dexes, &cand_set);
3945 if std::env::var("DDC_STATS").is_ok() {
3946 eprintln!(
3947 "[renames] field-deshadow scan: cands={} scan={:?}",
3948 cand_set.len(),
3949 t0.elapsed()
3950 );
3951 }
3952 for (fam, fields) in cand {
3953 let own_root = fam.find('/').map(|i| fam[..i].to_string());
3954 let scanned = fam_segs.get(&fam);
3955 // NO nested-tail collision trigger here: a private field named
3956 // like a nested class does capture `Fam.b.member` in expression
3957 // position (lark LKEvent static-ctx family, ~1k lines), but
3958 // renaming it is corpus-toxic even gated private+body-
3959 // referenced — lark +32k (43,643 renames), weixin/weibo −200.
3960 // Eighth falsification of loose rename gates: the resolution
3961 // perturbation surface of mass field renames dwarfs the target
3962 // family. The fam_nest scan stays wired for a future
3963 // render-side fix (the capture is only unfixable INSIDE the
3964 // family file; foreign files could import the nested type).
3965 let _ = &fam_nest;
3966 let hits: Vec<(String, String, String)> = fields
3967 .into_iter()
3968 .filter(|(disp, _, _, _access)| {
3969 scanned.is_some_and(|s| s.contains(disp))
3970 || own_root.as_deref() == Some(disp.as_str())
3971 })
3972 .map(|(d, n, de, _)| (d, n, de))
3973 .collect();
3974 if hits.is_empty() {
3975 continue;
3976 }
3977 // Names already taken in the family's member scope: field and
3978 // method displays plus direct nested-class tails (member types
3979 // share the lexical scope of the rendered file). Built only for
3980 // hit families — the per-family setup must stay off the 175k-
3981 // candidate loop (an O(pool.order) nested scan there cost 60s on
3982 // weixin).
3983 let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3984 if let Some(pc) = pool.get_if_materialized(&fam) {
3985 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
3986 taken.insert(crate::classdec::java_ident(&f.name).into_owned());
3987 }
3988 for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
3989 taken.insert(crate::classdec::java_ident(&m.name).into_owned());
3990 }
3991 }
3992 let mut nested_tails: jdc_core::FxHashSet<String> =
3993 jdc_core::FxHashSet::default();
3994 for child in pool.children_of(&fam) {
3995 if let Some(tail) = child.rsplit('$').next() {
3996 if !tail.is_empty() {
3997 taken.insert(tail.to_string());
3998 nested_tails.insert(tail.to_string());
3999 }
4000 }
4001 }
4002 for (disp, name, desc) in hits {
4003 let mut k = 0u32;
4004 let new_disp = loop {
4005 k += 1;
4006 let c = if k == 1 {
4007 format!("{disp}x")
4008 } else {
4009 format!("{disp}x{k}")
4010 };
4011 if !taken.contains(&c) && !root_segs.contains(&c) {
4012 break c;
4013 }
4014 };
4015 taken.insert(new_disp.clone());
4016 out.entry(std::sync::Arc::from(fam.as_str()))
4017 .or_default()
4018 .push(jdc_core::rename::FieldRename {
4019 name: std::sync::Arc::from(name.as_str()),
4020 desc: std::sync::Arc::from(desc.as_str()),
4021 display: std::sync::Arc::from(new_disp.as_str()),
4022 });
4023 }
4024 }
4025 out
4026}
4027pub use jdc_core::rename::apply_class_rename;
4028
4029// ---------------------------------------------------------------------------
4030// Default-package relocation.
4031// ---------------------------------------------------------------------------
4032
4033/// The synthetic package the default-package cohort currently relocates
4034/// to (Mutex, not OnceLock: `set_class_renames` replaces the registry per
4035/// pool, so a second pool in one process must be able to re-key this).
4036static ROOT_PKG: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
4037
4038/// The synthetic package display root-package classes were moved under
4039/// (`None` when no relocation ran). Consumers keying per-package caches
4040/// by the ORIGINAL package map this display back to `""`.
4041pub fn root_pkg_display() -> Option<String> {
4042 ROOT_PKG
4043 .lock()
4044 .unwrap_or_else(|e| e.into_inner())
4045 .clone()
4046}
4047
4048/// Default-package relocation: javac cannot resolve ANY reference from a
4049/// named package to an unnamed-package class (JLS 7.4.2 — `private it0 f;`
4050/// inside com.amap.X is 找不到符号 whatever the syntax; xingye ~198k +
4051/// Amap ~81k errors ≈ 73% of the whole-directory sweep total, and
4052/// kotlin.jvm.Function0 even EXTENDS a root class on xingye). No source
4053/// syntax escapes it — but the rename registry escapes the NAME: move the
4054/// whole root cohort under a synthetic package (`ddcroot/a.java`,
4055/// `package ddcroot;`) and every cross-package ref renders the resolvable
4056/// FQN `ddcroot.a`. Cohort-uniform: in-cohort refs keep resolving (one
4057/// new shared package), the case/lossy dedupes inside the cohort still
4058/// hold (a directory move preserves relative collisions), and the
4059/// synthetic segment is collision-bumped against every pool package and
4060/// class so it can never shadow or be shadowed. Runs LAST (prefixes the
4061/// settled displays). Fidelity note: declared package no longer matches
4062/// the binary name — the same trade every rename rule here makes, on a
4063/// cohort whose cross-package render is otherwise dead code.
4064/// Pool classes that shadow a FRAMEWORK class (d8 desugar stubs at the
4065/// framework FQN). The FILE identity relocates to the synthetic package,
4066/// but every REFERENCE renders the raw framework name — the bytecode's
4067/// FQN means the platform class (the stub is dead code the boot
4068/// classpath outranks).
4069pub(crate) static FW_SHADOW: std::sync::OnceLock<
4070 std::sync::Arc<jdc_core::FxHashSet<String>>,
4071> = std::sync::OnceLock::new();
4072
4073/// True when `internal` is a framework-shadow stub (fast empty-set
4074/// check when no corpus stubs exist).
4075pub fn is_fw_shadow(internal: &str) -> bool {
4076 let Some(s) = FW_SHADOW.get() else { return false };
4077 // Exact hit, or any $-chain PREFIX hits: a nested FRAMEWORK member
4078 // of a stub outer (`ImageDecoder$Source` with the stub
4079 // `ImageDecoder` in the pool) renders through the $-walk of the
4080 // stub's map entry — it must keep the raw framework name too.
4081 if s.contains(internal) {
4082 return true;
4083 }
4084 let mut idx = internal.len();
4085 while let Some(i) = internal[..idx].rfind('$') {
4086 if s.contains(&internal[..i]) {
4087 return true;
4088 }
4089 idx = i;
4090 }
4091 false
4092}
4093
4094/// d8's library/API desugaring embeds STUB classes under the framework's
4095/// own names (`android.graphics.Insets`, `android.app.Person` — minimal
4096/// shapes the runtime never loads, the boot classpath wins). Emitted at
4097/// their FQN they SHADOW the real framework class for every OTHER file
4098/// at compile time: javac resolves `android.graphics.Insets` to the
4099/// field-less stub and `insets.top` dies (reqable r2: the whole Insets
4100/// family). Relocate every pool class whose head resolves in the
4101/// embedded framework DB to a synthetic package; the rename registry
4102/// carries every reference.
4103fn fw_shadow_relocation(pool: &DexPool, map: &mut HashMap<String, String>) {
4104 let root_segs = pool.root_pkg_segs();
4105 // ANDROID-namespace only: a framework-shadow stub in another
4106 // namespace is usually a REAL bundled implementation (weibo ships
4107 // its own full org.json — relocating it broke every org.json user,
4108 // +86). d8's API-desugar stubs live under android/.
4109 let shadow_set: std::sync::Arc<jdc_core::FxHashSet<String>> =
4110 std::sync::Arc::new(
4111 pool.order
4112 .iter()
4113 .filter(|n| n.starts_with("android/"))
4114 .filter(|n| {
4115 let head = n.split('$').next().unwrap_or(n);
4116 crate::fwdb::find(head).is_some()
4117 })
4118 .map(|n| n.to_string())
4119 .collect(),
4120 );
4121 let _ = FW_SHADOW.set(shadow_set.clone());
4122 jdc_core::rename::set_fw_shadow(
4123 shadow_set.iter().cloned().collect(),
4124 );
4125 let mut pkg = "ddcfwstub".to_string();
4126 while pool.has_name(&pkg) || root_segs.contains(&pkg) {
4127 pkg.push('_');
4128 }
4129 let mut moved = 0usize;
4130 for name in &pool.order {
4131 // The relocation decision keys on the $-chain HEAD (a nested
4132 // stub rides its outer's identity); nested-of-pool-outer shapes
4133 // stay entry-less so apply_class_rename's $-walk composes them
4134 // through the relocated outer (the VasUIToken exact-entry
4135 // lesson from root_pkg_relocation). Android-namespace only —
4136 // see the set build above.
4137 if !name.starts_with("android/") {
4138 continue;
4139 }
4140 let head = name.split('$').next().unwrap_or(name);
4141 if crate::fwdb::find(head).is_none() {
4142 continue;
4143 }
4144 if head != name && pool.has_name(head) {
4145 continue;
4146 }
4147 let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
4148 if disp != *name {
4149 // An earlier rule renamed this class (obscuring/collision
4150 // family) — its display already diverges from the FQN and
4151 // the relocation prefix applies to the settled display.
4152 map.insert(name.clone(), format!("{pkg}/{disp}"));
4153 } else {
4154 map.insert(name.clone(), format!("{pkg}/{name}"));
4155 }
4156 moved += 1;
4157 }
4158 if std::env::var("DDC_STATS").is_ok() {
4159 eprintln!("[renames] fw-shadow relocation: {moved} classes -> {pkg}");
4160 }
4161}
4162
4163fn root_pkg_relocation(pool: &DexPool, map: &mut HashMap<String, String>) {
4164 // Gate on NAMES only (identical verdict in lazy browse and full
4165 // decompile — the display must not depend on materialization):
4166 // relocation matters only when BOTH cohorts exist — the JLS-dead
4167 // direction is named-package → root-package, so an all-root app
4168 // (hello fixtures, pure-demo dexes) keeps the faithful layout.
4169 let has_root = pool.order.iter().any(|n| !n.contains('/'));
4170 let has_named = pool.order.iter().any(|n| n.contains('/'));
4171 if !has_root || !has_named {
4172 *ROOT_PKG.lock().unwrap_or_else(|e| e.into_inner()) = None;
4173 return;
4174 }
4175 let root_segs = pool.root_pkg_segs();
4176 let mut pkg = "ddcroot".to_string();
4177 while pool.has_name(&pkg) || root_segs.contains(&pkg) {
4178 pkg.push('_');
4179 }
4180 // A root class whose simple name IS a top-level package segment
4181 // (`com`, `org`, ...) cannot simply MOVE into the synthetic
4182 // package: a same-package type shadows the package name in every
4183 // qualified ref from every cohort file (JLS 6.4.1 — rimet's root
4184 // `com`/`org` hijacked 235k `com.alibaba...` refs as 类 alibaba
4185 // 位置: 类 com). Those get a bumped first segment (`com` →
4186 // ddcroot/com2) — a display rename the registry carries through
4187 // every ref path, like the obscuring renames.
4188 const FW_SEGS: [&str; 10] = [
4189 "java", "javax", "android", "org", "junit", "dalvik", "libcore", "androidx", "kotlin",
4190 "kotlinx",
4191 ];
4192 let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4193 for name in &pool.order {
4194 if name.contains('/') {
4195 continue;
4196 }
4197 let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
4198 taken.insert(format!("{pkg}/{disp}"));
4199 }
4200 let mut moved = 0usize;
4201 let mut bumped = 0usize;
4202 for name in &pool.order {
4203 if name.contains('/') {
4204 continue;
4205 }
4206 let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
4207 if disp.contains('/') {
4208 continue; // defensive: some rule already packaged it
4209 }
4210 if disp == *name && name.contains('$') && find_outer_name(pool, name).is_some() {
4211 // An UNRENAMED nested name with a pool outer must stay
4212 // entry-less so apply_class_rename's $-walk composes it
4213 // through the outer's relocated+renamed display — an exact
4214 // entry short-circuits the walk and freezes the outer's
4215 // pre-rename simple (xingye's case-collision quads:
4216 // `ue5$a` exact-mapped to ddcroot/ue5$a instead of
4217 // ddcroot/ue5_4$a — 程序包ddcroot.ue5不存在 ×61k).
4218 // The case-collision machinery pre-inserts an IDENTITY
4219 // entry for every emission-set name, which short-circuits
4220 // the same walk and froze `$`-tails in the ROOT package
4221 // while their outer moved to ddcroot — family split: QQ's
4222 // VasUIToken$1..$1xx husks stayed at tree root, and every
4223 // `VasUIToken$N.INSTANCE` ref from ddcroot/VasUIToken.java
4224 // died (找不到符号 变量 VasUIToken$N ×202 in that one file).
4225 // Remove the pin; the walk composes through the outer.
4226 map.remove(name);
4227 continue;
4228 }
4229 let head = disp.split('$').next().unwrap_or(&disp);
4230 let collide = root_segs.contains(head) || FW_SEGS.contains(&head);
4231 if !collide {
4232 map.insert(name.clone(), format!("{pkg}/{disp}"));
4233 moved += 1;
4234 continue;
4235 }
4236 let rest = &disp[head.len()..];
4237 let mut k = 1u32;
4238 loop {
4239 k += 1;
4240 let nh = format!("{head}{k}");
4241 if root_segs.contains(&nh) || FW_SEGS.contains(&nh.as_str()) {
4242 continue;
4243 }
4244 let cand = format!("{pkg}/{nh}{rest}");
4245 if taken.contains(&cand) {
4246 continue;
4247 }
4248 taken.insert(cand.clone());
4249 map.insert(name.clone(), cand);
4250 moved += 1;
4251 bumped += 1;
4252 break;
4253 }
4254 }
4255 *ROOT_PKG.lock().unwrap_or_else(|e| e.into_inner()) = Some(pkg.clone());
4256 if std::env::var("DDC_STATS").is_ok() {
4257 eprintln!("[renames] root-pkg relocation: {moved} classes -> {pkg}/ ({bumped} segment-bumped)");
4258 }
4259}