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