Skip to main content

ddc_dec/
lib.rs

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