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