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