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