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