Skip to main content

ddc_dec/
lib.rs

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