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