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;
16
17use std::collections::HashMap;
18
19use ddc_dex::annotations::{self, EncodedValue};
20
21/// A resolved static-field initializer.
22#[derive(Debug, Clone)]
23pub enum StaticValue {
24    Int(i64),
25    Float(f32),
26    Double(f64),
27    Str(String),
28    Type(String),
29    Boolean(bool),
30    Null,
31    /// (declaring class, field name) — enum constants.
32    Field(String, String),
33    Other,
34}
35use ddc_dex::{ClassDef, DexFile};
36use jdc_core::types::{
37    parse_field_descriptor, parse_method_descriptor, JavaType, MethodDescriptor,
38};
39
40// ---------------------------------------------------------------------------
41// Access flags (Dalvik numbering, JVM-compatible subset).
42// ---------------------------------------------------------------------------
43pub mod access {
44    pub const ACC_PUBLIC: u32 = 0x1;
45    pub const ACC_PRIVATE: u32 = 0x2;
46    pub const ACC_PROTECTED: u32 = 0x4;
47    pub const ACC_STATIC: u32 = 0x8;
48    pub const ACC_FINAL: u32 = 0x10;
49    pub const ACC_SYNCHRONIZED: u32 = 0x20;
50    pub const ACC_BRIDGE: u32 = 0x40;
51    pub const ACC_VARARGS: u32 = 0x80;
52    pub const ACC_NATIVE: u32 = 0x100;
53    pub const ACC_INTERFACE: u32 = 0x200;
54    pub const ACC_ABSTRACT: u32 = 0x400;
55    pub const ACC_STRICT: u32 = 0x800;
56    pub const ACC_SYNTHETIC: u32 = 0x1000;
57    pub const ACC_ANNOTATION: u32 = 0x2000;
58    pub const ACC_ENUM: u32 = 0x4000;
59    pub const ACC_CONSTRUCTOR: u32 = 0x1_0000;
60    pub const ACC_DECLARED_SYNCHRONIZED: u32 = 0x2_0000;
61}
62
63/// One field of a pooled class.
64#[derive(Debug, Clone)]
65pub struct PoolField {
66    pub name: String,
67    /// Field descriptor (`I`, `Ljava/lang/String;`, ...).
68    pub desc: String,
69    pub access: u32,
70    pub is_static: bool,
71}
72
73/// One method of a pooled class.
74#[derive(Debug, Clone)]
75pub struct PoolMethod {
76    /// Shared with the dex string table (the idx IS the dedup key).
77    pub name: std::sync::Arc<str>,
78    /// Method descriptor, shared per proto (a dex's proto table is the
79    /// descriptor dedup layer — 1.9M methods on lark share ~1 proto
80    /// table's worth of unique descriptors).
81    pub desc: std::sync::Arc<str>,
82    pub access: u32,
83    pub code_off: u32,
84    pub debug_info_off: u32,
85    /// Which DEX image the body lives in.
86    pub dex_idx: usize,
87}
88
89impl PoolMethod {
90    pub fn parsed_desc(&self) -> Option<MethodDescriptor> {
91        parse_method_descriptor(&self.desc)
92    }
93    pub fn is_static(&self) -> bool {
94        self.access & access::ACC_STATIC != 0
95    }
96    pub fn is_abstract_or_native(&self) -> bool {
97        self.access & (access::ACC_ABSTRACT | access::ACC_NATIVE) != 0
98    }
99}
100
101/// One pool entry: either already built, or name-registered with its
102/// (dex index, class_def index) locator for on-demand construction.
103enum ClassEntry {
104    Eager(PoolClass),
105    Lazy {
106        at: (usize, usize),
107        pc: std::sync::OnceLock<PoolClass>,
108    },
109}
110
111/// A class materialized from one `class_def_item` (with ids resolved to
112/// names and the class data expanded).
113#[derive(Debug, Clone)]
114pub struct PoolClass {
115    pub name: String,
116    pub access: u32,
117    /// `None` for `java/lang/Object` roots.
118    pub super_name: Option<String>,
119    pub interfaces: Vec<String>,
120    pub source_file: Option<String>,
121    pub static_fields: Vec<PoolField>,
122    pub instance_fields: Vec<PoolField>,
123    pub direct_methods: Vec<PoolMethod>,
124    pub virtual_methods: Vec<PoolMethod>,
125    /// Static initial values aligned with the head of `static_fields`.
126    pub static_values: Vec<StaticValue>,
127    /// Nesting evidence with annotation ids resolved to names.
128    pub nesting: ResolvedNesting,
129    /// Which DEX image the class body lives in (provenance header).
130    pub dex_idx: usize,
131}
132
133/// Nesting evidence, names resolved at pooling time.
134#[derive(Debug, Clone, Default)]
135pub struct ResolvedNesting {
136    pub enclosing_class: Option<String>,
137    /// (class internal name, method name) of the enclosing method.
138    pub enclosing_method: Option<(String, String)>,
139    pub member_classes: Vec<String>,
140}
141
142impl PoolClass {
143    pub fn is_interface(&self) -> bool {
144        self.access & access::ACC_INTERFACE != 0
145    }
146
147    pub fn is_enum(&self) -> bool {
148        self.access & access::ACC_ENUM != 0
149    }
150
151    pub fn is_synthetic(&self) -> bool {
152        self.access & access::ACC_SYNTHETIC != 0
153    }
154
155    pub fn is_static_nested(&self) -> bool {
156        self.access & access::ACC_STATIC != 0
157    }
158
159    /// All methods in declaration order.
160    pub fn all_methods(&self) -> impl Iterator<Item = &PoolMethod> {
161        self.direct_methods
162            .iter()
163            .chain(self.virtual_methods.iter())
164    }
165
166    /// Find a method by name + descriptor.
167    pub fn find_method(&self, name: &str, desc: &str) -> Option<&PoolMethod> {
168        self.all_methods()
169            .find(|m| &*m.name == name && &*m.desc == desc)
170    }
171
172    /// Constructors `<init>` matching `arity` descriptor arguments.
173    pub fn ctors_by_arity(&self, arity: usize) -> Vec<&PoolMethod> {
174        self.all_methods()
175            .filter(|m| &*m.name == "<init>")
176            .filter(|m| {
177                m.parsed_desc()
178                    .map(|d| d.args.len() == arity)
179                    .unwrap_or(false)
180            })
181            .collect()
182    }
183
184    pub fn field_flags_of(&self, name: &str) -> Option<u32> {
185        self.static_fields
186            .iter()
187            .chain(self.instance_fields.iter())
188            .find(|f| f.name == name)
189            .map(|f| f.access)
190    }
191}
192
193/// Multi-DEX class pool: classes from all images, first definition wins.
194pub struct DexPool {
195    /// Images as Arcs: `dex()` hands out snapshots whose borrows stay
196    /// valid for the Arc's lifetime (decompile workers hold them), while
197    /// `release_images` empties the image data through exclusive access
198    /// when all of an image's classes are done (full-decompile driver
199    /// only — progressive callers keep images whole).
200    dexes: Vec<std::sync::Arc<DexFile>>,
201    /// Human label per image ("weibo!classes.dex") for the provenance
202    /// header; defaults to "dex N" until the CLI names the inputs.
203    pub dex_labels: Vec<String>,
204    /// Name → class entry. Eager pools materialize everything at add
205    /// time (full decompile, tests); lazy pools register names only and
206    /// materialize on first `get` (progressive `getclass` — one class's
207    /// PoolClass costs annotation reads, 145k of them cost ~0.2s).
208    classes: HashMap<String, ClassEntry>,
209    /// Class names in insertion order (stable for output).
210    pub order: Vec<String>,
211    /// Per-image remaining class count once `arm_retirement` fires.
212    retire_counts: std::sync::Mutex<Vec<u64>>,
213    retire_armed: std::sync::atomic::AtomicBool,
214    /// name → outer (computed once; `$` heuristic + dalvik annotations).
215    outers: std::sync::OnceLock<HashMap<String, Option<String>>>,
216    /// outer → direct children (computed once).
217    children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
218}
219
220impl DexPool {
221    pub fn new() -> Self {
222        DexPool {
223            dexes: Vec::new(),
224            retire_armed: std::sync::atomic::AtomicBool::new(false),
225            dex_labels: Vec::new(),
226            classes: HashMap::new(),
227            order: Vec::new(),
228            retire_counts: std::sync::Mutex::new(Vec::new()),
229            outers: std::sync::OnceLock::new(),
230            children: std::sync::OnceLock::new(),
231        }
232    }
233
234    /// name → outer map, computed lazily once.
235    fn outer_map(&self) -> &HashMap<String, Option<String>> {
236        self.outers.get_or_init(|| {
237            self.order
238                .iter()
239                .map(|n| (n.clone(), find_outer_name(self, n)))
240                .collect()
241        })
242    }
243
244    /// The outer class of `name`, from the cached map (borrowed).
245    pub fn outer_of(&self, name: &str) -> Option<&str> {
246        self.outer_map().get(name).and_then(|o| o.as_deref())
247    }
248
249    /// Direct nested children of `internal` (any `$` depth 1), cached.
250    /// (Perf: returns a borrowed slice — the owned-Vec clone ran once per
251    /// class on 98k-class runs.)
252    pub fn children_of(&self, internal: &str) -> &[String] {
253        if self.children.get().is_none() {
254            let idx: HashMap<String, Vec<String>> = HashMap::new();
255            let _ = self.children.set(idx);
256            let mut idx: HashMap<String, Vec<String>> = HashMap::new();
257            for name in &self.order {
258                if let Some(outer) = self.outer_of(name).map(str::to_string) {
259                    idx.entry(outer).or_default().push(name.clone());
260                }
261            }
262            let _ = self.children.set(idx);
263        }
264        self.children
265            .get()
266            .and_then(|m| m.get(internal))
267            .map(|v| v.as_slice())
268            .unwrap_or(&[])
269    }
270
271    /// Adds one image; duplicate class names keep their first definition.
272    /// Returns the image's index — call `set_dex_label` with it to give the
273    /// provenance header a real origin instead of the default "dex N".
274    pub fn add_dex(&mut self, dex: DexFile) -> usize {
275        let dex_idx = self.dexes.len();
276        // The annotation reader borrows the image; pool classes are built
277        // before ownership moves into `self.dexes` (no full-image copy).
278        for cd in &dex.class_defs {
279            let name = dex.class_name(cd.class_idx);
280            if self.classes.contains_key(&name) {
281                continue;
282            }
283            let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
284            self.classes.insert(name.clone(), ClassEntry::Eager(pc));
285            self.order.push(name);
286        }
287        self.dexes.push(std::sync::Arc::new(dex));
288        self.retire_counts.lock().unwrap().push(0);
289        self.dex_labels.push(format!("dex {}", dex_idx));
290        dex_idx
291    }
292
293    /// Registers class NAMES only; the PoolClass (annotation reads
294    /// included) is materialized on first `get`. Duplicate class names
295    /// keep their first definition, matching `add_dex`.
296    pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
297        let dex_idx = self.dexes.len();
298        for (ci, cd) in dex.class_defs.iter().enumerate() {
299            let name = dex.class_name(cd.class_idx);
300            if self.classes.contains_key(&name) {
301                continue;
302            }
303            self.classes.insert(
304                name.clone(),
305                ClassEntry::Lazy {
306                    at: (dex_idx, ci),
307                    pc: std::sync::OnceLock::new(),
308                },
309            );
310            self.order.push(name);
311        }
312        self.dexes.push(std::sync::Arc::new(dex));
313        self.retire_counts.lock().unwrap().push(0);
314        self.dex_labels.push(format!("dex {}", dex_idx));
315        dex_idx
316    }
317
318    /// Label the image at `idx` (see `add_dex`).
319    pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
320        if let Some(slot) = self.dex_labels.get_mut(idx) {
321            *slot = label.into();
322        }
323    }
324
325    pub fn len(&self) -> usize {
326        self.classes.len()
327    }
328
329    pub fn is_empty(&self) -> bool {
330        self.classes.is_empty()
331    }
332
333    pub fn get(&self, internal: &str) -> Option<&PoolClass> {
334        match self.classes.get(internal)? {
335            ClassEntry::Eager(pc) => Some(pc),
336            ClassEntry::Lazy { at, pc } => {
337                if let Some(built) = pc.get() {
338                    return Some(built);
339                }
340                let (di, ci) = *at;
341                let dex = self.dexes.get(di)?;
342                let cd = dex.class_defs.get(ci)?;
343                let built = pool_class_of(dex, dex.raw(), cd, di);
344                Some(pc.get_or_init(move || built))
345            }
346        }
347    }
348
349    /// Materialize every lazy entry (parallel across the given chunk
350    /// count). After this the pool is observationally identical to an
351    /// eagerly-built one — including the annotation-aware outer-map —
352    /// while the annotation reads ran on worker threads instead of the
353    /// serial pool build (~0.2s on weibo).
354    pub fn materialize_all(&self, threads: usize) {
355        let keys: Vec<String> = self
356            .classes
357            .iter()
358            .filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
359            .map(|(k, _)| k.clone())
360            .collect();
361        let threads = threads.max(1);
362        let next = std::sync::atomic::AtomicUsize::new(0);
363        std::thread::scope(|scope| {
364            for _ in 0..threads {
365                scope.spawn(|| loop {
366                    let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
367                    let Some(k) = keys.get(i) else { break };
368                    let _ = self.get(k);
369                });
370            }
371        });
372    }
373
374    /// `get` without materializing: None for not-yet-built lazy entries.
375    fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
376        match self.classes.get(internal)? {
377            ClassEntry::Eager(pc) => Some(pc),
378            ClassEntry::Lazy { pc, .. } => pc.get(),
379        }
380    }
381
382    /// Name registered (no materialization).
383    pub fn has_name(&self, internal: &str) -> bool {
384        self.classes.contains_key(internal)
385    }
386
387    /// One image as an Arc snapshot (borrows of the snapshot live as
388    /// long as the caller holds the Arc).
389    pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
390        self.dexes.get(idx).cloned()
391    }
392
393    pub fn dex_count(&self) -> usize {
394        self.dexes.len()
395    }
396
397    pub fn class_names(&self) -> impl Iterator<Item = &str> {
398        self.order.iter().map(|s| s.as_str())
399    }
400
401    /// True when `sub` is assignable to `sup` (internal names), walking the
402    /// pool's hierarchy. Unknown classes are never assignable (conservative).
403    pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
404        if sub == sup {
405            return true;
406        }
407        // Arrays: covariance by element type when both are arrays.
408        if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
409            if sub_elem.starts_with('L')
410                && sup_elem.starts_with('L')
411                && sub_elem.ends_with(';')
412                && sup_elem.ends_with(';')
413            {
414                return self.is_subtype(
415                    &sub_elem[1..sub_elem.len() - 1],
416                    &sup_elem[1..sup_elem.len() - 1],
417                );
418            }
419            return sub_elem == sup_elem;
420        }
421        let mut cur = self.get(sub);
422        let mut hops = 0;
423        while let Some(c) = cur {
424            hops += 1;
425            if hops > 64 {
426                return false;
427            }
428            for i in &c.interfaces {
429                if i == sup || self.is_subtype(i, sup) {
430                    return true;
431                }
432            }
433            match &c.super_name {
434                Some(s) if s == sup => return true,
435                Some(s) if s != "java/lang/Object" => {
436                    cur = self.get(s);
437                }
438                _ => return false,
439            }
440        }
441        false
442    }
443}
444
445impl Default for DexPool {
446    fn default() -> Self {
447        Self::new()
448    }
449}
450
451fn array_elem(desc: &str) -> Option<&str> {
452    desc.strip_prefix('[')
453}
454
455fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
456    match v {
457        EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
458        EncodedValue::Short(x) => StaticValue::Int(*x as i64),
459        EncodedValue::Char(x) => StaticValue::Int(*x as i64),
460        EncodedValue::Int(x) => StaticValue::Int(*x as i64),
461        EncodedValue::Long(x) => StaticValue::Int(*x),
462        EncodedValue::Float(x) => StaticValue::Float(*x),
463        EncodedValue::Double(x) => StaticValue::Double(*x),
464        EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
465        EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
466        EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
467        EncodedValue::Null => StaticValue::Null,
468        EncodedValue::Enum(i) | EncodedValue::Field(i) => {
469            let f = dex.field(*i);
470            StaticValue::Field(
471                dex.class_name(f.class_idx),
472                dex.string(f.name_idx).to_string(),
473            )
474        }
475        _ => StaticValue::Other,
476    }
477}
478
479fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
480    let name = dex.class_name(cd.class_idx);
481    let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
482        None
483    } else {
484        let s = dex.class_name(cd.superclass_idx);
485        if s.is_empty() || s == "java/lang/Object" {
486            None
487        } else {
488            Some(s)
489        }
490    };
491    let interfaces = dex
492        .interfaces_of(cd)
493        .into_iter()
494        .map(|t| dex.class_name(t))
495        .collect();
496    let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
497        None
498    } else {
499        Some(dex.string(cd.source_file_idx).to_string())
500    };
501
502    let data = dex.class_data(cd);
503    let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
504        let f = dex.field(ef.field_idx);
505        PoolField {
506            name: dex.string(f.name_idx).to_string(),
507            desc: dex.type_name(f.type_idx).to_string(),
508            access: ef.access_flags,
509            is_static,
510        }
511    };
512    let static_fields: Vec<PoolField> = data
513        .static_fields
514        .iter()
515        .map(|f| mk_field(f, true))
516        .collect();
517    let instance_fields: Vec<PoolField> = data
518        .instance_fields
519        .iter()
520        .map(|f| mk_field(f, false))
521        .collect();
522    let mk_method = |em: &ddc_dex::EncodedMethod| {
523        let m = dex.method(em.method_idx);
524        let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
525            Some(d) => (em.code_off, d),
526            None => (0, 0),
527        };
528        PoolMethod {
529            name: dex.string_arc(m.name_idx),
530            desc: dex.proto_desc(m.proto_idx),
531            access: em.access_flags,
532            code_off,
533            debug_info_off,
534            dex_idx,
535        }
536    };
537    let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
538    let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
539
540    let static_values: Vec<StaticValue> = dex
541        .static_values(cd.static_values_off)
542        .into_iter()
543        .map(|v| resolve_static_value(&v, dex))
544        .collect();
545    let anns = if cd.annotations_off != 0 {
546        annotations::read_class_annotations(raw, cd.annotations_off)
547    } else {
548        Vec::new()
549    };
550    let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
551    let nesting = ResolvedNesting {
552        enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
553        enclosing_method: raw.enclosing_method.map(|m| {
554            let mid = dex.method(m);
555            (
556                dex.class_name(mid.class_idx),
557                dex.string(mid.name_idx).to_string(),
558            )
559        }),
560        member_classes: raw
561            .member_classes
562            .into_iter()
563            .map(|t| dex.class_name(t))
564            .collect(),
565    };
566
567    PoolClass {
568        name,
569        access: cd.access_flags,
570        super_name,
571        interfaces,
572        source_file,
573        static_fields,
574        instance_fields,
575        direct_methods,
576        virtual_methods,
577        static_values,
578        nesting,
579        dex_idx,
580    }
581}
582
583/// JavaType for a field/method descriptor segment.
584pub fn desc_type(desc: &str) -> JavaType {
585    parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
586}
587
588/// Nesting evidence for `internal`: dalvik annotations first, then the
589/// `$`-name heuristic against the pool.
590pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
591    // Annotation refinement only for ALREADY-MATERIALIZED classes: eager
592    // pools (full decompile) behave exactly as before; lazy pools
593    // (progressive getclass) fall straight to the `$` chain without
594    // materializing the world.
595    if let Some(pc) = pool.get_if_materialized(internal) {
596        if let Some(enc) = &pc.nesting.enclosing_class {
597            return Some(enc.clone());
598        }
599    }
600    let mut rest = internal;
601    while let Some(d) = rest.rfind('$') {
602        let cand = &rest[..d];
603        if pool.has_name(cand) {
604            return Some(cand.to_string());
605        }
606        rest = cand;
607    }
608    None
609}
610
611/// True when the `$` tail names a plain member class (not anonymous/local/
612/// lambda), i.e. the class renders inside its outer's compilation unit.
613fn clean_member_tail(rest: &str) -> bool {
614    if rest.starts_with('-') {
615        return false;
616    }
617    let tail = rest.rsplit('$').next().unwrap_or(rest);
618    if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
619        return false;
620    }
621    true
622}
623
624/// Classes to emit as their own compilation units: top-level classes plus
625/// anonymous/local/lambda-shaped ones (clean members render inline).
626impl DexPool {
627    /// Arm image retirement (full-decompile driver only): count one
628    /// pending class per image by pool ownership.
629    pub fn arm_retirement(&self) {
630        self.retire_armed
631            .store(true, std::sync::atomic::Ordering::Release);
632        let mut counts = self.retire_counts.lock().unwrap();
633        for c in counts.iter_mut() {
634            *c = 0;
635        }
636        for entry in self.classes.values() {
637            let di = match entry {
638                ClassEntry::Eager(pc) => pc.dex_idx,
639                ClassEntry::Lazy { at, .. } => at.0,
640            };
641            if let Some(c) = counts.get_mut(di) {
642                *c += 1;
643            }
644        }
645    }
646
647    /// Report one class finished; Some(image) when it was the image's
648    /// last class (driver batches these into release_images).
649    pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
650        if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
651            return None;
652        }
653        let entry = self.classes.get(class_name)?;
654        let di = match entry {
655            ClassEntry::Eager(pc) => pc.dex_idx,
656            ClassEntry::Lazy { at, .. } => at.0,
657        };
658        let mut counts = self.retire_counts.lock().unwrap();
659        match counts.get_mut(di) {
660            Some(c) => {
661                *c = c.saturating_sub(1);
662                if *c == 0 {
663                    Some(di)
664                } else {
665                    None
666                }
667            }
668            None => None,
669        }
670    }
671
672    /// Release the images' inflated bytes. Safe: the driver calls this
673    /// only when every class of each image is emitted or failed; new
674    /// `dex()` snapshots still work (tables valid, code accessors empty).
675    pub fn release_images(&self, indexes: &[usize]) {
676        let mut counts = self.retire_counts.lock().unwrap();
677        for &i in indexes {
678            if let Some(dex) = self.dexes.get(i) {
679                // Mark the shared image: code accessors go empty
680                // immediately; the bytes drop when the last snapshot
681                // drops (workers hold snapshots only mid-class).
682                dex.mark_released();
683            }
684            if let Some(c) = counts.get_mut(i) {
685                *c = u64::MAX; // released marker
686            }
687        }
688    }
689}
690
691pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
692    pool.class_names()
693        .filter(|name| match pool.outer_of(name) {
694            None => true,
695            Some(outer) => {
696                if pool.get(outer).is_none() {
697                    return true;
698                }
699                // The outer can come from an ANNOTATION (EnclosingClass)
700                // with no naming relationship to this class — obfuscated
701                // apps pair a 1-char name with a long enclosing descriptor
702                // (weixin), which made the old blind slice panic. Only a
703                // real `outer$tail` prefix yields a member tail; anything
704                // else is a standalone unit.
705                let rest = name
706                    .strip_prefix(outer)
707                    .and_then(|t| t.strip_prefix('$'))
708                    .unwrap_or("");
709                !clean_member_tail(rest)
710            }
711        })
712        .map(|n| n.to_string())
713        .collect()
714}
715
716/// Deterministic case-collision renames over the FILE-emission set:
717/// classes whose internal names differ only in letter case cannot share
718/// one case-insensitive directory; the first (sorted) member of each
719/// group keeps its name, the others gain `_2`, `_3`, … on the simple
720/// segment. The map carries identity entries for unrenamed file-level
721/// classes (they anchor nested prefix walks in apply_class_rename).
722pub fn case_rename_map(pool: &DexPool) -> std::collections::HashMap<String, String> {
723    use std::collections::HashMap;
724    let mut groups: HashMap<String, Vec<String>> = HashMap::new();
725    for t in top_level_classes(pool) {
726        groups.entry(t.to_lowercase()).or_default().push(t);
727    }
728    let folds: std::collections::HashSet<String> = groups.keys().cloned().collect();
729    let mut map = HashMap::new();
730    for (_, mut members) in groups {
731        members.sort();
732        for (i, m) in members.iter().enumerate() {
733            if i == 0 {
734                map.insert(m.clone(), m.clone());
735                continue;
736            }
737            // Suffix the simple segment; keep incrementing if the fold
738            // of the suffixed name is already taken by a real class.
739            let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
740            let mut n = i + 1;
741            loop {
742                let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
743                let fold = cand.to_lowercase();
744                if !folds.contains(&fold) {
745                    map.insert(m.clone(), cand);
746                    break;
747                }
748                n += 1;
749            }
750        }
751    }
752    map
753}
754
755/// Compute and install the registry (call before worker threads spawn).
756pub fn install_case_renames(pool: &DexPool) {
757    jdc_core::rename::set_class_renames(case_rename_map(pool));
758}
759
760pub use jdc_core::rename::apply_class_rename;