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::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) — enum constants.
34    Field(String, String),
35    Other,
36}
37use ddc_dex::{ClassDef, DexFile};
38use jdc_core::types::{
39    parse_field_descriptor, parse_method_descriptor, JavaType, MethodDescriptor,
40};
41
42// ---------------------------------------------------------------------------
43// Access flags (Dalvik numbering, JVM-compatible subset).
44// ---------------------------------------------------------------------------
45pub mod access {
46    pub const ACC_PUBLIC: u32 = 0x1;
47    pub const ACC_PRIVATE: u32 = 0x2;
48    pub const ACC_PROTECTED: u32 = 0x4;
49    pub const ACC_STATIC: u32 = 0x8;
50    pub const ACC_FINAL: u32 = 0x10;
51    pub const ACC_SYNCHRONIZED: u32 = 0x20;
52    pub const ACC_BRIDGE: u32 = 0x40;
53    pub const ACC_VARARGS: u32 = 0x80;
54    pub const ACC_NATIVE: u32 = 0x100;
55    pub const ACC_INTERFACE: u32 = 0x200;
56    pub const ACC_ABSTRACT: u32 = 0x400;
57    pub const ACC_STRICT: u32 = 0x800;
58    pub const ACC_SYNTHETIC: u32 = 0x1000;
59    pub const ACC_ANNOTATION: u32 = 0x2000;
60    pub const ACC_ENUM: u32 = 0x4000;
61    pub const ACC_CONSTRUCTOR: u32 = 0x1_0000;
62    pub const ACC_DECLARED_SYNCHRONIZED: u32 = 0x2_0000;
63}
64
65/// One field of a pooled class.
66#[derive(Debug, Clone)]
67pub struct PoolField {
68    pub name: String,
69    /// Field descriptor (`I`, `Ljava/lang/String;`, ...).
70    pub desc: String,
71    pub access: u32,
72    pub is_static: bool,
73}
74
75/// One method of a pooled class.
76#[derive(Debug, Clone)]
77pub struct PoolMethod {
78    /// Shared with the dex string table (the idx IS the dedup key).
79    pub name: std::sync::Arc<str>,
80    /// Method descriptor, shared per proto (a dex's proto table is the
81    /// descriptor dedup layer — 1.9M methods on lark share ~1 proto
82    /// table's worth of unique descriptors).
83    pub desc: std::sync::Arc<str>,
84    pub access: u32,
85    pub code_off: u32,
86    pub debug_info_off: u32,
87    /// Which DEX image the body lives in.
88    pub dex_idx: usize,
89}
90
91impl PoolMethod {
92    pub fn parsed_desc(&self) -> Option<MethodDescriptor> {
93        parse_method_descriptor(&self.desc)
94    }
95    pub fn is_static(&self) -> bool {
96        self.access & access::ACC_STATIC != 0
97    }
98    pub fn is_abstract_or_native(&self) -> bool {
99        self.access & (access::ACC_ABSTRACT | access::ACC_NATIVE) != 0
100    }
101}
102
103/// One pool entry: either already built, or name-registered with its
104/// (dex index, class_def index) locator for on-demand construction.
105enum ClassEntry {
106    Eager(PoolClass),
107    Lazy {
108        at: (usize, usize),
109        pc: std::sync::OnceLock<PoolClass>,
110    },
111}
112
113/// A class materialized from one `class_def_item` (with ids resolved to
114/// names and the class data expanded).
115#[derive(Debug, Clone)]
116pub struct PoolClass {
117    pub name: String,
118    pub access: u32,
119    /// `None` for `java/lang/Object` roots.
120    pub super_name: Option<String>,
121    pub interfaces: Vec<String>,
122    pub source_file: Option<String>,
123    pub static_fields: Vec<PoolField>,
124    pub instance_fields: Vec<PoolField>,
125    pub direct_methods: Vec<PoolMethod>,
126    pub virtual_methods: Vec<PoolMethod>,
127    /// Static initial values aligned with the head of `static_fields`.
128    pub static_values: Vec<StaticValue>,
129    /// Nesting evidence with annotation ids resolved to names.
130    pub nesting: ResolvedNesting,
131    /// Which DEX image the class body lives in (provenance header).
132    pub dex_idx: usize,
133}
134
135/// Nesting evidence, names resolved at pooling time.
136#[derive(Debug, Clone, Default)]
137pub struct ResolvedNesting {
138    pub enclosing_class: Option<String>,
139    /// (class internal name, method name) of the enclosing method.
140    pub enclosing_method: Option<(String, String)>,
141    pub member_classes: Vec<String>,
142}
143
144impl PoolClass {
145    pub fn is_interface(&self) -> bool {
146        self.access & access::ACC_INTERFACE != 0
147    }
148
149    pub fn is_enum(&self) -> bool {
150        self.access & access::ACC_ENUM != 0
151    }
152
153    pub fn is_synthetic(&self) -> bool {
154        self.access & access::ACC_SYNTHETIC != 0
155    }
156
157    pub fn is_static_nested(&self) -> bool {
158        self.access & access::ACC_STATIC != 0
159    }
160
161    /// All methods in declaration order.
162    pub fn all_methods(&self) -> impl Iterator<Item = &PoolMethod> {
163        self.direct_methods
164            .iter()
165            .chain(self.virtual_methods.iter())
166    }
167
168    /// Find a method by name + descriptor.
169    pub fn find_method(&self, name: &str, desc: &str) -> Option<&PoolMethod> {
170        self.all_methods()
171            .find(|m| &*m.name == name && &*m.desc == desc)
172    }
173
174    /// Constructors `<init>` matching `arity` descriptor arguments.
175    pub fn ctors_by_arity(&self, arity: usize) -> Vec<&PoolMethod> {
176        self.all_methods()
177            .filter(|m| &*m.name == "<init>")
178            .filter(|m| {
179                m.parsed_desc()
180                    .map(|d| d.args.len() == arity)
181                    .unwrap_or(false)
182            })
183            .collect()
184    }
185
186    pub fn field_flags_of(&self, name: &str) -> Option<u32> {
187        self.static_fields
188            .iter()
189            .chain(self.instance_fields.iter())
190            .find(|f| f.name == name)
191            .map(|f| f.access)
192    }
193}
194
195/// Multi-DEX class pool: classes from all images, first definition wins.
196pub struct DexPool {
197    /// Images as Arcs: `dex()` hands out snapshots whose borrows stay
198    /// valid for the Arc's lifetime (decompile workers hold them), while
199    /// `release_images` empties the image data through exclusive access
200    /// when all of an image's classes are done (full-decompile driver
201    /// only — progressive callers keep images whole).
202    dexes: Vec<std::sync::Arc<DexFile>>,
203    /// Human label per image ("weibo!classes.dex") for the provenance
204    /// header; defaults to "dex N" until the CLI names the inputs.
205    pub dex_labels: Vec<String>,
206    /// Name → class entry. Eager pools materialize everything at add
207    /// time (full decompile, tests); lazy pools register names only and
208    /// materialize on first `get` (progressive `getclass` — one class's
209    /// PoolClass costs annotation reads, 145k of them cost ~0.2s).
210    classes: HashMap<String, ClassEntry>,
211    /// Class names in insertion order (stable for output).
212    pub order: Vec<String>,
213    /// Per-image remaining class count once `arm_retirement` fires.
214    retire_counts: std::sync::Mutex<Vec<u64>>,
215    retire_armed: std::sync::atomic::AtomicBool,
216    /// name → outer (computed once; `$` heuristic + dalvik annotations).
217    outers: std::sync::OnceLock<HashMap<String, Option<String>>>,
218    /// outer → direct children (computed once).
219    children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
220    /// Per-image hot-reference interning (parallel to `dexes`).
221    ref_caches: Vec<DexRefCache>,
222    /// Synthetic-static accessor code snapshots keyed by (dex_idx,
223    /// code_off). `inline_accessors` reads accessor bodies ACROSS images
224    /// during worker runs; image retirement can release those bytes
225    /// mid-run, making the inline decision — and with it var numbering
226    /// downstream — dependent on worker completion interleaving
227    /// (observed: reqable enum-constant args flipping `var0`/`var1`
228    /// between identical runs; DDC_NORETIRE=1 stable). The full-decompile
229    /// driver fills this BEFORE `arm_retirement`; progressive/lazy pools
230    /// never retire and fall through to live reads.
231    accessor_code: std::sync::OnceLock<AccessorSnapshots>,
232}
233
234/// Raw code_item bytes of every synthetic-static accessor, keyed by
235/// (dex_idx, code_off) — see `snapshot_accessor_code`.
236type AccessorSnapshots = HashMap<(usize, u32), std::sync::Arc<[u8]>>;
237
238/// Lazily-filled, thread-shared reference interning for one image.
239///
240/// The lifter used to rebuild class names, field/method names and WHOLE
241/// method descriptors per INSTRUCTION (`method_ref` alone: N param
242/// Strings → `format!` descriptor → re-parse — 6-10 allocations per
243/// invoke). Every entry here is built at most once per (image, table
244/// index); lifts and the endless IR clones downstream are refcount
245/// bumps. Slots are `OnceLock` so worker threads fill them race-free
246/// without a lock on the hot path.
247pub(crate) struct DexRefCache {
248    proto_descs: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::MethodDescriptor>>]>,
249    type_names: Box<[std::sync::OnceLock<std::sync::Arc<str>>]>,
250    type_javas: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::JavaType>>]>,
251}
252
253impl DexRefCache {
254    fn new(protos: usize, types: usize) -> Self {
255        DexRefCache {
256            proto_descs: (0..protos).map(|_| std::sync::OnceLock::new()).collect(),
257            type_names: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
258            type_javas: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
259        }
260    }
261}
262
263impl DexPool {
264    pub fn new() -> Self {
265        DexPool {
266            dexes: Vec::new(),
267            retire_armed: std::sync::atomic::AtomicBool::new(false),
268            dex_labels: Vec::new(),
269            classes: HashMap::default(),
270            order: Vec::new(),
271            retire_counts: std::sync::Mutex::new(Vec::new()),
272            outers: std::sync::OnceLock::new(),
273            children: std::sync::OnceLock::new(),
274            ref_caches: Vec::new(),
275            accessor_code: std::sync::OnceLock::new(),
276        }
277    }
278
279    /// Shared `MethodDescriptor` of a proto (one parse per image).
280    pub fn proto_desc_parsed(
281        &self,
282        di: usize,
283        proto_idx: u32,
284    ) -> std::sync::Arc<jdc_core::types::MethodDescriptor> {
285        let Some(dex) = self.dexes.get(di) else {
286            return std::sync::Arc::new(jdc_core::types::MethodDescriptor {
287                args: Vec::new(),
288                ret: jdc_core::types::JavaType::Void,
289            });
290        };
291        let build = || {
292            std::sync::Arc::new(parse_proto_desc(dex, proto_idx))
293        };
294        match self
295            .ref_caches
296            .get(di)
297            .and_then(|c| c.proto_descs.get(proto_idx as usize))
298        {
299            Some(slot) => slot.get_or_init(build).clone(),
300            None => build(),
301        }
302    }
303
304    /// Shared internal class name of a type id (one strip+alloc per image).
305    pub fn type_name_arc(&self, di: usize, type_idx: u32) -> std::sync::Arc<str> {
306        let Some(dex) = self.dexes.get(di) else {
307            return std::sync::Arc::from("");
308        };
309        let build = || std::sync::Arc::from(dex.class_name(type_idx).as_str());
310        match self
311            .ref_caches
312            .get(di)
313            .and_then(|c| c.type_names.get(type_idx as usize))
314        {
315            Some(slot) => slot.get_or_init(build).clone(),
316            None => build(),
317        }
318    }
319
320    /// Shared parsed type of a type id.
321    pub fn type_java(&self, di: usize, type_idx: u32) -> std::sync::Arc<jdc_core::types::JavaType> {
322        let Some(dex) = self.dexes.get(di) else {
323            return std::sync::Arc::new(jdc_core::types::JavaType::Object(
324                std::sync::Arc::from("java/lang/Object"),
325            ));
326        };
327        let build = || std::sync::Arc::new(desc_type(dex.type_name(type_idx)));
328        match self
329            .ref_caches
330            .get(di)
331            .and_then(|c| c.type_javas.get(type_idx as usize))
332        {
333            Some(slot) => slot.get_or_init(build).clone(),
334            None => build(),
335        }
336    }
337
338    /// name → outer map, computed lazily once.
339    fn outer_map(&self) -> &HashMap<String, Option<String>> {
340        self.outers.get_or_init(|| {
341            self.order
342                .iter()
343                .map(|n| (n.clone(), find_outer_name(self, n)))
344                .collect()
345        })
346    }
347
348    /// The outer class of `name`, from the cached map (borrowed).
349    pub fn outer_of(&self, name: &str) -> Option<&str> {
350        self.outer_map().get(name).and_then(|o| o.as_deref())
351    }
352
353    /// Direct nested children of `internal` (any `$` depth 1), cached.
354    /// (Perf: returns a borrowed slice — the owned-Vec clone ran once per
355    /// class on 98k-class runs.)
356    pub fn children_of(&self, internal: &str) -> &[String] {
357        if self.children.get().is_none() {
358            // Build FIRST, set ONCE: the previous shape set an empty map
359            // to claim the OnceLock and then silently failed to store the
360            // real index (`let _ = set(...)` on an initialized lock) —
361            // children_of returned [] forever, so member nested classes
362            // were neither inlined nor emitted as files (Guard$Report
363            // vanished whole; every corpus run since the borrow refactor
364            // dropped them, invisible to the syntax-only gate).
365            let mut idx: HashMap<String, Vec<String>> = HashMap::default();
366            for name in &self.order {
367                if let Some(outer) = self.outer_of(name).map(str::to_string) {
368                    idx.entry(outer).or_default().push(name.clone());
369                }
370            }
371            // A racing thread may have set an identical map first — the
372            // index is a pure function of `order`, so either copy is right.
373            let _ = self.children.set(idx);
374        }
375        self.children
376            .get()
377            .and_then(|m| m.get(internal))
378            .map(|v| v.as_slice())
379            .unwrap_or(&[])
380    }
381
382    /// Adds one image; duplicate class names keep their first definition.
383    /// Returns the image's index — call `set_dex_label` with it to give the
384    /// provenance header a real origin instead of the default "dex N".
385    pub fn add_dex(&mut self, dex: DexFile) -> usize {
386        let dex_idx = self.dexes.len();
387        // Pre-size the name map: 240k-entry growth rehashed repeatedly
388        // (reserve_rehash showed up in corpus profiles).
389        self.classes.reserve(dex.class_defs.len());
390        // The annotation reader borrows the image; pool classes are built
391        // before ownership moves into `self.dexes` (no full-image copy).
392        for cd in &dex.class_defs {
393            let name = dex.class_name(cd.class_idx);
394            if self.classes.contains_key(&name) {
395                continue;
396            }
397            let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
398            self.classes.insert(name.clone(), ClassEntry::Eager(pc));
399            self.order.push(name);
400        }
401        self.ref_caches
402            .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
403        self.dexes.push(std::sync::Arc::new(dex));
404        self.retire_counts.lock().unwrap().push(0);
405        self.dex_labels.push(format!("dex {}", dex_idx));
406        dex_idx
407    }
408
409    /// Registers class NAMES only; the PoolClass (annotation reads
410    /// included) is materialized on first `get`. Duplicate class names
411    /// keep their first definition, matching `add_dex`.
412    pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
413        let dex_idx = self.dexes.len();
414        self.classes.reserve(dex.class_defs.len());
415        for (ci, cd) in dex.class_defs.iter().enumerate() {
416            let name = dex.class_name(cd.class_idx);
417            if self.classes.contains_key(&name) {
418                continue;
419            }
420            self.classes.insert(
421                name.clone(),
422                ClassEntry::Lazy {
423                    at: (dex_idx, ci),
424                    pc: std::sync::OnceLock::new(),
425                },
426            );
427            self.order.push(name);
428        }
429        self.ref_caches
430            .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
431        self.dexes.push(std::sync::Arc::new(dex));
432        self.retire_counts.lock().unwrap().push(0);
433        self.dex_labels.push(format!("dex {}", dex_idx));
434        dex_idx
435    }
436
437    /// Label the image at `idx` (see `add_dex`).
438    pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
439        if let Some(slot) = self.dex_labels.get_mut(idx) {
440            *slot = label.into();
441        }
442    }
443
444    pub fn len(&self) -> usize {
445        self.classes.len()
446    }
447
448    pub fn is_empty(&self) -> bool {
449        self.classes.is_empty()
450    }
451
452    pub fn get(&self, internal: &str) -> Option<&PoolClass> {
453        self.get_inner(internal, None)
454    }
455
456    /// `get` with optional pre-taken raw-image snapshots (one per
457    /// image). `materialize_all` hoists the `DexFile::raw()` lock out of
458    /// the per-class loop — the parallel materialize used to take the
459    /// same per-image lock once PER CLASS across all threads.
460    fn get_inner(&self, internal: &str, raws: Option<&[&[u8]]>) -> Option<&PoolClass> {
461        match self.classes.get(internal)? {
462            ClassEntry::Eager(pc) => Some(pc),
463            ClassEntry::Lazy { at, pc } => {
464                if let Some(built) = pc.get() {
465                    return Some(built);
466                }
467                let (di, ci) = *at;
468                let dex = self.dexes.get(di)?;
469                let cd = dex.class_defs.get(ci)?;
470                let raw = match raws {
471                    Some(r) => r.get(di).copied().unwrap_or(&[]),
472                    None => dex.raw(),
473                };
474                let built = pool_class_of(dex, raw, cd, di);
475                Some(pc.get_or_init(move || built))
476            }
477        }
478    }
479
480    /// Materialize every lazy entry (parallel across the given chunk
481    /// count). After this the pool is observationally identical to an
482    /// eagerly-built one — including the annotation-aware outer-map —
483    /// while the annotation reads ran on worker threads instead of the
484    /// serial pool build (~0.2s on weibo).
485    pub fn materialize_all(&self, threads: usize) {
486        let keys: Vec<String> = self
487            .classes
488            .iter()
489            .filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
490            .map(|(k, _)| k.clone())
491            .collect();
492        if keys.is_empty() {
493            return;
494        }
495        // One raw() snapshot per image for the whole sweep (see
496        // get_inner). Safe window: materialize_all completes before
497        // retirement is armed, so no image releases while these slices
498        // are alive.
499        let raws: Vec<&[u8]> = self.dexes.iter().map(|d| d.raw()).collect();
500        let threads = threads.max(1);
501        let next = std::sync::atomic::AtomicUsize::new(0);
502        std::thread::scope(|scope| {
503            for _ in 0..threads {
504                scope.spawn(|| loop {
505                    let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
506                    let Some(k) = keys.get(i) else { break };
507                    let _ = self.get_inner(k, Some(&raws));
508                });
509            }
510        });
511    }
512
513    /// `get` without materializing: None for not-yet-built lazy entries.
514    fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
515        match self.classes.get(internal)? {
516            ClassEntry::Eager(pc) => Some(pc),
517            ClassEntry::Lazy { pc, .. } => pc.get(),
518        }
519    }
520
521    /// Name registered (no materialization).
522    pub fn has_name(&self, internal: &str) -> bool {
523        self.classes.contains_key(internal)
524    }
525
526    /// One image as an Arc snapshot (borrows of the snapshot live as
527    /// long as the caller holds the Arc).
528    pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
529        self.dexes.get(idx).cloned()
530    }
531
532    pub fn dex_count(&self) -> usize {
533        self.dexes.len()
534    }
535
536    pub fn class_names(&self) -> impl Iterator<Item = &str> {
537        self.order.iter().map(|s| s.as_str())
538    }
539
540    /// True when `sub` is assignable to `sup` (internal names), walking the
541    /// pool's hierarchy. Unknown classes are never assignable (conservative).
542    pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
543        if sub == sup {
544            return true;
545        }
546        // Arrays: covariance by element type when both are arrays.
547        if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
548            if sub_elem.starts_with('L')
549                && sup_elem.starts_with('L')
550                && sub_elem.ends_with(';')
551                && sup_elem.ends_with(';')
552            {
553                return self.is_subtype(
554                    &sub_elem[1..sub_elem.len() - 1],
555                    &sup_elem[1..sup_elem.len() - 1],
556                );
557            }
558            return sub_elem == sup_elem;
559        }
560        let mut cur = self.get(sub);
561        let mut hops = 0;
562        while let Some(c) = cur {
563            hops += 1;
564            if hops > 64 {
565                return false;
566            }
567            for i in &c.interfaces {
568                if i == sup || self.is_subtype(i, sup) {
569                    return true;
570                }
571            }
572            match &c.super_name {
573                Some(s) if s == sup => return true,
574                Some(s) if s != "java/lang/Object" => {
575                    cur = self.get(s);
576                }
577                _ => return false,
578            }
579        }
580        false
581    }
582}
583
584impl Default for DexPool {
585    fn default() -> Self {
586        Self::new()
587    }
588}
589
590fn array_elem(desc: &str) -> Option<&str> {
591    desc.strip_prefix('[')
592}
593
594fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
595    match v {
596        EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
597        EncodedValue::Short(x) => StaticValue::Int(*x as i64),
598        EncodedValue::Char(x) => StaticValue::Int(*x as i64),
599        EncodedValue::Int(x) => StaticValue::Int(*x as i64),
600        EncodedValue::Long(x) => StaticValue::Int(*x),
601        EncodedValue::Float(x) => StaticValue::Float(*x),
602        EncodedValue::Double(x) => StaticValue::Double(*x),
603        EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
604        EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
605        EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
606        EncodedValue::Null => StaticValue::Null,
607        EncodedValue::Enum(i) | EncodedValue::Field(i) => {
608            let f = dex.field(*i);
609            StaticValue::Field(
610                dex.class_name(f.class_idx),
611                dex.string(f.name_idx).to_string(),
612            )
613        }
614        _ => StaticValue::Other,
615    }
616}
617
618fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
619    let name = dex.class_name(cd.class_idx);
620    let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
621        None
622    } else {
623        let s = dex.class_name(cd.superclass_idx);
624        if s.is_empty() || s == "java/lang/Object" {
625            None
626        } else {
627            Some(s)
628        }
629    };
630    let interfaces = dex
631        .interfaces_of(cd)
632        .into_iter()
633        .map(|t| dex.class_name(t))
634        .collect();
635    let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
636        None
637    } else {
638        Some(dex.string(cd.source_file_idx).to_string())
639    };
640
641    let data = dex.class_data(cd);
642    let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
643        let f = dex.field(ef.field_idx);
644        PoolField {
645            name: dex.string(f.name_idx).to_string(),
646            desc: dex.type_name(f.type_idx).to_string(),
647            access: ef.access_flags,
648            is_static,
649        }
650    };
651    let static_fields: Vec<PoolField> = data
652        .static_fields
653        .iter()
654        .map(|f| mk_field(f, true))
655        .collect();
656    let instance_fields: Vec<PoolField> = data
657        .instance_fields
658        .iter()
659        .map(|f| mk_field(f, false))
660        .collect();
661    let mk_method = |em: &ddc_dex::EncodedMethod| {
662        let m = dex.method(em.method_idx);
663        let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
664            Some(d) => (em.code_off, d),
665            None => (0, 0),
666        };
667        PoolMethod {
668            name: dex.string_arc(m.name_idx),
669            desc: dex.proto_desc(m.proto_idx),
670            access: em.access_flags,
671            code_off,
672            debug_info_off,
673            dex_idx,
674        }
675    };
676    let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
677    let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
678
679    let static_values: Vec<StaticValue> = dex
680        .static_values(cd.static_values_off)
681        .into_iter()
682        .map(|v| resolve_static_value(&v, dex))
683        .collect();
684    let anns = if cd.annotations_off != 0 {
685        annotations::read_class_annotations(raw, cd.annotations_off)
686    } else {
687        Vec::new()
688    };
689    let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
690    let nesting = ResolvedNesting {
691        enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
692        enclosing_method: raw.enclosing_method.map(|m| {
693            let mid = dex.method(m);
694            (
695                dex.class_name(mid.class_idx),
696                dex.string(mid.name_idx).to_string(),
697            )
698        }),
699        member_classes: raw
700            .member_classes
701            .into_iter()
702            .map(|t| dex.class_name(t))
703            .collect(),
704    };
705
706    PoolClass {
707        name,
708        access: cd.access_flags,
709        super_name,
710        interfaces,
711        source_file,
712        static_fields,
713        instance_fields,
714        direct_methods,
715        virtual_methods,
716        static_values,
717        nesting,
718        dex_idx,
719    }
720}
721
722/// JavaType for a field/method descriptor segment.
723/// Parse a proto's descriptor into a MethodDescriptor straight from the
724/// tables (no string round-trip).
725fn parse_proto_desc(
726    dex: &ddc_dex::DexFile,
727    proto_idx: u32,
728) -> jdc_core::types::MethodDescriptor {
729    let proto = dex.proto(proto_idx);
730    let args = dex
731        .proto_params(proto_idx)
732        .iter()
733        .map(|&t| desc_type(dex.type_name(t)))
734        .collect();
735    jdc_core::types::MethodDescriptor {
736        args,
737        ret: desc_type(dex.type_name(proto.return_type_idx)),
738    }
739}
740
741pub fn desc_type(desc: &str) -> JavaType {
742    parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
743}
744
745/// Nesting evidence for `internal`: dalvik annotations first, then the
746/// `$`-name heuristic against the pool.
747pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
748    // Annotation refinement only for ALREADY-MATERIALIZED classes: eager
749    // pools (full decompile) behave exactly as before; lazy pools
750    // (progressive getclass) fall straight to the `$` chain without
751    // materializing the world.
752    if let Some(pc) = pool.get_if_materialized(internal) {
753        if let Some(enc) = &pc.nesting.enclosing_class {
754            return Some(enc.clone());
755        }
756    }
757    let mut rest = internal;
758    while let Some(d) = rest.rfind('$') {
759        let cand = &rest[..d];
760        if pool.has_name(cand) {
761            return Some(cand.to_string());
762        }
763        rest = cand;
764    }
765    None
766}
767
768/// True when the `$` tail names a plain member class (not anonymous/local/
769/// lambda), i.e. the class renders inside its outer's compilation unit.
770fn clean_member_tail(rest: &str) -> bool {
771    if rest.starts_with('-') {
772        return false;
773    }
774    let tail = rest.rsplit('$').next().unwrap_or(rest);
775    if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
776        return false;
777    }
778    true
779}
780
781/// Classes to emit as their own compilation units: top-level classes plus
782/// anonymous/local/lambda-shaped ones (clean members render inline).
783impl DexPool {
784    /// Arm image retirement (full-decompile driver only): count one
785    /// pending class per image by pool ownership.
786    /// Snapshot every synthetic-static accessor body (the exact predicate
787    /// `inline_accessors` uses) as RAW code_item bytes, so cross-image
788    /// accessor reads during worker runs survive image retirement.
789    /// Call AFTER materialization and BEFORE `arm_retirement`; idempotent
790    /// (first fill wins). Raw memcpy, no decode — accessors are numerous
791    /// (weixin ~100k) but only a fraction are ever inlined, so decoding
792    /// stays lazy at the use site. The slice bound covers the fixed
793    /// header + insns + try items with generous handler slack (accessor
794    /// tries are vanishingly rare; the snapshot is a deterministic
795    /// function of the image either way).
796    pub fn snapshot_accessor_code(&self) {
797        let mut snap: AccessorSnapshots = HashMap::default();
798        let mut bytes_total = 0usize;
799        for name in &self.order {
800            let Some(pc) = self.get_if_materialized(name) else {
801                continue;
802            };
803            for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
804                if m.code_off == 0
805                    || !m.is_static()
806                    || m.access & crate::access::ACC_SYNTHETIC == 0
807                {
808                    continue;
809                }
810                if snap.contains_key(&(m.dex_idx, m.code_off)) {
811                    continue;
812                }
813                let Some(dex) = self.dex(m.dex_idx) else {
814                    continue;
815                };
816                let raw = dex.raw();
817                let off = m.code_off as usize;
818                if off + 16 > raw.len() {
819                    continue;
820                }
821                let u2 = |o: usize| u16::from_le_bytes([raw[o], raw[o + 1]]) as usize;
822                let u4 = |o: usize| {
823                    u32::from_le_bytes([raw[o], raw[o + 1], raw[o + 2], raw[o + 3]]) as usize
824                };
825                let tries = u2(off + 6);
826                let insns = u4(off + 12);
827                let mut end = off + 16 + 2 * insns;
828                if tries > 0 {
829                    // pad to 4-align + try_item[tries] + handler blobs
830                    // (uleb-coded; slack covers realistic accessor shapes).
831                    end = end.next_multiple_of(4) + 8 * tries + 256 + 64 * tries;
832                }
833                let end = end.min(raw.len());
834                let slice: std::sync::Arc<[u8]> = raw[off..end].into();
835                bytes_total += slice.len();
836                snap.insert((m.dex_idx, m.code_off), slice);
837            }
838        }
839        if std::env::var("DDC_WALL").is_ok() {
840            eprintln!(
841                "[wall] accessor snapshot: {} entries, {} KB",
842                snap.len(),
843                bytes_total / 1024
844            );
845        }
846        let _ = self.accessor_code.set(snap);
847    }
848
849    /// Snapshotted accessor code_item bytes, when the snapshot was taken.
850    /// Parse with `CodeItem::parse(&bytes, 0)`.
851    pub fn accessor_code(&self, dex_idx: usize, code_off: u32) -> Option<std::sync::Arc<[u8]>> {
852        self.accessor_code.get()?.get(&(dex_idx, code_off)).cloned()
853    }
854
855    pub fn arm_retirement(&self) {
856        self.retire_armed
857            .store(true, std::sync::atomic::Ordering::Release);
858        let mut counts = self.retire_counts.lock().unwrap();
859        for c in counts.iter_mut() {
860            *c = 0;
861        }
862        for entry in self.classes.values() {
863            let di = match entry {
864                ClassEntry::Eager(pc) => pc.dex_idx,
865                ClassEntry::Lazy { at, .. } => at.0,
866            };
867            if let Some(c) = counts.get_mut(di) {
868                *c += 1;
869            }
870        }
871    }
872
873    /// Report one class finished; Some(image) when it was the image's
874    /// last class (driver batches these into release_images).
875    pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
876        if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
877            return None;
878        }
879        let entry = self.classes.get(class_name)?;
880        let di = match entry {
881            ClassEntry::Eager(pc) => pc.dex_idx,
882            ClassEntry::Lazy { at, .. } => at.0,
883        };
884        let mut counts = self.retire_counts.lock().unwrap();
885        match counts.get_mut(di) {
886            Some(c) => {
887                *c = c.saturating_sub(1);
888                if *c == 0 {
889                    Some(di)
890                } else {
891                    None
892                }
893            }
894            None => None,
895        }
896    }
897
898    /// Release the images' inflated bytes. Safe: the driver calls this
899    /// only when every class of each image is emitted or failed; new
900    /// `dex()` snapshots still work (tables valid, code accessors empty).
901    pub fn release_images(&self, indexes: &[usize]) {
902        let mut counts = self.retire_counts.lock().unwrap();
903        for &i in indexes {
904            if let Some(dex) = self.dexes.get(i) {
905                // Mark the shared image: code accessors go empty
906                // immediately; the bytes drop when the last snapshot
907                // drops (workers hold snapshots only mid-class).
908                dex.mark_released();
909            }
910            if let Some(c) = counts.get_mut(i) {
911                *c = u64::MAX; // released marker
912            }
913        }
914    }
915}
916
917pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
918    pool.class_names()
919        .filter(|name| match pool.outer_of(name) {
920            None => true,
921            Some(outer) => {
922                if pool.get(outer).is_none() {
923                    return true;
924                }
925                // The outer can come from an ANNOTATION (EnclosingClass)
926                // with no naming relationship to this class — obfuscated
927                // apps pair a 1-char name with a long enclosing descriptor
928                // (weixin), which made the old blind slice panic. Only a
929                // real `outer$tail` prefix yields a member tail; anything
930                // else is a standalone unit.
931                let rest = name
932                    .strip_prefix(outer)
933                    .and_then(|t| t.strip_prefix('$'))
934                    .unwrap_or("");
935                !clean_member_tail(rest)
936            }
937        })
938        .map(|n| n.to_string())
939        .collect()
940}
941
942/// Deterministic case-collision renames over the FILE-emission set:
943/// classes whose internal names differ only in letter case cannot share
944/// one case-insensitive directory; the first (sorted) member of each
945/// group keeps its name, the others gain `_2`, `_3`, … on the simple
946/// segment. The map carries identity entries for unrenamed file-level
947/// classes (they anchor nested prefix walks in apply_class_rename).
948pub fn case_rename_map(pool: &DexPool) -> HashMap<String, String> {
949    use std::collections::HashMap;
950    let mut groups: HashMap<String, Vec<String>> = HashMap::default();
951    for t in top_level_classes(pool) {
952        groups.entry(t.to_lowercase()).or_default().push(t);
953    }
954    let folds: std::collections::HashSet<String> = groups.keys().cloned().collect();
955    let mut map = HashMap::default();
956    for (_, mut members) in groups {
957        members.sort();
958        for (i, m) in members.iter().enumerate() {
959            if i == 0 {
960                map.insert(m.clone(), m.clone());
961                continue;
962            }
963            // Suffix the simple segment; keep incrementing if the fold
964            // of the suffixed name is already taken by a real class.
965            let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
966            let mut n = i + 1;
967            loop {
968                let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
969                let fold = cand.to_lowercase();
970                if !folds.contains(&fold) {
971                    map.insert(m.clone(), cand);
972                    break;
973                }
974                n += 1;
975            }
976        }
977    }
978    map
979}
980
981/// Display-level renames for two nested-class shapes that render
982/// uncompilable or inconsistent output:
983///
984/// 1. A member class whose simple tail equals its rendered parent's
985///    tail (`x0$a$a` inside `x0$a`): javac rejects a member class with
986///    the same simple name as its immediately enclosing class ("已在类
987///    x0中定义了类 x0.a") — a shape Kotlin lambda families hit by the
988///    thousand (lark alone: 22k). Bump the tail: `a` → `a2`, `a3`, …
989/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent from the
990///    pool): the member DECLARES as `b` inside `x0` but references
991///    printed `x0.a$b` — a type that exists nowhere. Flatten the
992///    display name to `x0$b` so every site agrees.
993///
994/// Both are pure display renames keyed by internal name; declarations,
995/// ctor names, file names and every type reference funnel through
996/// apply_class_rename, so one map keeps all sites consistent. Children
997/// Display-level renames for nested member classes that render
998/// uncompilable or inconsistent output:
999///
1000/// 1. A member whose simple tail equals ANY enclosing class's simple
1001///    name in its nesting chain. javac rejects more than the immediate
1002///    parent (`class a { static class a2 { static class a {} } }` —
1003///    member `a` two levels under `a` — is "已在类 X中定义了类 X.a");
1004///    Kotlin lambda families (`x0$a$a` inside `x0$a`) hit it by the
1005///    thousand (lark alone: 22k). Bump the tail until it clears every
1006///    ancestor name: `a` → `a3`.
1007/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent): the
1008///    member DECLARES as `b` inside `x0` but references printed
1009///    `x0.a$b` — a type that exists nowhere. Flatten the display to
1010///    `x0$b` so every site agrees.
1011///
1012/// Candidates build on the parent's DISPLAY name (ancestors are
1013/// renamed first — ascending `$`-depth order), so declarations,
1014/// constructor names, file names and every type reference stay
1015/// consistent through apply_class_rename. All are pure display
1016/// renames keyed by internal name.
1017fn nested_collision_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
1018    // Ancestors before descendants: a parent's rename must be in the
1019    // map when its children compute their display chains.
1020    let mut names: Vec<&String> = pool.order.iter().collect();
1021    names.sort_by_key(|n| n.matches('$').count());
1022    let mut assigned: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1023    // Top-level package segments. A nested member type DISPLAYED as `a2`
1024    // shadows package `a2` for every qualified `a2.x` reference in its
1025    // file (member types outrank package names in class scope), so a
1026    // renamed tail must not become a package shadow — round 47's failure
1027    // mode at nested level: candidate `a2` (for j2/g$a) broke the SAME
1028    // file's `extends a2.a` ("找不到符号 类 a" + a 9450-error Object
1029    // cascade as every dependent of the corrupt head failed too).
1030    let mut pkg_segments: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1031    for n in &pool.order {
1032        if let Some((seg, _)) = n.split_once('/') {
1033            pkg_segments.insert(seg.to_string());
1034        }
1035    }
1036    // LOCAL-OK gate (round-59 Design B): a field-obscuring rename may
1037    // only fire when every class referencing the candidate lives inside
1038    // the root file's family, so the rename perturbs exactly ONE rendered
1039    // file. Ungated (Design A) the rule netted reqable −1653 / lark −5221
1040    // but weibo +36916: renames there flipped ambiguous type-vs-package
1041    // resolutions across thousands of pre-existing conflict families and
1042    // javac's error recovery turned "missing supertype, body suppressed"
1043    // files into full Object cascades. Lazy (progressive-browse) pools
1044    // skip the gate AND the rule — the multi-second image scan must not
1045    // stall single-class queries; the full-decompile pipeline always
1046    // materializes everything before installing renames.
1047    let local_ok: jdc_core::FxHashSet<String> = if pool_majority_materialized(pool) {
1048        refscan::local_ok(&pool.dexes, &field_clash_cands(pool, map))
1049    } else {
1050        jdc_core::FxHashSet::default()
1051    };
1052    for name in names {
1053        if !name.contains('$') || map.contains_key(name) {
1054            continue;
1055        }
1056        let Some(parent) = find_outer_name(pool, name) else {
1057            continue;
1058        };
1059        // Only a REAL `parent$rest` name renders as an inline member;
1060        // anonymous/local/lambda tails keep their flat own-file names.
1061        let Some(rest) = name
1062            .strip_prefix(parent.as_str())
1063            .and_then(|t| t.strip_prefix('$'))
1064        else {
1065            continue;
1066        };
1067        if !clean_member_tail(rest) {
1068            continue;
1069        }
1070        // Display simple names of every enclosing level, bottom-up; `cur`
1071        // ends at the ROOT (the top-level class whose file renders this
1072        // nested type — the scope the renamed tail lives in).
1073        let mut chain: Vec<String> = Vec::new();
1074        let mut cur = parent.clone();
1075        loop {
1076            let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
1077            let simple = disp.rsplit('/').next().unwrap_or(&disp);
1078            chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
1079            match find_outer_name(pool, &cur) {
1080                Some(o) if o != cur => cur = o,
1081                _ => break,
1082            }
1083        }
1084        let root: &str = &cur;
1085        let disp_parent = map
1086            .get(&parent)
1087            .cloned()
1088            .unwrap_or_else(|| parent.clone());
1089        let tail = rest.rsplit('$').next().unwrap_or(rest);
1090        let orphan = rest.contains('$');
1091        // Field obscuring (JLS 6.4.2): a nested type whose simple name
1092        // equals an enclosing-class field is OBSCURED by it in a
1093        // `<Parent>.<tail>` qualified reference — `io/flutter/view/g$i`
1094        // (a `static enum i`) renders `g.i.t`, but g also declares an
1095        // instance field `i`, so javac resolves `g.i` to the field:
1096        // "无法从静态上下文中引用非静态 变量 i" (975 of 1140 reqable
1097        // static-context errors). The fix renames the NESTED TYPE (the
1098        // registry carries every render path: shorten, inner_simple —
1099        // whose bypass was the round-55 blowup, fixed in jdc-core —
1100        // classdec declarations), gated to LOCAL-OK candidates by the
1101        // refscan above. PoC: in-file rename of flutter g's `enum i`
1102        // took the class from 2452 errors to 1.
1103        let field_clash = local_ok.contains(name.as_str());
1104        let clash = chain.iter().any(|c| c == tail) || field_clash;
1105        if !orphan && !clash {
1106            continue;
1107        }
1108        // The enclosing class's field names (sanitized like the renderer)
1109        // — only needed once a rename actually fires, to keep the new
1110        // tail from being obscured again. Computed lazily so progressive
1111        // pools don't materialize parents for rules that don't rename.
1112        let mut enc_fields: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1113        if field_clash {
1114            if let Some(pc) = pool.get(&parent) {
1115                for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
1116                    enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
1117                }
1118            }
1119        }
1120        let mut k = 0u32;
1121        loop {
1122            k += 1;
1123            let cand_tail = if k == 1 {
1124                tail.to_string()
1125            } else {
1126                format!("{tail}{k}")
1127            };
1128            // The candidate must clear every ancestor display name (javac
1129            // checks the whole chain) AND every enclosing field name (else
1130            // the renamed type is just obscured again). Sibling nested
1131            // tails are covered by the pool.has_name check below.
1132            if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
1133                continue;
1134            }
1135            // Package shadow: the renamed tail becomes a member type of
1136            // the root file's class, outranking any package of the same
1137            // name for qualified refs in that file. Same-package top-level
1138            // classes likewise outrank-then-break in-file simple refs.
1139            // BOTH checks apply to field-clash renames only: for the
1140            // orphan rule the k=1 candidate is the natural display name
1141            // (`g0$$r` → `g0$r` renders identically — an invisible rename),
1142            // and rejecting it forced weixin's whole `g0$$X` Runnable
1143            // family to visible `X17` renames, breaking every lucky
1144            // `g0.r`-style resolution corpus-wide (+2911 `变量 r` — the
1145            // orphan rule is ungated by design, predating LOCAL-OK).
1146            if field_clash {
1147                if pkg_segments.contains(&cand_tail) {
1148                    continue;
1149                }
1150                if let Some((root_pkg, _)) = root.rsplit_once('/') {
1151                    if pool.has_name(&format!("{root_pkg}/{cand_tail}")) {
1152                        continue;
1153                    }
1154                }
1155            }
1156            let cand = format!("{disp_parent}${cand_tail}");
1157            if pool.has_name(&cand) || assigned.contains(&cand) {
1158                continue;
1159            }
1160            assigned.insert(cand.clone());
1161            map.insert(name.clone(), cand);
1162            break;
1163        }
1164    }
1165}
1166
1167/// Field-obscuring rename candidates: nested member types whose simple
1168/// name equals a field of the enclosing class, mapped to the top-level
1169/// class whose file renders them (the LOCAL-OK family root). Mirrors the
1170/// skip conditions of `nested_collision_renames` (same-name entries must
1171/// agree, or the gate proves locality for a rename that never fires — or
1172/// worse, skips one that does).
1173fn field_clash_cands(pool: &DexPool, map: &HashMap<String, String>) -> HashMap<String, String> {
1174    let mut out: HashMap<String, String> = HashMap::default();
1175    for name in &pool.order {
1176        if !name.contains('$') || map.contains_key(name) {
1177            continue;
1178        }
1179        let Some(parent) = find_outer_name(pool, name) else {
1180            continue;
1181        };
1182        let Some(rest) = name
1183            .strip_prefix(parent.as_str())
1184            .and_then(|t| t.strip_prefix('$'))
1185        else {
1186            continue;
1187        };
1188        if !clean_member_tail(rest) {
1189            continue;
1190        }
1191        let tail = rest.rsplit('$').next().unwrap_or(rest);
1192        let Some(pc) = pool.get(&parent) else {
1193            continue;
1194        };
1195        let clash = pc
1196            .static_fields
1197            .iter()
1198            .chain(pc.instance_fields.iter())
1199            .any(|f| crate::classdec::java_ident(&f.name).as_ref() == tail);
1200        if !clash {
1201            continue;
1202        }
1203        let mut root = parent.clone();
1204        loop {
1205            match find_outer_name(pool, &root) {
1206                Some(o) if o != root => root = o,
1207                _ => break,
1208            }
1209        }
1210        out.insert(name.clone(), root);
1211    }
1212    out
1213}
1214
1215/// Whether the pool is (majority-)materialized: full-decompile pipelines
1216/// call `materialize_all` before installing renames, progressive browse
1217/// pools materialize a handful of classes on demand. The refscan gate
1218/// must not run (or stall startup) in the progressive case.
1219fn pool_majority_materialized(pool: &DexPool) -> bool {
1220    let (mut mat, mut lazy) = (0usize, 0usize);
1221    for e in pool.classes.values() {
1222        match e {
1223            ClassEntry::Eager(_) => mat += 1,
1224            ClassEntry::Lazy { pc, .. } => {
1225                if pc.get().is_some() {
1226                    mat += 1;
1227                } else {
1228                    lazy += 1;
1229                }
1230            }
1231        }
1232    }
1233    lazy == 0 || mat >= lazy
1234}
1235
1236/// Compute and install the registry (call before worker threads spawn).
1237pub fn install_case_renames(pool: &DexPool) {
1238    let mut map = case_rename_map(pool);
1239    // Package-leaf shadows FIRST: the nested-collision renames compute
1240    // display chains off the (renamed) parent display names — running
1241    // them before the parent rename left both rules minting the same
1242    // display (`a2` twice in weibo's AIDL families).
1243    pkg_leaf_shadow_renames(pool, &mut map);
1244    nested_collision_renames(pool, &mut map);
1245    jdc_core::rename::set_class_renames(map);
1246    jdc_core::rename::set_field_renames(member_collision_renames(pool));
1247}
1248
1249/// Obfuscators can name a class after its own package leaf (`package k;`
1250/// `class k extends EditText`). Every SAME-PACKAGE reference to `k.Anything`
1251/// then resolves the first segment to the CLASS, not the package —
1252/// javac looks for `Anything` as a member of class k ("找不到符号 class p2,
1253/// location: class k"). Renaming the class (display-level, the registry
1254/// carries declarations and references) makes the package the only
1255/// thing the leaf name resolves to.
1256fn pkg_leaf_shadow_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
1257    let taken: jdc_core::FxHashSet<String> = pool.order.iter().cloned().collect();
1258    for name in &pool.order {
1259        let Some((pkg, simple)) = name.rsplit_once('/') else {
1260            continue;
1261        };
1262        if simple.is_empty() || pkg.is_empty() {
1263            continue;
1264        }
1265        let leaf = pkg.rsplit('/').next().unwrap_or(pkg);
1266        // class name == package leaf, and the package really exists
1267        // (more classes than just this one live under it).
1268        if simple != leaf {
1269            continue;
1270        }
1271        let prefix = format!("{pkg}/");
1272        let siblings = pool
1273            .order
1274            .iter()
1275            .filter(|n| n.starts_with(&prefix) && **n != *name)
1276            .count();
1277        if siblings == 0 || map.get(name).is_some_and(|v| v != name) {
1278            continue; // already renamed by an earlier rule; identity anchors are fine to overwrite
1279        }
1280        let mut k = 1u32;
1281        loop {
1282            k += 1;
1283            let cand = format!("{pkg}/{simple}{k}");
1284            if !taken.contains(&cand) && !map.values().any(|v| *v == cand) {
1285                map.insert(name.clone(), cand);
1286                break;
1287            }
1288        }
1289    }
1290}
1291
1292/// Member-level collision renames (fields AND methods of one class).
1293/// Two sources collapse onto one display name: obfuscators renaming a
1294/// synthetic outer reference (`this$0`) onto a real field's name, and
1295/// the identifier sanitizer mapping non-ASCII to `_` (whole
1296/// `ERROR_中文` families render one `ERROR________`).
1297/// Both are legal in bytecode (members resolve by index) and both are
1298/// javac "already defined" errors in source. Policy per colliding group
1299/// (fields keep the first non-synthetic name; a synthetic outer
1300/// reference restores `this$0`; everything else gets `2`/`3` suffixes;
1301/// method keys include erased parameter types):
1302/// Keys stay ORIGINAL (name, descriptor) so references resolve exactly;
1303/// only the rendered identifier changes.
1304fn member_collision_renames(
1305    pool: &DexPool,
1306) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
1307    let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
1308        HashMap::default();
1309    for name in &pool.order {
1310        let Some(pc) = pool.get_if_materialized(name) else {
1311            continue;
1312        };
1313        let outer_ref_desc = find_outer_name(pool, name)
1314            .filter(|o| o != name)
1315            .map(|o| format!("L{o};"));
1316        // ---- fields: one shared namespace (static + instance) ----
1317        let fields: Vec<&PoolField> = pc
1318            .static_fields
1319            .iter()
1320            .chain(pc.instance_fields.iter())
1321            .collect();
1322        let fgroups: HashMap<String, Vec<&PoolField>> = HashMap::default();
1323        let mut fgroups = fgroups;
1324        for f in &fields {
1325            let disp = crate::classdec::java_ident(&f.name).into_owned();
1326            fgroups.entry(disp).or_default().push(f);
1327        }
1328        let f_taken: jdc_core::FxHashSet<String> = fields
1329            .iter()
1330            .map(|f| crate::classdec::java_ident(&f.name).into_owned())
1331            .collect();
1332        let mut f_taken = f_taken;
1333        for group in fgroups.values() {
1334            if group.len() < 2 {
1335                continue;
1336            }
1337            let keeper_pos = group
1338                .iter()
1339                .position(|f| f.access & crate::access::ACC_SYNTHETIC == 0)
1340                .unwrap_or(0);
1341            for (gi, f) in group.iter().enumerate() {
1342                if gi == keeper_pos {
1343                    continue;
1344                }
1345                let base = crate::classdec::java_ident(&f.name).into_owned();
1346                let display: String = match &outer_ref_desc {
1347                    Some(ord)
1348                        if f.access & crate::access::ACC_SYNTHETIC != 0
1349                            && !f.is_static
1350                            && *ord == f.desc =>
1351                    {
1352                        let mut k = 0u32;
1353                        loop {
1354                            let cand = if k == 0 {
1355                                "this$0".to_string()
1356                            } else {
1357                                format!("this$0{k}")
1358                            };
1359                            if f_taken.insert(cand.clone()) {
1360                                break cand;
1361                            }
1362                            k += 1;
1363                        }
1364                    }
1365                    _ => suffix_unique(&base, &mut f_taken),
1366                };
1367                out.entry(std::sync::Arc::from(name.as_str()))
1368                    .or_default()
1369                    .push(jdc_core::rename::FieldRename {
1370                        name: std::sync::Arc::from(f.name.as_str()),
1371                        desc: std::sync::Arc::from(f.desc.as_str()),
1372                        display: std::sync::Arc::from(display.as_str()),
1373                    });
1374            }
1375        }
1376        // ---- methods: display key = sanitized name + erased params ----
1377        let methods: Vec<&PoolMethod> = pc.all_methods().collect();
1378        let mut m_taken: jdc_core::FxHashSet<String> = methods
1379            .iter()
1380            .map(|m| crate::classdec::java_ident(&m.name).into_owned())
1381            .collect();
1382        let mut mgroups: HashMap<(String, String), Vec<&PoolMethod>> = HashMap::default();
1383        for m in &methods {
1384            let d: &str = &m.desc;
1385            let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1386            let hi = d.find(')').unwrap_or(d.len());
1387            let key = (
1388                crate::classdec::java_ident(&m.name).into_owned(),
1389                d[lo..hi].to_string(),
1390            );
1391            mgroups.entry(key).or_default().push(m);
1392        }
1393        for ((base, _), group) in &mgroups {
1394            if group.len() < 2 {
1395                continue;
1396            }
1397            // A COVARIANT override pair (same ORIGINAL name, different
1398            // descriptors — the compiler's bridge shape: the interface's
1399            // `deserialize(e)` plus the narrower implementation). Renaming
1400            // either side breaks @Override ("is not abstract and does not
1401            // override abstract method deserialize(e)", ~2.2k on reqable);
1402            // the emitter's claim logic already renders only the
1403            // non-bridge member of the pair.
1404            let covariant = group
1405                .iter()
1406                .any(|m| group.iter().any(|o| o.name == m.name && o.desc != m.desc));
1407            if covariant {
1408                continue;
1409            }
1410            // Real re-declarations (same original name AND descriptor —
1411            // R8 duplicates) are already deduped by the emitter's claim
1412            // logic; only SANITIZER collapses reach here and every one of
1413            // them is a distinct method.
1414            let mut base = base.clone();
1415            let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
1416                jdc_core::FxHashSet::default();
1417            for m in group {
1418                if !seen_orig.insert((&m.name, &m.desc)) {
1419                    continue; // exact duplicate: emitter drops it
1420                }
1421                let display = suffix_unique(&base, &mut m_taken);
1422                out.entry(std::sync::Arc::from(name.as_str()))
1423                    .or_default()
1424                    .push(jdc_core::rename::FieldRename {
1425                        name: m.name.clone(),
1426                        desc: m.desc.clone(),
1427                        display: std::sync::Arc::from(display.as_str()),
1428                    });
1429                // The base stays the sanitized ORIGINAL so the first
1430                // occurrence keeps the plain name.
1431                let _ = &mut base;
1432            }
1433        }
1434    }
1435    out
1436}
1437
1438fn suffix_unique(base: &str, taken: &mut jdc_core::FxHashSet<String>) -> String {
1439    let mut k = 1u32;
1440    loop {
1441        k += 1;
1442        let cand = format!("{base}{k}");
1443        if taken.insert(cand.clone()) {
1444            return cand;
1445        }
1446    }
1447}
1448
1449/// Install member renames (call with the class rename install, before
1450/// workers spawn).
1451pub fn install_field_renames(pool: &DexPool) {
1452    jdc_core::rename::set_field_renames(member_collision_renames(pool));
1453}
1454pub use jdc_core::rename::apply_class_rename;