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