Skip to main content

ddc_dec/
lib.rs

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