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 std::collections::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    /// outer → direct children (computed once).
218    children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
219}
220
221impl DexPool {
222    pub fn new() -> Self {
223        DexPool {
224            dexes: Vec::new(),
225            retire_armed: std::sync::atomic::AtomicBool::new(false),
226            dex_labels: Vec::new(),
227            classes: HashMap::new(),
228            order: Vec::new(),
229            retire_counts: std::sync::Mutex::new(Vec::new()),
230            outers: std::sync::OnceLock::new(),
231            children: std::sync::OnceLock::new(),
232        }
233    }
234
235    /// name → outer map, computed lazily once.
236    fn outer_map(&self) -> &HashMap<String, Option<String>> {
237        self.outers.get_or_init(|| {
238            self.order
239                .iter()
240                .map(|n| (n.clone(), find_outer_name(self, n)))
241                .collect()
242        })
243    }
244
245    /// The outer class of `name`, from the cached map (borrowed).
246    pub fn outer_of(&self, name: &str) -> Option<&str> {
247        self.outer_map().get(name).and_then(|o| o.as_deref())
248    }
249
250    /// Direct nested children of `internal` (any `$` depth 1), cached.
251    /// (Perf: returns a borrowed slice — the owned-Vec clone ran once per
252    /// class on 98k-class runs.)
253    pub fn children_of(&self, internal: &str) -> &[String] {
254        if self.children.get().is_none() {
255            let idx: HashMap<String, Vec<String>> = HashMap::new();
256            let _ = self.children.set(idx);
257            let mut idx: HashMap<String, Vec<String>> = HashMap::new();
258            for name in &self.order {
259                if let Some(outer) = self.outer_of(name).map(str::to_string) {
260                    idx.entry(outer).or_default().push(name.clone());
261                }
262            }
263            let _ = self.children.set(idx);
264        }
265        self.children
266            .get()
267            .and_then(|m| m.get(internal))
268            .map(|v| v.as_slice())
269            .unwrap_or(&[])
270    }
271
272    /// Adds one image; duplicate class names keep their first definition.
273    /// Returns the image's index — call `set_dex_label` with it to give the
274    /// provenance header a real origin instead of the default "dex N".
275    pub fn add_dex(&mut self, dex: DexFile) -> usize {
276        let dex_idx = self.dexes.len();
277        // The annotation reader borrows the image; pool classes are built
278        // before ownership moves into `self.dexes` (no full-image copy).
279        for cd in &dex.class_defs {
280            let name = dex.class_name(cd.class_idx);
281            if self.classes.contains_key(&name) {
282                continue;
283            }
284            let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
285            self.classes.insert(name.clone(), ClassEntry::Eager(pc));
286            self.order.push(name);
287        }
288        self.dexes.push(std::sync::Arc::new(dex));
289        self.retire_counts.lock().unwrap().push(0);
290        self.dex_labels.push(format!("dex {}", dex_idx));
291        dex_idx
292    }
293
294    /// Registers class NAMES only; the PoolClass (annotation reads
295    /// included) is materialized on first `get`. Duplicate class names
296    /// keep their first definition, matching `add_dex`.
297    pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
298        let dex_idx = self.dexes.len();
299        for (ci, cd) in dex.class_defs.iter().enumerate() {
300            let name = dex.class_name(cd.class_idx);
301            if self.classes.contains_key(&name) {
302                continue;
303            }
304            self.classes.insert(
305                name.clone(),
306                ClassEntry::Lazy {
307                    at: (dex_idx, ci),
308                    pc: std::sync::OnceLock::new(),
309                },
310            );
311            self.order.push(name);
312        }
313        self.dexes.push(std::sync::Arc::new(dex));
314        self.retire_counts.lock().unwrap().push(0);
315        self.dex_labels.push(format!("dex {}", dex_idx));
316        dex_idx
317    }
318
319    /// Label the image at `idx` (see `add_dex`).
320    pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
321        if let Some(slot) = self.dex_labels.get_mut(idx) {
322            *slot = label.into();
323        }
324    }
325
326    pub fn len(&self) -> usize {
327        self.classes.len()
328    }
329
330    pub fn is_empty(&self) -> bool {
331        self.classes.is_empty()
332    }
333
334    pub fn get(&self, internal: &str) -> Option<&PoolClass> {
335        match self.classes.get(internal)? {
336            ClassEntry::Eager(pc) => Some(pc),
337            ClassEntry::Lazy { at, pc } => {
338                if let Some(built) = pc.get() {
339                    return Some(built);
340                }
341                let (di, ci) = *at;
342                let dex = self.dexes.get(di)?;
343                let cd = dex.class_defs.get(ci)?;
344                let built = pool_class_of(dex, dex.raw(), cd, di);
345                Some(pc.get_or_init(move || built))
346            }
347        }
348    }
349
350    /// Materialize every lazy entry (parallel across the given chunk
351    /// count). After this the pool is observationally identical to an
352    /// eagerly-built one — including the annotation-aware outer-map —
353    /// while the annotation reads ran on worker threads instead of the
354    /// serial pool build (~0.2s on weibo).
355    pub fn materialize_all(&self, threads: usize) {
356        let keys: Vec<String> = self
357            .classes
358            .iter()
359            .filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
360            .map(|(k, _)| k.clone())
361            .collect();
362        let threads = threads.max(1);
363        let next = std::sync::atomic::AtomicUsize::new(0);
364        std::thread::scope(|scope| {
365            for _ in 0..threads {
366                scope.spawn(|| loop {
367                    let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
368                    let Some(k) = keys.get(i) else { break };
369                    let _ = self.get(k);
370                });
371            }
372        });
373    }
374
375    /// `get` without materializing: None for not-yet-built lazy entries.
376    fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
377        match self.classes.get(internal)? {
378            ClassEntry::Eager(pc) => Some(pc),
379            ClassEntry::Lazy { pc, .. } => pc.get(),
380        }
381    }
382
383    /// Name registered (no materialization).
384    pub fn has_name(&self, internal: &str) -> bool {
385        self.classes.contains_key(internal)
386    }
387
388    /// One image as an Arc snapshot (borrows of the snapshot live as
389    /// long as the caller holds the Arc).
390    pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
391        self.dexes.get(idx).cloned()
392    }
393
394    pub fn dex_count(&self) -> usize {
395        self.dexes.len()
396    }
397
398    pub fn class_names(&self) -> impl Iterator<Item = &str> {
399        self.order.iter().map(|s| s.as_str())
400    }
401
402    /// True when `sub` is assignable to `sup` (internal names), walking the
403    /// pool's hierarchy. Unknown classes are never assignable (conservative).
404    pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
405        if sub == sup {
406            return true;
407        }
408        // Arrays: covariance by element type when both are arrays.
409        if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
410            if sub_elem.starts_with('L')
411                && sup_elem.starts_with('L')
412                && sub_elem.ends_with(';')
413                && sup_elem.ends_with(';')
414            {
415                return self.is_subtype(
416                    &sub_elem[1..sub_elem.len() - 1],
417                    &sup_elem[1..sup_elem.len() - 1],
418                );
419            }
420            return sub_elem == sup_elem;
421        }
422        let mut cur = self.get(sub);
423        let mut hops = 0;
424        while let Some(c) = cur {
425            hops += 1;
426            if hops > 64 {
427                return false;
428            }
429            for i in &c.interfaces {
430                if i == sup || self.is_subtype(i, sup) {
431                    return true;
432                }
433            }
434            match &c.super_name {
435                Some(s) if s == sup => return true,
436                Some(s) if s != "java/lang/Object" => {
437                    cur = self.get(s);
438                }
439                _ => return false,
440            }
441        }
442        false
443    }
444}
445
446impl Default for DexPool {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452fn array_elem(desc: &str) -> Option<&str> {
453    desc.strip_prefix('[')
454}
455
456fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
457    match v {
458        EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
459        EncodedValue::Short(x) => StaticValue::Int(*x as i64),
460        EncodedValue::Char(x) => StaticValue::Int(*x as i64),
461        EncodedValue::Int(x) => StaticValue::Int(*x as i64),
462        EncodedValue::Long(x) => StaticValue::Int(*x),
463        EncodedValue::Float(x) => StaticValue::Float(*x),
464        EncodedValue::Double(x) => StaticValue::Double(*x),
465        EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
466        EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
467        EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
468        EncodedValue::Null => StaticValue::Null,
469        EncodedValue::Enum(i) | EncodedValue::Field(i) => {
470            let f = dex.field(*i);
471            StaticValue::Field(
472                dex.class_name(f.class_idx),
473                dex.string(f.name_idx).to_string(),
474            )
475        }
476        _ => StaticValue::Other,
477    }
478}
479
480fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
481    let name = dex.class_name(cd.class_idx);
482    let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
483        None
484    } else {
485        let s = dex.class_name(cd.superclass_idx);
486        if s.is_empty() || s == "java/lang/Object" {
487            None
488        } else {
489            Some(s)
490        }
491    };
492    let interfaces = dex
493        .interfaces_of(cd)
494        .into_iter()
495        .map(|t| dex.class_name(t))
496        .collect();
497    let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
498        None
499    } else {
500        Some(dex.string(cd.source_file_idx).to_string())
501    };
502
503    let data = dex.class_data(cd);
504    let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
505        let f = dex.field(ef.field_idx);
506        PoolField {
507            name: dex.string(f.name_idx).to_string(),
508            desc: dex.type_name(f.type_idx).to_string(),
509            access: ef.access_flags,
510            is_static,
511        }
512    };
513    let static_fields: Vec<PoolField> = data
514        .static_fields
515        .iter()
516        .map(|f| mk_field(f, true))
517        .collect();
518    let instance_fields: Vec<PoolField> = data
519        .instance_fields
520        .iter()
521        .map(|f| mk_field(f, false))
522        .collect();
523    let mk_method = |em: &ddc_dex::EncodedMethod| {
524        let m = dex.method(em.method_idx);
525        let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
526            Some(d) => (em.code_off, d),
527            None => (0, 0),
528        };
529        PoolMethod {
530            name: dex.string_arc(m.name_idx),
531            desc: dex.proto_desc(m.proto_idx),
532            access: em.access_flags,
533            code_off,
534            debug_info_off,
535            dex_idx,
536        }
537    };
538    let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
539    let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
540
541    let static_values: Vec<StaticValue> = dex
542        .static_values(cd.static_values_off)
543        .into_iter()
544        .map(|v| resolve_static_value(&v, dex))
545        .collect();
546    let anns = if cd.annotations_off != 0 {
547        annotations::read_class_annotations(raw, cd.annotations_off)
548    } else {
549        Vec::new()
550    };
551    let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
552    let nesting = ResolvedNesting {
553        enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
554        enclosing_method: raw.enclosing_method.map(|m| {
555            let mid = dex.method(m);
556            (
557                dex.class_name(mid.class_idx),
558                dex.string(mid.name_idx).to_string(),
559            )
560        }),
561        member_classes: raw
562            .member_classes
563            .into_iter()
564            .map(|t| dex.class_name(t))
565            .collect(),
566    };
567
568    PoolClass {
569        name,
570        access: cd.access_flags,
571        super_name,
572        interfaces,
573        source_file,
574        static_fields,
575        instance_fields,
576        direct_methods,
577        virtual_methods,
578        static_values,
579        nesting,
580        dex_idx,
581    }
582}
583
584/// JavaType for a field/method descriptor segment.
585pub fn desc_type(desc: &str) -> JavaType {
586    parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
587}
588
589/// Nesting evidence for `internal`: dalvik annotations first, then the
590/// `$`-name heuristic against the pool.
591pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
592    // Annotation refinement only for ALREADY-MATERIALIZED classes: eager
593    // pools (full decompile) behave exactly as before; lazy pools
594    // (progressive getclass) fall straight to the `$` chain without
595    // materializing the world.
596    if let Some(pc) = pool.get_if_materialized(internal) {
597        if let Some(enc) = &pc.nesting.enclosing_class {
598            return Some(enc.clone());
599        }
600    }
601    let mut rest = internal;
602    while let Some(d) = rest.rfind('$') {
603        let cand = &rest[..d];
604        if pool.has_name(cand) {
605            return Some(cand.to_string());
606        }
607        rest = cand;
608    }
609    None
610}
611
612/// True when the `$` tail names a plain member class (not anonymous/local/
613/// lambda), i.e. the class renders inside its outer's compilation unit.
614fn clean_member_tail(rest: &str) -> bool {
615    if rest.starts_with('-') {
616        return false;
617    }
618    let tail = rest.rsplit('$').next().unwrap_or(rest);
619    if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
620        return false;
621    }
622    true
623}
624
625/// Classes to emit as their own compilation units: top-level classes plus
626/// anonymous/local/lambda-shaped ones (clean members render inline).
627impl DexPool {
628    /// Arm image retirement (full-decompile driver only): count one
629    /// pending class per image by pool ownership.
630    pub fn arm_retirement(&self) {
631        self.retire_armed
632            .store(true, std::sync::atomic::Ordering::Release);
633        let mut counts = self.retire_counts.lock().unwrap();
634        for c in counts.iter_mut() {
635            *c = 0;
636        }
637        for entry in self.classes.values() {
638            let di = match entry {
639                ClassEntry::Eager(pc) => pc.dex_idx,
640                ClassEntry::Lazy { at, .. } => at.0,
641            };
642            if let Some(c) = counts.get_mut(di) {
643                *c += 1;
644            }
645        }
646    }
647
648    /// Report one class finished; Some(image) when it was the image's
649    /// last class (driver batches these into release_images).
650    pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
651        if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
652            return None;
653        }
654        let entry = self.classes.get(class_name)?;
655        let di = match entry {
656            ClassEntry::Eager(pc) => pc.dex_idx,
657            ClassEntry::Lazy { at, .. } => at.0,
658        };
659        let mut counts = self.retire_counts.lock().unwrap();
660        match counts.get_mut(di) {
661            Some(c) => {
662                *c = c.saturating_sub(1);
663                if *c == 0 {
664                    Some(di)
665                } else {
666                    None
667                }
668            }
669            None => None,
670        }
671    }
672
673    /// Release the images' inflated bytes. Safe: the driver calls this
674    /// only when every class of each image is emitted or failed; new
675    /// `dex()` snapshots still work (tables valid, code accessors empty).
676    pub fn release_images(&self, indexes: &[usize]) {
677        let mut counts = self.retire_counts.lock().unwrap();
678        for &i in indexes {
679            if let Some(dex) = self.dexes.get(i) {
680                // Mark the shared image: code accessors go empty
681                // immediately; the bytes drop when the last snapshot
682                // drops (workers hold snapshots only mid-class).
683                dex.mark_released();
684            }
685            if let Some(c) = counts.get_mut(i) {
686                *c = u64::MAX; // released marker
687            }
688        }
689    }
690}
691
692pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
693    pool.class_names()
694        .filter(|name| match pool.outer_of(name) {
695            None => true,
696            Some(outer) => {
697                if pool.get(outer).is_none() {
698                    return true;
699                }
700                // The outer can come from an ANNOTATION (EnclosingClass)
701                // with no naming relationship to this class — obfuscated
702                // apps pair a 1-char name with a long enclosing descriptor
703                // (weixin), which made the old blind slice panic. Only a
704                // real `outer$tail` prefix yields a member tail; anything
705                // else is a standalone unit.
706                let rest = name
707                    .strip_prefix(outer)
708                    .and_then(|t| t.strip_prefix('$'))
709                    .unwrap_or("");
710                !clean_member_tail(rest)
711            }
712        })
713        .map(|n| n.to_string())
714        .collect()
715}
716
717/// Deterministic case-collision renames over the FILE-emission set:
718/// classes whose internal names differ only in letter case cannot share
719/// one case-insensitive directory; the first (sorted) member of each
720/// group keeps its name, the others gain `_2`, `_3`, … on the simple
721/// segment. The map carries identity entries for unrenamed file-level
722/// classes (they anchor nested prefix walks in apply_class_rename).
723pub fn case_rename_map(pool: &DexPool) -> std::collections::HashMap<String, String> {
724    use std::collections::HashMap;
725    let mut groups: HashMap<String, Vec<String>> = HashMap::new();
726    for t in top_level_classes(pool) {
727        groups.entry(t.to_lowercase()).or_default().push(t);
728    }
729    let folds: std::collections::HashSet<String> = groups.keys().cloned().collect();
730    let mut map = HashMap::new();
731    for (_, mut members) in groups {
732        members.sort();
733        for (i, m) in members.iter().enumerate() {
734            if i == 0 {
735                map.insert(m.clone(), m.clone());
736                continue;
737            }
738            // Suffix the simple segment; keep incrementing if the fold
739            // of the suffixed name is already taken by a real class.
740            let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
741            let mut n = i + 1;
742            loop {
743                let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
744                let fold = cand.to_lowercase();
745                if !folds.contains(&fold) {
746                    map.insert(m.clone(), cand);
747                    break;
748                }
749                n += 1;
750            }
751        }
752    }
753    map
754}
755
756/// Compute and install the registry (call before worker threads spawn).
757pub fn install_case_renames(pool: &DexPool) {
758    jdc_core::rename::set_class_renames(case_rename_map(pool));
759}
760
761pub use jdc_core::rename::apply_class_rename;