Skip to main content

ddc_dec/
lib.rs

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