Skip to main content

ddc_dec/
lib.rs

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