ddc_dec/lib.rs
1//! ddc-dec — DEX → Java decompiler front-end over `jdc-core`.
2//!
3//! Pipeline per method: DEX code units → CFG → per-block register→IR
4//! lifting → `jdc-core` structuring/conversion → refinement passes →
5//! emission. Class-level rendering mirrors jcdc's `classdec` but reads
6//! DEX metadata (no signatures, no generics).
7
8pub mod cfg;
9pub mod classdec;
10
11pub use classdec::ClassOptions;
12pub mod ctx;
13pub mod lift;
14pub mod method;
15pub mod passes;
16pub mod platform;
17mod refscan;
18
19use jdc_core::FxHashMap as HashMap;
20
21use ddc_dex::annotations::{self, EncodedValue};
22
23/// A resolved static-field initializer.
24#[derive(Debug, Clone)]
25pub enum StaticValue {
26 Int(i64),
27 Float(f32),
28 Double(f64),
29 Str(String),
30 Type(String),
31 Boolean(bool),
32 Null,
33 /// (declaring class, field name) — enum constants.
34 Field(String, String),
35 Other,
36}
37use ddc_dex::{ClassDef, DexFile};
38use jdc_core::types::{
39 parse_field_descriptor, parse_method_descriptor, JavaType, MethodDescriptor,
40};
41
42// ---------------------------------------------------------------------------
43// Access flags (Dalvik numbering, JVM-compatible subset).
44// ---------------------------------------------------------------------------
45pub mod access {
46 pub const ACC_PUBLIC: u32 = 0x1;
47 pub const ACC_PRIVATE: u32 = 0x2;
48 pub const ACC_PROTECTED: u32 = 0x4;
49 pub const ACC_STATIC: u32 = 0x8;
50 pub const ACC_FINAL: u32 = 0x10;
51 pub const ACC_SYNCHRONIZED: u32 = 0x20;
52 pub const ACC_BRIDGE: u32 = 0x40;
53 pub const ACC_VARARGS: u32 = 0x80;
54 pub const ACC_NATIVE: u32 = 0x100;
55 pub const ACC_INTERFACE: u32 = 0x200;
56 pub const ACC_ABSTRACT: u32 = 0x400;
57 pub const ACC_STRICT: u32 = 0x800;
58 pub const ACC_SYNTHETIC: u32 = 0x1000;
59 pub const ACC_ANNOTATION: u32 = 0x2000;
60 pub const ACC_ENUM: u32 = 0x4000;
61 pub const ACC_CONSTRUCTOR: u32 = 0x1_0000;
62 pub const ACC_DECLARED_SYNCHRONIZED: u32 = 0x2_0000;
63}
64
65/// One field of a pooled class.
66#[derive(Debug, Clone)]
67pub struct PoolField {
68 pub name: String,
69 /// Field descriptor (`I`, `Ljava/lang/String;`, ...).
70 pub desc: String,
71 pub access: u32,
72 pub is_static: bool,
73}
74
75/// One method of a pooled class.
76#[derive(Debug, Clone)]
77pub struct PoolMethod {
78 /// Shared with the dex string table (the idx IS the dedup key).
79 pub name: std::sync::Arc<str>,
80 /// Method descriptor, shared per proto (a dex's proto table is the
81 /// descriptor dedup layer — 1.9M methods on lark share ~1 proto
82 /// table's worth of unique descriptors).
83 pub desc: std::sync::Arc<str>,
84 pub access: u32,
85 pub code_off: u32,
86 pub debug_info_off: u32,
87 /// Which DEX image the body lives in.
88 pub dex_idx: usize,
89}
90
91impl PoolMethod {
92 pub fn parsed_desc(&self) -> Option<MethodDescriptor> {
93 parse_method_descriptor(&self.desc)
94 }
95 pub fn is_static(&self) -> bool {
96 self.access & access::ACC_STATIC != 0
97 }
98 pub fn is_abstract_or_native(&self) -> bool {
99 self.access & (access::ACC_ABSTRACT | access::ACC_NATIVE) != 0
100 }
101}
102
103/// One pool entry: either already built, or name-registered with its
104/// (dex index, class_def index) locator for on-demand construction.
105enum ClassEntry {
106 Eager(PoolClass),
107 Lazy {
108 at: (usize, usize),
109 pc: std::sync::OnceLock<PoolClass>,
110 },
111}
112
113/// A class materialized from one `class_def_item` (with ids resolved to
114/// names and the class data expanded).
115#[derive(Debug, Clone)]
116pub struct PoolClass {
117 pub name: String,
118 pub access: u32,
119 /// `None` for `java/lang/Object` roots.
120 pub super_name: Option<String>,
121 pub interfaces: Vec<String>,
122 pub source_file: Option<String>,
123 pub static_fields: Vec<PoolField>,
124 pub instance_fields: Vec<PoolField>,
125 pub direct_methods: Vec<PoolMethod>,
126 pub virtual_methods: Vec<PoolMethod>,
127 /// Static initial values aligned with the head of `static_fields`.
128 pub static_values: Vec<StaticValue>,
129 /// Nesting evidence with annotation ids resolved to names.
130 pub nesting: ResolvedNesting,
131 /// Which DEX image the class body lives in (provenance header).
132 pub dex_idx: usize,
133}
134
135/// Nesting evidence, names resolved at pooling time.
136#[derive(Debug, Clone, Default)]
137pub struct ResolvedNesting {
138 pub enclosing_class: Option<String>,
139 /// (class internal name, method name) of the enclosing method.
140 pub enclosing_method: Option<(String, String)>,
141 pub member_classes: Vec<String>,
142}
143
144impl PoolClass {
145 pub fn is_interface(&self) -> bool {
146 self.access & access::ACC_INTERFACE != 0
147 }
148
149 pub fn is_enum(&self) -> bool {
150 self.access & access::ACC_ENUM != 0
151 }
152
153 pub fn is_synthetic(&self) -> bool {
154 self.access & access::ACC_SYNTHETIC != 0
155 }
156
157 pub fn is_static_nested(&self) -> bool {
158 self.access & access::ACC_STATIC != 0
159 }
160
161 /// All methods in declaration order.
162 pub fn all_methods(&self) -> impl Iterator<Item = &PoolMethod> {
163 self.direct_methods
164 .iter()
165 .chain(self.virtual_methods.iter())
166 }
167
168 /// Find a method by name + descriptor.
169 pub fn find_method(&self, name: &str, desc: &str) -> Option<&PoolMethod> {
170 self.all_methods()
171 .find(|m| &*m.name == name && &*m.desc == desc)
172 }
173
174 /// Constructors `<init>` matching `arity` descriptor arguments.
175 pub fn ctors_by_arity(&self, arity: usize) -> Vec<&PoolMethod> {
176 self.all_methods()
177 .filter(|m| &*m.name == "<init>")
178 .filter(|m| {
179 m.parsed_desc()
180 .map(|d| d.args.len() == arity)
181 .unwrap_or(false)
182 })
183 .collect()
184 }
185
186 pub fn field_flags_of(&self, name: &str) -> Option<u32> {
187 self.static_fields
188 .iter()
189 .chain(self.instance_fields.iter())
190 .find(|f| f.name == name)
191 .map(|f| f.access)
192 }
193}
194
195/// Multi-DEX class pool: classes from all images, first definition wins.
196/// One facade-map entry: the public base plus, for PARTIALLY covered
197/// parts, the exact static members the base declares (None = the whole
198/// part rewrites; Some = per-member gate). Single map = one hash probe
199/// per static reference (two maps measured +2.4s on weibo).
200type FacadeTarget = (
201 String,
202 Option<
203 std::sync::Arc<(
204 jdc_core::FxHashSet<(String, String)>,
205 jdc_core::FxHashSet<String>,
206 )>,
207 >,
208);
209
210pub struct DexPool {
211 /// Images as Arcs: `dex()` hands out snapshots whose borrows stay
212 /// valid for the Arc's lifetime (decompile workers hold them), while
213 /// `release_images` empties the image data through exclusive access
214 /// when all of an image's classes are done (full-decompile driver
215 /// only — progressive callers keep images whole).
216 dexes: Vec<std::sync::Arc<DexFile>>,
217 /// Human label per image ("weibo!classes.dex") for the provenance
218 /// header; defaults to "dex N" until the CLI names the inputs.
219 pub dex_labels: Vec<String>,
220 /// Name → class entry. Eager pools materialize everything at add
221 /// time (full decompile, tests); lazy pools register names only and
222 /// materialize on first `get` (progressive `getclass` — one class's
223 /// PoolClass costs annotation reads, 145k of them cost ~0.2s).
224 classes: HashMap<String, ClassEntry>,
225 /// Class names in insertion order (stable for output).
226 pub order: Vec<String>,
227 /// Per-image remaining class count once `arm_retirement` fires.
228 retire_counts: std::sync::Mutex<Vec<u64>>,
229 retire_armed: std::sync::atomic::AtomicBool,
230 /// name → outer (computed once; `$` heuristic + dalvik annotations).
231 outers: std::sync::OnceLock<HashMap<String, Option<String>>>,
232 /// outer → direct children (computed once).
233 children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
234 /// Per-image hot-reference interning (parallel to `dexes`).
235 ref_caches: Vec<DexRefCache>,
236 /// Synthetic-static accessor code snapshots keyed by (dex_idx,
237 /// code_off). `inline_accessors` reads accessor bodies ACROSS images
238 /// during worker runs; image retirement can release those bytes
239 /// mid-run, making the inline decision — and with it var numbering
240 /// downstream — dependent on worker completion interleaving
241 /// (observed: reqable enum-constant args flipping `var0`/`var1`
242 /// between identical runs; DDC_NORETIRE=1 stable). The full-decompile
243 /// driver fills this BEFORE `arm_retirement`; progressive/lazy pools
244 /// never retire and fall through to live reads.
245 accessor_code: std::sync::OnceLock<AccessorSnapshots>,
246 /// Cached package → direct-class simple names (import-collision gate).
247 pkg_simples: std::sync::OnceLock<jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>>,
248 /// First path segments of all packages (`v2` for `v2/n`): local or
249 /// field names equal to one capture package-qualified renders
250 /// (`v2.n.a`) — deshadow_locals consults this.
251 root_segs: std::sync::OnceLock<jdc_core::FxHashSet<String>>,
252 /// Partially-covered facade parts (see kotlin_facade_partial_map).
253 /// Kotlin multi-file facade parts → public facade (see
254 /// `kotlin_facade_map`).
255 kotlin_facades: std::sync::OnceLock<jdc_core::FxHashMap<String, FacadeTarget>>,
256 /// Per class internal name: the set of `<init>` ARGUMENT descriptors
257 /// (the `(..)` inner text) REFERENCED through that class anywhere in
258 /// the pool. Dex resolves a `<init>` method-ref up the superclass
259 /// chain, so a subclass `super(..)` may target `C.<init>(sig)` that C
260 /// does NOT declare (an ancestor does) — Java has no inherited ctors,
261 /// so C needs a bridge. Computed once from the method-id tables
262 /// (parsed, survive image retirement). See `inherited_ctor_bridges`.
263 ctor_refs: std::sync::OnceLock<jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>>,
264}
265
266/// Raw code_item bytes of every synthetic-static accessor, keyed by
267/// (dex_idx, code_off) — see `snapshot_accessor_code`.
268type AccessorSnapshots = HashMap<(usize, u32), std::sync::Arc<[u8]>>;
269
270/// Lazily-filled, thread-shared reference interning for one image.
271///
272/// The lifter used to rebuild class names, field/method names and WHOLE
273/// method descriptors per INSTRUCTION (`method_ref` alone: N param
274/// Strings → `format!` descriptor → re-parse — 6-10 allocations per
275/// invoke). Every entry here is built at most once per (image, table
276/// index); lifts and the endless IR clones downstream are refcount
277/// bumps. Slots are `OnceLock` so worker threads fill them race-free
278/// without a lock on the hot path.
279pub(crate) struct DexRefCache {
280 proto_descs: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::MethodDescriptor>>]>,
281 type_names: Box<[std::sync::OnceLock<std::sync::Arc<str>>]>,
282 type_javas: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::JavaType>>]>,
283}
284
285impl DexRefCache {
286 fn new(protos: usize, types: usize) -> Self {
287 DexRefCache {
288 proto_descs: (0..protos).map(|_| std::sync::OnceLock::new()).collect(),
289 type_names: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
290 type_javas: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
291 }
292 }
293}
294
295impl DexPool {
296 pub fn new() -> Self {
297 DexPool {
298 dexes: Vec::new(),
299 retire_armed: std::sync::atomic::AtomicBool::new(false),
300 dex_labels: Vec::new(),
301 classes: HashMap::default(),
302 order: Vec::new(),
303 retire_counts: std::sync::Mutex::new(Vec::new()),
304 outers: std::sync::OnceLock::new(),
305 children: std::sync::OnceLock::new(),
306 ref_caches: Vec::new(),
307 accessor_code: std::sync::OnceLock::new(),
308 pkg_simples: std::sync::OnceLock::new(),
309 root_segs: std::sync::OnceLock::new(),
310 kotlin_facades: std::sync::OnceLock::new(),
311 ctor_refs: std::sync::OnceLock::new(),
312 }
313 }
314
315 /// Referenced `<init>` argument-descriptor set for a class internal
316 /// name (see `ctor_refs`). Builds the whole index on first call by
317 /// scanning every image's method-id table for `<init>` refs — cheap
318 /// (flat table, integer name-idx compare against the interned
319 /// "<init>" string) and done once for the whole decompile.
320 pub fn ctor_ref_sigs(&self, class: &str) -> Option<&jdc_core::FxHashSet<String>> {
321 self.ctor_refs
322 .get_or_init(|| {
323 let mut map: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
324 jdc_core::FxHashMap::default();
325 for dex in &self.dexes {
326 let n = dex.method_count();
327 for i in 0..n {
328 let m = dex.method(i as u32);
329 if dex.string(m.name_idx) != "<init>" {
330 continue;
331 }
332 let desc = dex.proto_desc(m.proto_idx);
333 let (Some(lo), Some(hi)) = (desc.find('('), desc.find(')')) else {
334 continue;
335 };
336 map.entry(dex.class_name(m.class_idx))
337 .or_default()
338 .insert(desc[lo + 1..hi].to_string());
339 }
340 }
341 map
342 })
343 .get(class)
344 }
345
346 /// Shared `MethodDescriptor` of a proto (one parse per image).
347 pub fn proto_desc_parsed(
348 &self,
349 di: usize,
350 proto_idx: u32,
351 ) -> std::sync::Arc<jdc_core::types::MethodDescriptor> {
352 let Some(dex) = self.dexes.get(di) else {
353 return std::sync::Arc::new(jdc_core::types::MethodDescriptor {
354 args: Vec::new(),
355 ret: jdc_core::types::JavaType::Void,
356 });
357 };
358 let build = || {
359 std::sync::Arc::new(parse_proto_desc(dex, proto_idx))
360 };
361 match self
362 .ref_caches
363 .get(di)
364 .and_then(|c| c.proto_descs.get(proto_idx as usize))
365 {
366 Some(slot) => slot.get_or_init(build).clone(),
367 None => build(),
368 }
369 }
370
371 /// Shared internal class name of a type id (one strip+alloc per image).
372 pub fn type_name_arc(&self, di: usize, type_idx: u32) -> std::sync::Arc<str> {
373 let Some(dex) = self.dexes.get(di) else {
374 return std::sync::Arc::from("");
375 };
376 let build = || std::sync::Arc::from(dex.class_name(type_idx).as_str());
377 match self
378 .ref_caches
379 .get(di)
380 .and_then(|c| c.type_names.get(type_idx as usize))
381 {
382 Some(slot) => slot.get_or_init(build).clone(),
383 None => build(),
384 }
385 }
386
387 /// Shared parsed type of a type id.
388 pub fn type_java(&self, di: usize, type_idx: u32) -> std::sync::Arc<jdc_core::types::JavaType> {
389 let Some(dex) = self.dexes.get(di) else {
390 return std::sync::Arc::new(jdc_core::types::JavaType::Object(
391 std::sync::Arc::from("java/lang/Object"),
392 ));
393 };
394 let build = || std::sync::Arc::new(desc_type(dex.type_name(type_idx)));
395 match self
396 .ref_caches
397 .get(di)
398 .and_then(|c| c.type_javas.get(type_idx as usize))
399 {
400 Some(slot) => slot.get_or_init(build).clone(),
401 None => build(),
402 }
403 }
404
405 /// name → outer map, computed lazily once.
406 fn outer_map(&self) -> &HashMap<String, Option<String>> {
407 self.outers.get_or_init(|| {
408 self.order
409 .iter()
410 .map(|n| (n.clone(), find_outer_name(self, n)))
411 .collect()
412 })
413 }
414
415 /// The outer class of `name`, from the cached map (borrowed).
416 pub fn outer_of(&self, name: &str) -> Option<&str> {
417 self.outer_map().get(name).and_then(|o| o.as_deref())
418 }
419
420 /// Direct nested children of `internal` (any `$` depth 1), cached.
421 /// (Perf: returns a borrowed slice — the owned-Vec clone ran once per
422 /// class on 98k-class runs.)
423 pub fn children_of(&self, internal: &str) -> &[String] {
424 if self.children.get().is_none() {
425 // Build FIRST, set ONCE: the previous shape set an empty map
426 // to claim the OnceLock and then silently failed to store the
427 // real index (`let _ = set(...)` on an initialized lock) —
428 // children_of returned [] forever, so member nested classes
429 // were neither inlined nor emitted as files (Guard$Report
430 // vanished whole; every corpus run since the borrow refactor
431 // dropped them, invisible to the syntax-only gate).
432 let mut idx: HashMap<String, Vec<String>> = HashMap::default();
433 for name in &self.order {
434 if let Some(outer) = self.outer_of(name).map(str::to_string) {
435 idx.entry(outer).or_default().push(name.clone());
436 }
437 }
438 // A racing thread may have set an identical map first — the
439 // index is a pure function of `order`, so either copy is right.
440 let _ = self.children.set(idx);
441 }
442 self.children
443 .get()
444 .and_then(|m| m.get(internal))
445 .map(|v| v.as_slice())
446 .unwrap_or(&[])
447 }
448
449 /// Adds one image; duplicate class names keep their first definition.
450 /// Returns the image's index — call `set_dex_label` with it to give the
451 /// provenance header a real origin instead of the default "dex N".
452 pub fn add_dex(&mut self, dex: DexFile) -> usize {
453 let dex_idx = self.dexes.len();
454 // Pre-size the name map: 240k-entry growth rehashed repeatedly
455 // (reserve_rehash showed up in corpus profiles).
456 self.classes.reserve(dex.class_defs.len());
457 // The annotation reader borrows the image; pool classes are built
458 // before ownership moves into `self.dexes` (no full-image copy).
459 for cd in &dex.class_defs {
460 let name = dex.class_name(cd.class_idx);
461 if self.classes.contains_key(&name) {
462 continue;
463 }
464 let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
465 self.classes.insert(name.clone(), ClassEntry::Eager(pc));
466 self.order.push(name);
467 }
468 self.ref_caches
469 .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
470 self.dexes.push(std::sync::Arc::new(dex));
471 self.retire_counts.lock().unwrap().push(0);
472 self.dex_labels.push(format!("dex {}", dex_idx));
473 dex_idx
474 }
475
476 /// Registers class NAMES only; the PoolClass (annotation reads
477 /// included) is materialized on first `get`. Duplicate class names
478 /// keep their first definition, matching `add_dex`.
479 pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
480 let dex_idx = self.dexes.len();
481 self.classes.reserve(dex.class_defs.len());
482 for (ci, cd) in dex.class_defs.iter().enumerate() {
483 let name = dex.class_name(cd.class_idx);
484 if self.classes.contains_key(&name) {
485 continue;
486 }
487 self.classes.insert(
488 name.clone(),
489 ClassEntry::Lazy {
490 at: (dex_idx, ci),
491 pc: std::sync::OnceLock::new(),
492 },
493 );
494 self.order.push(name);
495 }
496 self.ref_caches
497 .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
498 self.dexes.push(std::sync::Arc::new(dex));
499 self.retire_counts.lock().unwrap().push(0);
500 self.dex_labels.push(format!("dex {}", dex_idx));
501 dex_idx
502 }
503
504 /// Label the image at `idx` (see `add_dex`).
505 pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
506 if let Some(slot) = self.dex_labels.get_mut(idx) {
507 *slot = label.into();
508 }
509 }
510
511 pub fn len(&self) -> usize {
512 self.classes.len()
513 }
514
515 pub fn is_empty(&self) -> bool {
516 self.classes.is_empty()
517 }
518
519 pub fn get(&self, internal: &str) -> Option<&PoolClass> {
520 self.get_inner(internal, None)
521 }
522
523 /// `get` with optional pre-taken raw-image snapshots (one per
524 /// image). `materialize_all` hoists the `DexFile::raw()` lock out of
525 /// the per-class loop — the parallel materialize used to take the
526 /// same per-image lock once PER CLASS across all threads.
527 fn get_inner(&self, internal: &str, raws: Option<&[&[u8]]>) -> Option<&PoolClass> {
528 match self.classes.get(internal)? {
529 ClassEntry::Eager(pc) => Some(pc),
530 ClassEntry::Lazy { at, pc } => {
531 if let Some(built) = pc.get() {
532 return Some(built);
533 }
534 let (di, ci) = *at;
535 let dex = self.dexes.get(di)?;
536 let cd = dex.class_defs.get(ci)?;
537 let raw = match raws {
538 Some(r) => r.get(di).copied().unwrap_or(&[]),
539 None => dex.raw(),
540 };
541 let built = pool_class_of(dex, raw, cd, di);
542 Some(pc.get_or_init(move || built))
543 }
544 }
545 }
546
547 /// Materialize every lazy entry (parallel across the given chunk
548 /// count). After this the pool is observationally identical to an
549 /// eagerly-built one — including the annotation-aware outer-map —
550 /// while the annotation reads ran on worker threads instead of the
551 /// serial pool build (~0.2s on weibo).
552 pub fn materialize_all(&self, threads: usize) {
553 let keys: Vec<String> = self
554 .classes
555 .iter()
556 .filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
557 .map(|(k, _)| k.clone())
558 .collect();
559 if keys.is_empty() {
560 return;
561 }
562 // One raw() snapshot per image for the whole sweep (see
563 // get_inner). Safe window: materialize_all completes before
564 // retirement is armed, so no image releases while these slices
565 // are alive.
566 let raws: Vec<&[u8]> = self.dexes.iter().map(|d| d.raw()).collect();
567 let threads = threads.max(1);
568 let next = std::sync::atomic::AtomicUsize::new(0);
569 std::thread::scope(|scope| {
570 for _ in 0..threads {
571 scope.spawn(|| loop {
572 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
573 let Some(k) = keys.get(i) else { break };
574 let _ = self.get_inner(k, Some(&raws));
575 });
576 }
577 });
578 }
579
580 /// `get` without materializing: None for not-yet-built lazy entries.
581 fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
582 match self.classes.get(internal)? {
583 ClassEntry::Eager(pc) => Some(pc),
584 ClassEntry::Lazy { pc, .. } => pc.get(),
585 }
586 }
587
588 /// Name registered (no materialization).
589 pub fn has_name(&self, internal: &str) -> bool {
590 self.classes.contains_key(internal)
591 }
592
593 /// One image as an Arc snapshot (borrows of the snapshot live as
594 /// long as the caller holds the Arc).
595 pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
596 self.dexes.get(idx).cloned()
597 }
598
599 pub fn dex_count(&self) -> usize {
600 self.dexes.len()
601 }
602
603 /// Raw image list (install-phase whole-program scans).
604 pub fn dexes(&self) -> &[std::sync::Arc<ddc_dex::DexFile>] {
605 &self.dexes
606 }
607
608 /// First path segments of every package in the pool (cached once).
609 pub fn root_pkg_segs(&self) -> &jdc_core::FxHashSet<String> {
610 self.root_segs.get_or_init(|| {
611 self.order
612 .iter()
613 .filter_map(|n| n.find('/').map(|i| n[..i].to_string()))
614 .collect()
615 })
616 }
617
618 /// Package → simple names of its direct classes, cached once per
619 /// pool (the import-collision gate consults it per class — the
620 /// uncached scan was O(classes²) and 10×'d weixin's wall time).
621 pub fn package_simples(&self) -> &jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> {
622 self.pkg_simples.get_or_init(|| {
623 let mut m: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
624 jdc_core::FxHashMap::default();
625 for n in &self.order {
626 if let Some((pkg, simple)) = n.rsplit_once('/') {
627 m.entry(pkg.to_string())
628 .or_default()
629 .insert(simple.to_string());
630 }
631 }
632 m
633 })
634 }
635
636 /// Kotlin multi-file class facades: the compiler splits a file facade
637 /// (`kotlin.text.StringsKt`) into package-private parts
638 /// (`StringsKt__StringsKt`, `StringsKt__StringsJVMKt`) and call sites
639 /// target the PARTS directly. Java cannot access a package-private
640 /// class cross-package ("StringsKt__StringsKt在kotlin.text中不是公共的"
641 /// — 1.4k lark root errors), but the public facade EXTENDS the parts,
642 /// so static members resolve through it: rewrite the reference's owner
643 /// class to the facade. part → base, gated on base existing, being
644 /// public, and transitively extending the part (static inheritance is
645 /// the resolution guarantee).
646 pub fn kotlin_facade_map(&self) -> &jdc_core::FxHashMap<String, FacadeTarget> {
647 self.kotlin_facades.get_or_init(|| {
648 let t_fb = std::time::Instant::now();
649 let mut m: jdc_core::FxHashMap<String, FacadeTarget> =
650 jdc_core::FxHashMap::default();
651 let mut npartial = 0usize;
652 for name in &self.order {
653 let Some(i) = name.find("__") else { continue };
654 let base = &name[..i];
655 if base.is_empty() {
656 continue;
657 }
658 let Some(basec) = self.get(base) else {
659 continue;
660 };
661 if basec.access & crate::access::ACC_PUBLIC == 0 {
662 continue;
663 }
664 // base must transitively extend the part...
665 let mut cur = basec.super_name.clone();
666 let mut hops = 0u32;
667 let mut reaches = false;
668 while let Some(c) = cur {
669 if c == *name {
670 reaches = true;
671 break;
672 }
673 if hops >= 8 {
674 break;
675 }
676 hops += 1;
677 cur = self.get(&c).and_then(|cc| cc.super_name.clone());
678 }
679 // ...OR cover the part's static members: the Kotlin
680 // compiler MERGES every part's statics into the public
681 // facade, but the facade's super chain only includes ONE
682 // part — lark's `CollectionsKt extends
683 // CollectionsKt___CollectionsKt` leaves
684 // `CollectionsKt__CollectionsJVMKt` uncovered (463
685 // 不是公共的 errors). Member coverage (name+desc for
686 // methods, name for fields) is the precise rewrite
687 // condition: every reference through the facade then
688 // resolves exactly as through the part.
689 if reaches {
690 m.insert(name.clone(), (base.to_string(), None));
691 continue;
692 }
693 // Member-coverage fallback: the Kotlin compiler MERGES
694 // the parts' statics into the public facade, but R8 can
695 // sever the super chain (lark: `CollectionsKt extends
696 // CollectionsKt___CollectionsKt extends
697 // kotlin/collections/s` — `__CollectionsJVMKt` is off
698 // the chain, 463 不是公共的 errors) and may drop facade
699 // members no direct facade call needed. Map at (name,
700 // desc) granularity then: covered members rewrite, the
701 // rest keep the part owner (a latent error either way —
702 // Java cannot access the package-private part).
703 let Some(part) = self.get(name) else {
704 continue;
705 };
706 let base_methods: jdc_core::FxHashSet<(&str, &str)> = basec
707 .direct_methods
708 .iter()
709 .chain(basec.virtual_methods.iter())
710 .map(|mm| (&*mm.name, &*mm.desc))
711 .collect();
712 let base_fields: jdc_core::FxHashSet<&str> =
713 basec.static_fields.iter().map(|f| &*f.name).collect();
714 let mut methods: jdc_core::FxHashSet<(String, String)> =
715 jdc_core::FxHashSet::default();
716 let mut fields: jdc_core::FxHashSet<String> =
717 jdc_core::FxHashSet::default();
718 let mut all = true;
719 for mm in part
720 .direct_methods
721 .iter()
722 .chain(part.virtual_methods.iter())
723 .filter(|mm| mm.access & crate::access::ACC_STATIC != 0)
724 {
725 if base_methods.contains(&(&*mm.name, &*mm.desc)) {
726 methods.insert((mm.name.to_string(), mm.desc.to_string()));
727 } else {
728 all = false;
729 }
730 }
731 for pf in &part.static_fields {
732 if base_fields.contains(&*pf.name) {
733 fields.insert(pf.name.to_string());
734 } else {
735 all = false;
736 }
737 }
738 if all {
739 m.insert(name.clone(), (base.to_string(), None));
740 } else if !methods.is_empty() || !fields.is_empty() {
741 npartial += 1;
742 m.insert(
743 name.clone(),
744 (base.to_string(), Some(std::sync::Arc::new((methods, fields)))),
745 );
746 }
747 }
748 if std::env::var("DDC_STATS").is_ok() {
749 eprintln!(
750 "[renames] facade map: entries={} partial={} build={:?}",
751 m.len(),
752 npartial,
753 t_fb.elapsed()
754 );
755 }
756 m
757 })
758 }
759
760 pub fn class_names(&self) -> impl Iterator<Item = &str> {
761 self.order.iter().map(|s| s.as_str())
762 }
763
764 /// True when `sub` is assignable to `sup` (internal names), walking the
765 /// pool's hierarchy. Unknown classes are never assignable (conservative).
766 pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
767 if sub == sup {
768 return true;
769 }
770 // Arrays: covariance by element type when both are arrays.
771 if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
772 if sub_elem.starts_with('L')
773 && sup_elem.starts_with('L')
774 && sub_elem.ends_with(';')
775 && sup_elem.ends_with(';')
776 {
777 return self.is_subtype(
778 &sub_elem[1..sub_elem.len() - 1],
779 &sup_elem[1..sup_elem.len() - 1],
780 );
781 }
782 return sub_elem == sup_elem;
783 }
784 let mut cur = self.get(sub);
785 let mut hops = 0;
786 while let Some(c) = cur {
787 hops += 1;
788 if hops > 64 {
789 return false;
790 }
791 for i in &c.interfaces {
792 if i == sup || self.is_subtype(i, sup) {
793 return true;
794 }
795 }
796 match &c.super_name {
797 Some(s) if s == sup => return true,
798 Some(s) if s != "java/lang/Object" => {
799 cur = self.get(s);
800 }
801 _ => return false,
802 }
803 }
804 false
805 }
806}
807
808impl Default for DexPool {
809 fn default() -> Self {
810 Self::new()
811 }
812}
813
814fn array_elem(desc: &str) -> Option<&str> {
815 desc.strip_prefix('[')
816}
817
818fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
819 match v {
820 EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
821 EncodedValue::Short(x) => StaticValue::Int(*x as i64),
822 EncodedValue::Char(x) => StaticValue::Int(*x as i64),
823 EncodedValue::Int(x) => StaticValue::Int(*x as i64),
824 EncodedValue::Long(x) => StaticValue::Int(*x),
825 EncodedValue::Float(x) => StaticValue::Float(*x),
826 EncodedValue::Double(x) => StaticValue::Double(*x),
827 EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
828 EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
829 EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
830 EncodedValue::Null => StaticValue::Null,
831 EncodedValue::Enum(i) | EncodedValue::Field(i) => {
832 let f = dex.field(*i);
833 StaticValue::Field(
834 dex.class_name(f.class_idx),
835 dex.string(f.name_idx).to_string(),
836 )
837 }
838 _ => StaticValue::Other,
839 }
840}
841
842fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
843 let name = dex.class_name(cd.class_idx);
844 let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
845 None
846 } else {
847 let s = dex.class_name(cd.superclass_idx);
848 if s.is_empty() || s == "java/lang/Object" {
849 None
850 } else {
851 Some(s)
852 }
853 };
854 let interfaces = dex
855 .interfaces_of(cd)
856 .into_iter()
857 .map(|t| dex.class_name(t))
858 .collect();
859 let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
860 None
861 } else {
862 Some(dex.string(cd.source_file_idx).to_string())
863 };
864
865 let data = dex.class_data(cd);
866 let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
867 let f = dex.field(ef.field_idx);
868 PoolField {
869 name: dex.string(f.name_idx).to_string(),
870 desc: dex.type_name(f.type_idx).to_string(),
871 access: ef.access_flags,
872 is_static,
873 }
874 };
875 let static_fields: Vec<PoolField> = data
876 .static_fields
877 .iter()
878 .map(|f| mk_field(f, true))
879 .collect();
880 let instance_fields: Vec<PoolField> = data
881 .instance_fields
882 .iter()
883 .map(|f| mk_field(f, false))
884 .collect();
885 let mk_method = |em: &ddc_dex::EncodedMethod| {
886 let m = dex.method(em.method_idx);
887 let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
888 Some(d) => (em.code_off, d),
889 None => (0, 0),
890 };
891 PoolMethod {
892 name: dex.string_arc(m.name_idx),
893 desc: dex.proto_desc(m.proto_idx),
894 access: em.access_flags,
895 code_off,
896 debug_info_off,
897 dex_idx,
898 }
899 };
900 let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
901 let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
902
903 let static_values: Vec<StaticValue> = dex
904 .static_values(cd.static_values_off)
905 .into_iter()
906 .map(|v| resolve_static_value(&v, dex))
907 .collect();
908 let anns = if cd.annotations_off != 0 {
909 annotations::read_class_annotations(raw, cd.annotations_off)
910 } else {
911 Vec::new()
912 };
913 let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
914 let nesting = ResolvedNesting {
915 enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
916 enclosing_method: raw.enclosing_method.map(|m| {
917 let mid = dex.method(m);
918 (
919 dex.class_name(mid.class_idx),
920 dex.string(mid.name_idx).to_string(),
921 )
922 }),
923 member_classes: raw
924 .member_classes
925 .into_iter()
926 .map(|t| dex.class_name(t))
927 .collect(),
928 };
929
930 PoolClass {
931 name,
932 access: cd.access_flags,
933 super_name,
934 interfaces,
935 source_file,
936 static_fields,
937 instance_fields,
938 direct_methods,
939 virtual_methods,
940 static_values,
941 nesting,
942 dex_idx,
943 }
944}
945
946/// JavaType for a field/method descriptor segment.
947/// Parse a proto's descriptor into a MethodDescriptor straight from the
948/// tables (no string round-trip).
949fn parse_proto_desc(
950 dex: &ddc_dex::DexFile,
951 proto_idx: u32,
952) -> jdc_core::types::MethodDescriptor {
953 let proto = dex.proto(proto_idx);
954 let args = dex
955 .proto_params(proto_idx)
956 .iter()
957 .map(|&t| desc_type(dex.type_name(t)))
958 .collect();
959 jdc_core::types::MethodDescriptor {
960 args,
961 ret: desc_type(dex.type_name(proto.return_type_idx)),
962 }
963}
964
965pub fn desc_type(desc: &str) -> JavaType {
966 parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
967}
968
969/// Nesting evidence for `internal`: dalvik annotations first, then the
970/// `$`-name heuristic against the pool.
971pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
972 // Annotation refinement only for ALREADY-MATERIALIZED classes: eager
973 // pools (full decompile) behave exactly as before; lazy pools
974 // (progressive getclass) fall straight to the `$` chain without
975 // materializing the world.
976 if let Some(pc) = pool.get_if_materialized(internal) {
977 if let Some(enc) = &pc.nesting.enclosing_class {
978 // The annotation outer must still EXIST: R8 deletes whole
979 // outer classes and leaves the nestlings as flat `$` files
980 // (weixin matrix JiffiesMonitorFeature deleted,
981 // $JiffiesSnapshot survives) — an unresolved annotation
982 // outer made refs render dotted THROUGH a missing type
983 // ("是在不可访问的类或接口中定义的"). Fall through to the
984 // existence-checked `$` walk.
985 if pool.has_name(enc) {
986 return Some(enc.clone());
987 }
988 }
989 }
990 let mut rest = internal;
991 while let Some(d) = rest.rfind('$') {
992 let cand = &rest[..d];
993 if pool.has_name(cand) {
994 return Some(cand.to_string());
995 }
996 rest = cand;
997 }
998 None
999}
1000
1001/// True when the `$` tail names a plain member class (not anonymous/local/
1002/// lambda), i.e. the class renders inside its outer's compilation unit.
1003fn clean_member_tail(rest: &str) -> bool {
1004 if rest.starts_with('-') {
1005 return false;
1006 }
1007 let tail = rest.rsplit('$').next().unwrap_or(rest);
1008 if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
1009 return false;
1010 }
1011 true
1012}
1013
1014/// Classes to emit as their own compilation units: top-level classes plus
1015/// anonymous/local/lambda-shaped ones (clean members render inline).
1016impl DexPool {
1017 /// Arm image retirement (full-decompile driver only): count one
1018 /// pending class per image by pool ownership.
1019 /// Snapshot every synthetic-static accessor body (the exact predicate
1020 /// `inline_accessors` uses) as RAW code_item bytes, so cross-image
1021 /// accessor reads during worker runs survive image retirement.
1022 /// Call AFTER materialization and BEFORE `arm_retirement`; idempotent
1023 /// (first fill wins). Raw memcpy, no decode — accessors are numerous
1024 /// (weixin ~100k) but only a fraction are ever inlined, so decoding
1025 /// stays lazy at the use site. The slice bound covers the fixed
1026 /// header + insns + try items with generous handler slack (accessor
1027 /// tries are vanishingly rare; the snapshot is a deterministic
1028 /// function of the image either way).
1029 pub fn snapshot_accessor_code(&self) {
1030 let mut snap: AccessorSnapshots = HashMap::default();
1031 let mut bytes_total = 0usize;
1032 for name in &self.order {
1033 let Some(pc) = self.get_if_materialized(name) else {
1034 continue;
1035 };
1036 for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
1037 if m.code_off == 0
1038 || !m.is_static()
1039 || m.access & crate::access::ACC_SYNTHETIC == 0
1040 {
1041 continue;
1042 }
1043 if snap.contains_key(&(m.dex_idx, m.code_off)) {
1044 continue;
1045 }
1046 let Some(dex) = self.dex(m.dex_idx) else {
1047 continue;
1048 };
1049 let raw = dex.raw();
1050 let off = m.code_off as usize;
1051 if off + 16 > raw.len() {
1052 continue;
1053 }
1054 let u2 = |o: usize| u16::from_le_bytes([raw[o], raw[o + 1]]) as usize;
1055 let u4 = |o: usize| {
1056 u32::from_le_bytes([raw[o], raw[o + 1], raw[o + 2], raw[o + 3]]) as usize
1057 };
1058 let tries = u2(off + 6);
1059 let insns = u4(off + 12);
1060 let mut end = off + 16 + 2 * insns;
1061 if tries > 0 {
1062 // pad to 4-align + try_item[tries] + handler blobs
1063 // (uleb-coded; slack covers realistic accessor shapes).
1064 end = end.next_multiple_of(4) + 8 * tries + 256 + 64 * tries;
1065 }
1066 let end = end.min(raw.len());
1067 let slice: std::sync::Arc<[u8]> = raw[off..end].into();
1068 bytes_total += slice.len();
1069 snap.insert((m.dex_idx, m.code_off), slice);
1070 }
1071 }
1072 if std::env::var("DDC_WALL").is_ok() {
1073 eprintln!(
1074 "[wall] accessor snapshot: {} entries, {} KB",
1075 snap.len(),
1076 bytes_total / 1024
1077 );
1078 }
1079 let _ = self.accessor_code.set(snap);
1080 }
1081
1082 /// Snapshotted accessor code_item bytes, when the snapshot was taken.
1083 /// Parse with `CodeItem::parse(&bytes, 0)`.
1084 pub fn accessor_code(&self, dex_idx: usize, code_off: u32) -> Option<std::sync::Arc<[u8]>> {
1085 self.accessor_code.get()?.get(&(dex_idx, code_off)).cloned()
1086 }
1087
1088 pub fn arm_retirement(&self) {
1089 self.retire_armed
1090 .store(true, std::sync::atomic::Ordering::Release);
1091 let mut counts = self.retire_counts.lock().unwrap();
1092 for c in counts.iter_mut() {
1093 *c = 0;
1094 }
1095 for entry in self.classes.values() {
1096 let di = match entry {
1097 ClassEntry::Eager(pc) => pc.dex_idx,
1098 ClassEntry::Lazy { at, .. } => at.0,
1099 };
1100 if let Some(c) = counts.get_mut(di) {
1101 *c += 1;
1102 }
1103 }
1104 }
1105
1106 /// Report one class finished; Some(image) when it was the image's
1107 /// last class (driver batches these into release_images).
1108 pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
1109 if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
1110 return None;
1111 }
1112 let entry = self.classes.get(class_name)?;
1113 let di = match entry {
1114 ClassEntry::Eager(pc) => pc.dex_idx,
1115 ClassEntry::Lazy { at, .. } => at.0,
1116 };
1117 let mut counts = self.retire_counts.lock().unwrap();
1118 match counts.get_mut(di) {
1119 Some(c) => {
1120 *c = c.saturating_sub(1);
1121 if *c == 0 {
1122 Some(di)
1123 } else {
1124 None
1125 }
1126 }
1127 None => None,
1128 }
1129 }
1130
1131 /// Release the images' inflated bytes. Safe: the driver calls this
1132 /// only when every class of each image is emitted or failed; new
1133 /// `dex()` snapshots still work (tables valid, code accessors empty).
1134 pub fn release_images(&self, indexes: &[usize]) {
1135 let mut counts = self.retire_counts.lock().unwrap();
1136 for &i in indexes {
1137 if let Some(dex) = self.dexes.get(i) {
1138 // Mark the shared image: code accessors go empty
1139 // immediately; the bytes drop when the last snapshot
1140 // drops (workers hold snapshots only mid-class).
1141 dex.mark_released();
1142 }
1143 if let Some(c) = counts.get_mut(i) {
1144 *c = u64::MAX; // released marker
1145 }
1146 }
1147 }
1148}
1149
1150pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
1151 pool.class_names()
1152 .filter(|name| match pool.outer_of(name) {
1153 None => true,
1154 Some(outer) => {
1155 if pool.get(outer).is_none() {
1156 return true;
1157 }
1158 // The outer can come from an ANNOTATION (EnclosingClass)
1159 // with no naming relationship to this class — obfuscated
1160 // apps pair a 1-char name with a long enclosing descriptor
1161 // (weixin), which made the old blind slice panic. Only a
1162 // real `outer$tail` prefix yields a member tail; anything
1163 // else is a standalone unit.
1164 let rest = name
1165 .strip_prefix(outer)
1166 .and_then(|t| t.strip_prefix('$'))
1167 .unwrap_or("");
1168 !clean_member_tail(rest)
1169 }
1170 })
1171 .map(|n| n.to_string())
1172 .collect()
1173}
1174
1175/// Deterministic case-collision renames over the FILE-emission set:
1176/// classes whose internal names differ only in letter case cannot share
1177/// one case-insensitive directory; the first (sorted) member of each
1178/// group keeps its name, the others gain `_2`, `_3`, … on the simple
1179/// segment. The map carries identity entries for unrenamed file-level
1180/// classes (they anchor nested prefix walks in apply_class_rename).
1181pub fn case_rename_map(pool: &DexPool) -> HashMap<String, String> {
1182 use std::collections::HashMap;
1183 let mut groups: HashMap<String, Vec<String>> = HashMap::default();
1184 for t in top_level_classes(pool) {
1185 groups.entry(t.to_lowercase()).or_default().push(t);
1186 }
1187 let folds: std::collections::HashSet<String> = groups.keys().cloned().collect();
1188 let mut map = HashMap::default();
1189 for (_, mut members) in groups {
1190 members.sort();
1191 for (i, m) in members.iter().enumerate() {
1192 if i == 0 {
1193 map.insert(m.clone(), m.clone());
1194 continue;
1195 }
1196 // Suffix the simple segment; keep incrementing if the fold
1197 // of the suffixed name is already taken by a real class.
1198 let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
1199 let mut n = i + 1;
1200 loop {
1201 let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
1202 let fold = cand.to_lowercase();
1203 if !folds.contains(&fold) {
1204 map.insert(m.clone(), cand);
1205 break;
1206 }
1207 n += 1;
1208 }
1209 }
1210 }
1211 map
1212}
1213
1214/// Display-level renames for two nested-class shapes that render
1215/// uncompilable or inconsistent output:
1216///
1217/// 1. A member class whose simple tail equals its rendered parent's
1218/// tail (`x0$a$a` inside `x0$a`): javac rejects a member class with
1219/// the same simple name as its immediately enclosing class ("已在类
1220/// x0中定义了类 x0.a") — a shape Kotlin lambda families hit by the
1221/// thousand (lark alone: 22k). Bump the tail: `a` → `a2`, `a3`, …
1222/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent from the
1223/// pool): the member DECLARES as `b` inside `x0` but references
1224/// printed `x0.a$b` — a type that exists nowhere. Flatten the
1225/// display name to `x0$b` so every site agrees.
1226///
1227/// Both are pure display renames keyed by internal name; declarations,
1228/// ctor names, file names and every type reference funnel through
1229/// apply_class_rename, so one map keeps all sites consistent. Children
1230/// Display-level renames for nested member classes that render
1231/// uncompilable or inconsistent output:
1232///
1233/// 1. A member whose simple tail equals ANY enclosing class's simple
1234/// name in its nesting chain. javac rejects more than the immediate
1235/// parent (`class a { static class a2 { static class a {} } }` —
1236/// member `a` two levels under `a` — is "已在类 X中定义了类 X.a");
1237/// Kotlin lambda families (`x0$a$a` inside `x0$a`) hit it by the
1238/// thousand (lark alone: 22k). Bump the tail until it clears every
1239/// ancestor name: `a` → `a3`.
1240/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent): the
1241/// member DECLARES as `b` inside `x0` but references printed
1242/// `x0.a$b` — a type that exists nowhere. Flatten the display to
1243/// `x0$b` so every site agrees.
1244///
1245/// Candidates build on the parent's DISPLAY name (ancestors are
1246/// renamed first — ascending `$`-depth order), so declarations,
1247/// constructor names, file names and every type reference stay
1248/// consistent through apply_class_rename. All are pure display
1249/// renames keyed by internal name.
1250fn nested_collision_renames(
1251 pool: &DexPool,
1252 map: &mut HashMap<String, String>,
1253 fam_segs: &SegMap,
1254) {
1255 // Ancestors before descendants: a parent's rename must be in the
1256 // map when its children compute their display chains.
1257 let mut names: Vec<&String> = pool.order.iter().collect();
1258 names.sort_by_key(|n| n.matches('$').count());
1259 let mut assigned: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1260 // Top-level package segments. A nested member type DISPLAYED as `a2`
1261 // shadows package `a2` for every qualified `a2.x` reference in its
1262 // file (member types outrank package names in class scope), so a
1263 // renamed tail must not become a package shadow — round 47's failure
1264 // mode at nested level: candidate `a2` (for j2/g$a) broke the SAME
1265 // file's `extends a2.a` ("找不到符号 类 a" + a 9450-error Object
1266 // cascade as every dependent of the corrupt head failed too).
1267 let mut pkg_segments: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1268 for n in &pool.order {
1269 if let Some((seg, _)) = n.split_once('/') {
1270 pkg_segments.insert(seg.to_string());
1271 }
1272 }
1273 // LOCAL-OK gate (round-59 Design B): a field-obscuring rename may
1274 // only fire when every class referencing the candidate lives inside
1275 // the root file's family, so the rename perturbs exactly ONE rendered
1276 // file. Ungated (Design A) the rule netted reqable −1653 / lark −5221
1277 // but weibo +36916: renames there flipped ambiguous type-vs-package
1278 // resolutions across thousands of pre-existing conflict families and
1279 // javac's error recovery turned "missing supertype, body suppressed"
1280 // files into full Object cascades. Lazy (progressive-browse) pools
1281 // skip the gate AND the rule — the multi-second image scan must not
1282 // stall single-class queries; the full-decompile pipeline always
1283 // materializes everything before installing renames.
1284 // Round-59 Design B stands: ungated field-clash renames were
1285 // retried AFTER the package-obscuring rules landed (weibo gate,
1286 // DDC_FC_UNGATED A/B) and still exploded to the 500k error cap —
1287 // fifth falsification of the ungated rename route.
1288 let local_ok: jdc_core::FxHashSet<String> = if pool_majority_materialized(pool) {
1289 refscan::local_ok(&pool.dexes, &field_clash_cands(pool, map))
1290 } else {
1291 jdc_core::FxHashSet::default()
1292 };
1293 let mut taken_sibling_displays: jdc_core::FxHashSet<String> =
1294 map.values().cloned().collect();
1295 for name in names {
1296 if !name.contains('$') || map.contains_key(name) {
1297 continue;
1298 }
1299 let Some(parent) = find_outer_name(pool, name) else {
1300 continue;
1301 };
1302 // Only a REAL `parent$rest` name renders as an inline member;
1303 // anonymous/local/lambda tails keep their flat own-file names.
1304 let Some(rest) = name
1305 .strip_prefix(parent.as_str())
1306 .and_then(|t| t.strip_prefix('$'))
1307 else {
1308 continue;
1309 };
1310 if !clean_member_tail(rest) {
1311 continue;
1312 }
1313 // Display simple names of every enclosing level, bottom-up; `cur`
1314 // ends at the ROOT (the top-level class whose file renders this
1315 // nested type — the scope the renamed tail lives in).
1316 let mut chain: Vec<String> = Vec::new();
1317 let mut cur = parent.clone();
1318 loop {
1319 let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
1320 let simple = disp.rsplit('/').next().unwrap_or(&disp);
1321 chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
1322 match find_outer_name(pool, &cur) {
1323 Some(o) if o != cur => cur = o,
1324 _ => break,
1325 }
1326 }
1327 let root: &str = &cur;
1328 let disp_parent = map
1329 .get(&parent)
1330 .cloned()
1331 .unwrap_or_else(|| parent.clone());
1332 let tail = rest.rsplit('$').next().unwrap_or(rest);
1333 let orphan = rest.contains('$');
1334 // Field obscuring (JLS 6.4.2): a nested type whose simple name
1335 // equals an enclosing-class field is OBSCURED by it in a
1336 // `<Parent>.<tail>` qualified reference — `io/flutter/view/g$i`
1337 // (a `static enum i`) renders `g.i.t`, but g also declares an
1338 // instance field `i`, so javac resolves `g.i` to the field:
1339 // "无法从静态上下文中引用非静态 变量 i" (975 of 1140 reqable
1340 // static-context errors). The fix renames the NESTED TYPE (the
1341 // registry carries every render path: shorten, inner_simple —
1342 // whose bypass was the round-55 blowup, fixed in jdc-core —
1343 // classdec declarations), gated to LOCAL-OK candidates by the
1344 // refscan above. PoC: in-file rename of flutter g's `enum i`
1345 // took the class from 2452 errors to 1.
1346 let field_clash = local_ok.contains(name.as_str());
1347 // Package shadow (JLS 6.4.2 at nested level): a nested type
1348 // DISPLAYED with a simple name equal to the first segment of a
1349 // package its FAMILY references binds every `s.x` qualified ref
1350 // in the root file to the member type — the family's own outer
1351 // rename can EXPOSE this (e/b → e/b2 un-collided the nested `b`,
1352 // which then hijacked `b.b2` — 循环继承). The round-47 candidate
1353 // guard only kept RENAMED tails off package segments; pre-
1354 // existing shadows need the rename too, family-gated: fire only
1355 // when this family's own descriptors reference the segment.
1356 // Orphans INCLUDED: an `Outer$$x` orphan normalizes to the
1357 // nested display `x` (k=1 invisible rename), and a clean tail
1358 // renders BARE in the family file — same package shadow as a
1359 // true nested (weixin li/u0$$r → `class r` hijacked `r.a` for
1360 // root package r).
1361 let pkg_shadow = fam_segs
1362 .get(root)
1363 .is_some_and(|segs| segs.contains(tail));
1364 let clash = chain.iter().any(|c| c == tail) || field_clash || pkg_shadow;
1365 if !orphan && !clash {
1366 continue;
1367 }
1368 // The enclosing class's field names (sanitized like the renderer)
1369 // — only needed once a rename actually fires, to keep the new
1370 // tail from being obscured again. Computed lazily so progressive
1371 // pools don't materialize parents for rules that don't rename.
1372 let mut enc_fields: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1373 if field_clash || pkg_shadow {
1374 if let Some(pc) = pool.get(&parent) {
1375 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
1376 enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
1377 }
1378 }
1379 }
1380 let mut k = 0u32;
1381 loop {
1382 k += 1;
1383 let cand_tail = if k == 1 {
1384 tail.to_string()
1385 } else {
1386 format!("{tail}{k}")
1387 };
1388 // The candidate must clear every ancestor display name (javac
1389 // checks the whole chain) AND every enclosing field name (else
1390 // the renamed type is just obscured again). Sibling nested
1391 // tails are covered by the pool.has_name check below.
1392 if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
1393 continue;
1394 }
1395 // Package shadow: the renamed tail becomes a member type of
1396 // the root file's class, outranking any package of the same
1397 // name for qualified refs in that file. Same-package top-level
1398 // classes likewise outrank-then-break in-file simple refs.
1399 // BOTH checks apply to field-clash renames only: for the
1400 // orphan rule the k=1 candidate is the natural display name
1401 // (`g0$$r` → `g0$r` renders identically — an invisible rename),
1402 // and rejecting it forced weixin's whole `g0$$X` Runnable
1403 // family to visible `X17` renames, breaking every lucky
1404 // `g0.r`-style resolution corpus-wide (+2911 `变量 r` — the
1405 // orphan rule is ungated by design, predating LOCAL-OK).
1406 if field_clash || pkg_shadow {
1407 if pkg_segments.contains(&cand_tail) {
1408 continue;
1409 }
1410 if pkg_shadow
1411 && fam_segs
1412 .get(root)
1413 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1414 {
1415 continue;
1416 }
1417 if let Some((root_pkg, _)) = root.rsplit_once('/') {
1418 let sib = format!("{root_pkg}/{cand_tail}");
1419 if pool.has_name(&sib) || taken_sibling_displays.contains(&sib)
1420 {
1421 // A top-level sibling exists — or an EARLIER rule
1422 // minted that display (obscuring_class_renames
1423 // moved ssosdk/b → ssosdk/b2 while this loop was
1424 // about to mint nested $b → b2: two `b2`s in one
1425 // scope, member type outranks the package sibling
1426 // and hijacked every bare `b2.b(..)` call).
1427 continue;
1428 }
1429 taken_sibling_displays.insert(sib);
1430 }
1431 }
1432 let cand = format!("{disp_parent}${cand_tail}");
1433 if pool.has_name(&cand) || assigned.contains(&cand) {
1434 continue;
1435 }
1436 assigned.insert(cand.clone());
1437 map.insert(name.clone(), cand);
1438 break;
1439 }
1440 }
1441}
1442
1443/// Field-obscuring rename candidates: nested member types whose simple
1444/// name equals a field of the enclosing class, mapped to the top-level
1445/// class whose file renders them (the LOCAL-OK family root). Mirrors the
1446/// skip conditions of `nested_collision_renames` (same-name entries must
1447/// agree, or the gate proves locality for a rename that never fires — or
1448/// worse, skips one that does).
1449fn field_clash_cands(pool: &DexPool, map: &HashMap<String, String>) -> HashMap<String, String> {
1450 let mut out: HashMap<String, String> = HashMap::default();
1451 for name in &pool.order {
1452 if !name.contains('$') || map.contains_key(name) {
1453 continue;
1454 }
1455 let Some(parent) = find_outer_name(pool, name) else {
1456 continue;
1457 };
1458 let Some(rest) = name
1459 .strip_prefix(parent.as_str())
1460 .and_then(|t| t.strip_prefix('$'))
1461 else {
1462 continue;
1463 };
1464 if !clean_member_tail(rest) {
1465 continue;
1466 }
1467 let tail = rest.rsplit('$').next().unwrap_or(rest);
1468 let Some(pc) = pool.get(&parent) else {
1469 continue;
1470 };
1471 let clash = pc
1472 .static_fields
1473 .iter()
1474 .chain(pc.instance_fields.iter())
1475 .any(|f| crate::classdec::java_ident(&f.name).as_ref() == tail);
1476 if !clash {
1477 continue;
1478 }
1479 let mut root = parent.clone();
1480 loop {
1481 match find_outer_name(pool, &root) {
1482 Some(o) if o != root => root = o,
1483 _ => break,
1484 }
1485 }
1486 out.insert(name.clone(), root);
1487 }
1488 out
1489}
1490
1491/// Whether the pool is (majority-)materialized: full-decompile pipelines
1492/// call `materialize_all` before installing renames, progressive browse
1493/// pools materialize a handful of classes on demand. The refscan gate
1494/// must not run (or stall startup) in the progressive case.
1495fn pool_majority_materialized(pool: &DexPool) -> bool {
1496 let (mut mat, mut lazy) = (0usize, 0usize);
1497 for e in pool.classes.values() {
1498 match e {
1499 ClassEntry::Eager(_) => mat += 1,
1500 ClassEntry::Lazy { pc, .. } => {
1501 if pc.get().is_some() {
1502 mat += 1;
1503 } else {
1504 lazy += 1;
1505 }
1506 }
1507 }
1508 }
1509 lazy == 0 || mat >= lazy
1510}
1511
1512/// Compute and install the registry (call before worker threads spawn).
1513pub fn install_case_renames(pool: &DexPool) {
1514 let mut map = case_rename_map(pool);
1515 // Cross-package reference first segments (descriptor level). Lazy
1516 // (progressive-browse) pools skip the scan — materializing every
1517 // class must not stall single-class queries; the obscuring rules
1518 // then simply never fire (browse mode has no whole-program javac).
1519 let (pkg_segs, fam_segs) = if pool_majority_materialized(pool) {
1520 ref_segments(pool)
1521 } else {
1522 (
1523 jdc_core::FxHashMap::default(),
1524 jdc_core::FxHashMap::default(),
1525 )
1526 };
1527 // Package-leaf shadows FIRST: the nested-collision renames compute
1528 // display chains off the (renamed) parent display names — running
1529 // them before the parent rename left both rules minting the same
1530 // display (`a2` twice in weibo's AIDL families).
1531 pkg_leaf_shadow_renames(pool, &mut map);
1532 class_pkg_collision_renames(pool, &mut map);
1533 obscuring_class_renames(pool, &mut map, &pkg_segs);
1534 nested_collision_renames(pool, &mut map, &fam_segs);
1535 jdc_core::rename::set_class_renames(map);
1536 jdc_core::rename::set_field_renames(combined_field_renames(pool));
1537 if pool_majority_materialized(pool) {
1538 crate::classdec::install_access_widening(pool);
1539 }
1540}
1541
1542/// A top-level class `P/s` whose simple name equals the FIRST PACKAGE
1543/// SEGMENT of types that other classes in P reference cross-package.
1544/// Such a reference renders as the FQN `s.rest...`, and javac binds the
1545/// first segment to the nearest type in scope — the same-package class
1546/// `P/s` (JLS 6.4.2): `n91.f.a(x)` inside pc5/bd looks up member `f` OF
1547/// CLASS pc5.n91 (weixin's n0/n91/uc6/vc6 protobuf cluster: ~12k root
1548/// files, ~90k cascade lines; weibo's `a.g.b.a.b` chains root the same
1549/// way — class a beside subpackage a/ inside package a/g/b). No Java
1550/// syntax escapes an obscured first segment (imports can bypass it, but
1551/// single-type imports are blocked exactly where obfuscated leaf names
1552/// collide with same-package siblings), so the class moves: the r66
1553/// rename mechanism at a new trigger. Gated on DESCRIPTOR-level refs of
1554/// the candidate's own package (supers/fields/protos) — body-only refs
1555/// are missed by design; the import layer covers unblocked leaves there.
1556/// Descriptor-level cross-package reference first segments, aggregated
1557/// per PACKAGE (top-level obscurers: every file in P shares the scope)
1558/// and per FAMILY root (nested obscurers: a nested simple name is in
1559/// scope only inside its own root file). Raw `L..;` scans — no JavaType
1560/// parsing. Body-level refs are missed by design (the import layer
1561/// catches unblocked leaves there); descriptors cover supers, field
1562/// types and method protos — the header positions whose breakage starts
1563/// the error-type cascades.
1564/// Package (or family) → first segments of the cross-package types its
1565/// classes reference at descriptor level. Obscuring-rename trigger sets.
1566type SegMap<'a> = jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>;
1567
1568fn ref_segments(pool: &DexPool) -> (SegMap<'_>, SegMap<'_>) {
1569 fn pkg_of(n: &str) -> &str {
1570 match n.rsplit_once('/') {
1571 Some((p, _)) => p,
1572 None => "",
1573 }
1574 }
1575 fn note<'a>(
1576 pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
1577 fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
1578 pkg: &'a str,
1579 fam: &'a str,
1580 t: &'a str,
1581 ) {
1582 if !t.contains('/') {
1583 return; // root-package target: unreferenceable from named pkgs
1584 }
1585 let _ = pkg; // same-package refs COUNT: ddc renders headers/field
1586 // types fully qualified (print_class_name), so `a.g.b.a.b`
1587 // inside package a/g/b is obscured by class a/g/b/a just
1588 // like a cross-package FQN (weibo's whole `a`-tree family).
1589 let seg = t.split('/').next().unwrap_or("");
1590 if !seg.is_empty() {
1591 pkg_segs.entry(pkg).or_default().insert(seg);
1592 fam_segs.entry(fam).or_default().insert(seg);
1593 }
1594 }
1595 fn scan<'a>(
1596 pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
1597 fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
1598 pkg: &'a str,
1599 fam: &'a str,
1600 desc: &'a str,
1601 ) {
1602 let b = desc.as_bytes();
1603 let mut i = 0usize;
1604 while i < b.len() {
1605 if b[i] == b'L' {
1606 if let Some(end) = desc[i + 1..].find(';') {
1607 note(pkg_segs, fam_segs, pkg, fam, &desc[i + 1..i + 1 + end]);
1608 i += end + 2;
1609 continue;
1610 }
1611 }
1612 i += 1;
1613 }
1614 }
1615 let mut pkg_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
1616 jdc_core::FxHashMap::default();
1617 let mut fam_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
1618 jdc_core::FxHashMap::default();
1619 for name in &pool.order {
1620 let Some(c) = pool.get(name) else { continue };
1621 let pkg = pkg_of(name);
1622 let fam = match name.find('$') {
1623 Some(i) => &name[..i],
1624 None => name.as_str(),
1625 };
1626 if let Some(sup) = &c.super_name {
1627 note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
1628 }
1629 for sup in c.interfaces.iter() {
1630 note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
1631 }
1632 for f in c.static_fields.iter().chain(c.instance_fields.iter()) {
1633 scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &f.desc);
1634 }
1635 for m in c.direct_methods.iter().chain(c.virtual_methods.iter()) {
1636 scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &m.desc);
1637 }
1638 }
1639 (pkg_segs, fam_segs)
1640}
1641
1642/// A top-level class `P/s` whose simple name equals the FIRST PACKAGE
1643/// SEGMENT of types that classes in P reference cross-package. Such a
1644/// reference renders as the FQN `s.rest...`, and javac binds the first
1645/// segment to the nearest type in scope — the same-package class `P/s`
1646/// (JLS 6.4.2, no package fallback — verified): `n91.f.a(x)` inside
1647/// pc5/bd looks up member `f` OF CLASS pc5.n91 (weixin's n0/n91/uc6/vc6
1648/// protobuf cluster: ~12k root files, ~90k cascade lines; weibo's
1649/// `a.g.b.a.b` chains root the same way). No Java syntax escapes an
1650/// obscured first segment, so the class moves: the r66 rename mechanism
1651/// at a new trigger, gated on DESCRIPTOR-level refs of the candidate's
1652/// package (body-only refs go to the import layer). Nested obscurers
1653/// (a nested simple name shadowing a package inside its family file)
1654/// are the companion trigger in nested_collision_renames.
1655fn obscuring_class_renames(
1656 pool: &DexPool,
1657 map: &mut HashMap<String, String>,
1658 pkg_segs: &SegMap,
1659) {
1660 fn pkg_of(n: &str) -> &str {
1661 match n.rsplit_once('/') {
1662 Some((p, _)) => p,
1663 None => "",
1664 }
1665 }
1666 // Root package first segments (segments some class lives under).
1667 let mut root_segs: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
1668 for n in &pool.order {
1669 if let Some(i) = n.find('/') {
1670 root_segs.insert(&n[..i]);
1671 }
1672 }
1673 let mut cands: Vec<&String> = Vec::new();
1674 for name in &pool.order {
1675 let simple = name.rsplit('/').next().unwrap_or(name);
1676 let pkg = pkg_of(name);
1677 if simple.is_empty() || simple.contains('$') || pkg.is_empty() {
1678 continue;
1679 }
1680 if !root_segs.contains(simple) {
1681 continue;
1682 }
1683 if map.get(name).is_some_and(|v| v != name) {
1684 continue; // an earlier rule moved it
1685 }
1686 // NB: NO "class lives under the s tree" exclusion. weibo's
1687 // a/a/b/c/l/a shadowed the FQN refs `a.a.a.a2` that its own
1688 // package files render for cross-subpackage targets (the
1689 // starts_with("a/") skip left the WHOLE a-tree unrenamed —
1690 // 找不到符号 类 a chains). The two-stage refsegs gates below
1691 // require an actual cross-package FQN ref for the trigger.
1692 cands.push(name);
1693 }
1694 let ncands = cands.len();
1695 if std::env::var("DDC_STATS").is_ok() {
1696 eprintln!(
1697 "[renames] obscuring candidates={ncands} pkgs-with-segs={}",
1698 pkg_segs.len()
1699 );
1700 }
1701 if cands.is_empty() {
1702 return;
1703 }
1704 // Fast collision oracle: sorted names + prefix range scan (the r66
1705 // candidate loop's `order.iter().any(starts_with)` is O(n) per
1706 // candidate — this rule can mint tens of thousands of renames).
1707 let mut sorted: Vec<&String> = pool.order.iter().collect();
1708 sorted.sort_unstable();
1709 let prefix_free = |cand: &str| -> bool {
1710 // No class named cand, nothing nested under cand$..., no package cand/...
1711 let idx = sorted.partition_point(|n| n.as_str() < cand);
1712 !sorted[idx..].first().is_some_and(|n| n.starts_with(cand))
1713 };
1714 let pkg_simples = pool.package_simples();
1715 let mut taken_displays: jdc_core::FxHashSet<String> = map.values().cloned().collect();
1716 let mut renamed = 0usize;
1717 // Stage 1: descriptor-level hits rename now; the rest queue for the
1718 // BODY-level image scan (weixin pc5's `invoke-static Ln91/f;.a` —
1719 // 7.8k+6.6k error lines the descriptor gate cannot see).
1720 let mut stage2: Vec<&String> = Vec::new();
1721 for name in cands {
1722 let pkg = pkg_of(name);
1723 let simple = name.rsplit('/').next().unwrap_or(name);
1724 if !pkg_segs.get(pkg).is_some_and(|segs| segs.contains(simple)) {
1725 stage2.push(name);
1726 continue;
1727 }
1728 let siblings = pkg_simples.get(pkg);
1729 let mut k = 1u32;
1730 loop {
1731 k += 1;
1732 let new_simple = format!("{simple}{k}");
1733 let cand = if pkg.is_empty() {
1734 new_simple.clone()
1735 } else {
1736 format!("{pkg}/{new_simple}")
1737 };
1738 // Exact/prefix pool clash, another rule's display target, the
1739 // package's own referenced segments (the new name must not
1740 // obscure ANOTHER package the same files reference), or a
1741 // case-insensitive clash with a same-package sibling
1742 // (case-insensitive filesystems fold the files).
1743 let clash = !prefix_free(&cand)
1744 || taken_displays.contains(&cand)
1745 || pkg_segs
1746 .get(pkg)
1747 .is_some_and(|segs| segs.contains(new_simple.as_str()))
1748 || siblings.is_some_and(|s| {
1749 s.iter()
1750 .any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
1751 });
1752 if !clash {
1753 taken_displays.insert(cand.clone());
1754 map.insert(name.clone(), cand);
1755 renamed += 1;
1756 break;
1757 }
1758 }
1759 }
1760 let mut body_renamed = 0usize;
1761 if !stage2.is_empty() && pool_majority_materialized(pool) {
1762 let stage2_pkgs: jdc_core::FxHashSet<String> =
1763 stage2.iter().map(|n| pkg_of(n).to_string()).collect();
1764 let body_segs = crate::refscan::body_ref_segments(&pool.dexes, &stage2_pkgs);
1765 for name in stage2 {
1766 let pkg = pkg_of(name);
1767 let simple = name.rsplit('/').next().unwrap_or(name);
1768 if !body_segs
1769 .get(pkg)
1770 .is_some_and(|segs| segs.contains(simple))
1771 {
1772 continue;
1773 }
1774 let siblings = pkg_simples.get(pkg);
1775 let mut k = 1u32;
1776 loop {
1777 k += 1;
1778 let new_simple = format!("{simple}{k}");
1779 let cand = format!("{pkg}/{new_simple}");
1780 let clash = !prefix_free(&cand)
1781 || taken_displays.contains(&cand)
1782 || pkg_segs
1783 .get(pkg)
1784 .is_some_and(|segs| segs.contains(new_simple.as_str()))
1785 || body_segs
1786 .get(pkg)
1787 .is_some_and(|segs| segs.contains(new_simple.as_str()))
1788 || siblings.is_some_and(|s| {
1789 s.iter()
1790 .any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
1791 });
1792 if !clash {
1793 taken_displays.insert(cand.clone());
1794 map.insert(name.clone(), cand);
1795 renamed += 1;
1796 body_renamed += 1;
1797 break;
1798 }
1799 }
1800 }
1801 }
1802 if std::env::var("DDC_STATS").is_ok() {
1803 eprintln!(
1804 "[renames] obscuring-class renames: {renamed} (body-gated {body_renamed}; pkgs-with-segs={}, cands={ncands})",
1805 pkg_segs.len()
1806 );
1807 }
1808}
1809
1810/// A class named `P/s` while OTHER classes live under `P/s/` — the dex
1811/// namespace is flat, so obfuscators freely mint a class whose simple
1812/// name equals a subpackage of its own package (`ptr.a` +
1813/// `ptr.a.a`, weibo rsplay/ptr families). That namespace is
1814/// unrepresentable in Java: the declaration itself is
1815/// "类 a与带有相同名称的程序包冲突" and every qualified reference
1816/// through the segment becomes unresolvable (the 53.8k weibo
1817/// missing-class errors root here, with Object/getClass cascades on
1818/// top). Rename the class — the registry carries declarations and
1819/// references — with a candidate that cannot recreate the shape (not a
1820/// class, not a package, not a nested-class prefix, not another rule's
1821/// target).
1822fn class_pkg_collision_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
1823 // Every package that exists in the pool — ALL ancestor prefixes of
1824 // every class path, not just direct parents: Java sees package
1825 // `z` as existing when any class lives at `z/a/b` (the weibo
1826 // `com.sina.weibo.z` interface collides with a subpackage-only
1827 // `z/` that has no directly-resident class).
1828 let mut pkgs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1829 for n in &pool.order {
1830 let mut rest = n.as_str();
1831 while let Some(i) = rest.rfind('/') {
1832 rest = &rest[..i];
1833 pkgs.insert(rest.to_string());
1834 }
1835 }
1836 for name in &pool.order {
1837 let Some((pkg, simple)) = name.rsplit_once('/') else {
1838 continue;
1839 };
1840 if simple.is_empty() || pkg.is_empty() || simple.contains('$') {
1841 // Nested classes render dotted (`Outer.a`) — no package
1842 // effect from their `$` tail.
1843 continue;
1844 }
1845 if !pkgs.contains(name) || map.get(name).is_some_and(|v| v != name) {
1846 continue; // no subpackage under it, or an earlier rule moved it
1847 }
1848 let mut k = 1u32;
1849 loop {
1850 k += 1;
1851 let cand = format!("{pkg}/{simple}{k}");
1852 let clash = pkgs.contains(&cand)
1853 || pool.order.iter().any(|n| n.starts_with(&cand))
1854 || map.values().any(|v| *v == cand);
1855 if !clash {
1856 map.insert(name.clone(), cand);
1857 break;
1858 }
1859 }
1860 }
1861}
1862
1863/// Obfuscators can name a class after its own package leaf (`package k;`
1864/// `class k extends EditText`). Every SAME-PACKAGE reference to `k.Anything`
1865/// then resolves the first segment to the CLASS, not the package —
1866/// javac looks for `Anything` as a member of class k ("找不到符号 class p2,
1867/// location: class k"). Renaming the class (display-level, the registry
1868/// carries declarations and references) makes the package the only
1869/// thing the leaf name resolves to.
1870fn pkg_leaf_shadow_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
1871 let taken: jdc_core::FxHashSet<String> = pool.order.iter().cloned().collect();
1872 for name in &pool.order {
1873 let Some((pkg, simple)) = name.rsplit_once('/') else {
1874 continue;
1875 };
1876 if simple.is_empty() || pkg.is_empty() {
1877 continue;
1878 }
1879 let leaf = pkg.rsplit('/').next().unwrap_or(pkg);
1880 // class name == package leaf, and the package really exists
1881 // (more classes than just this one live under it).
1882 if simple != leaf {
1883 continue;
1884 }
1885 let prefix = format!("{pkg}/");
1886 let siblings = pool
1887 .order
1888 .iter()
1889 .filter(|n| n.starts_with(&prefix) && **n != *name)
1890 .count();
1891 if siblings == 0 || map.get(name).is_some_and(|v| v != name) {
1892 continue; // already renamed by an earlier rule; identity anchors are fine to overwrite
1893 }
1894 let mut k = 1u32;
1895 loop {
1896 k += 1;
1897 let cand = format!("{pkg}/{simple}{k}");
1898 if !taken.contains(&cand) && !map.values().any(|v| *v == cand) {
1899 map.insert(name.clone(), cand);
1900 break;
1901 }
1902 }
1903 }
1904}
1905
1906/// Member-level collision renames (fields AND methods of one class).
1907/// Two sources collapse onto one display name: obfuscators renaming a
1908/// synthetic outer reference (`this$0`) onto a real field's name, and
1909/// the identifier sanitizer mapping non-ASCII to `_` (whole
1910/// `ERROR_中文` families render one `ERROR________`).
1911/// Both are legal in bytecode (members resolve by index) and both are
1912/// javac "already defined" errors in source. Policy per colliding group
1913/// (fields keep the first non-synthetic name; a synthetic outer
1914/// reference restores `this$0`; everything else gets `2`/`3` suffixes;
1915/// method keys include erased parameter types):
1916/// Keys stay ORIGINAL (name, descriptor) so references resolve exactly;
1917/// only the rendered identifier changes.
1918fn member_collision_renames(
1919 pool: &DexPool,
1920) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
1921 let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
1922 HashMap::default();
1923 // Direct nested tails per base class (`C$a` → a for base C), for
1924 // the companion-field rule below.
1925 let mut child_tails: jdc_core::FxHashMap<&str, Vec<&str>> =
1926 jdc_core::FxHashMap::default();
1927 for n in &pool.order {
1928 if let Some(i) = n.find('$') {
1929 let base = &n[..i];
1930 let rest = &n[i + 1..];
1931 let tail = match rest.find('$') {
1932 Some(j) => &rest[..j],
1933 None => rest,
1934 };
1935 if !tail.is_empty() {
1936 child_tails.entry(base).or_default().push(tail);
1937 }
1938 }
1939 }
1940 for name in &pool.order {
1941 let Some(pc) = pool.get_if_materialized(name) else {
1942 continue;
1943 };
1944 let outer_ref_desc = find_outer_name(pool, name)
1945 .filter(|o| o != name)
1946 .map(|o| format!("L{o};"));
1947 // ---- fields: one shared namespace (static + instance) ----
1948 let fields: Vec<&PoolField> = pc
1949 .static_fields
1950 .iter()
1951 .chain(pc.instance_fields.iter())
1952 .collect();
1953 let fgroups: HashMap<String, Vec<&PoolField>> = HashMap::default();
1954 let mut fgroups = fgroups;
1955 for f in &fields {
1956 let disp = crate::classdec::java_ident(&f.name).into_owned();
1957 fgroups.entry(disp).or_default().push(f);
1958 }
1959 let f_taken: jdc_core::FxHashSet<String> = fields
1960 .iter()
1961 .map(|f| crate::classdec::java_ident(&f.name).into_owned())
1962 .collect();
1963 let mut f_taken = f_taken;
1964 for group in fgroups.values() {
1965 if group.len() < 2 {
1966 continue;
1967 }
1968 let keeper_pos = group
1969 .iter()
1970 .position(|f| f.access & crate::access::ACC_SYNTHETIC == 0)
1971 .unwrap_or(0);
1972 for (gi, f) in group.iter().enumerate() {
1973 if gi == keeper_pos {
1974 continue;
1975 }
1976 let base = crate::classdec::java_ident(&f.name).into_owned();
1977 let display: String = match &outer_ref_desc {
1978 Some(ord)
1979 if f.access & crate::access::ACC_SYNTHETIC != 0
1980 && !f.is_static
1981 && *ord == f.desc =>
1982 {
1983 let mut k = 0u32;
1984 loop {
1985 let cand = if k == 0 {
1986 "this$0".to_string()
1987 } else {
1988 format!("this$0{k}")
1989 };
1990 if f_taken.insert(cand.clone()) {
1991 break cand;
1992 }
1993 k += 1;
1994 }
1995 }
1996 _ => suffix_unique(&base, &mut f_taken),
1997 };
1998 out.entry(std::sync::Arc::from(name.as_str()))
1999 .or_default()
2000 .push(jdc_core::rename::FieldRename {
2001 name: std::sync::Arc::from(f.name.as_str()),
2002 desc: std::sync::Arc::from(f.desc.as_str()),
2003 display: std::sync::Arc::from(display.as_str()),
2004 });
2005 }
2006 }
2007 // ---- companion-holder fields vs nested types (JLS 6.4.2 in
2008 // expression context): Kotlin compiles `object`/companion
2009 // holders as a static field whose name EQUALS the nested class
2010 // it instances (`static final j$a a`), and static members of the
2011 // nested are invoked as `j.a.c(..)` — but `j.a` in an EXPRESSION
2012 // resolves to the FIELD (variables outrank member types), so the
2013 // call becomes a member lookup on the instance type... which is
2014 // the same class, yet javac reports 找不到符号 for statics
2015 // reached through the shadowed path, and the whole file
2016 // error-types (weibo feed/business/j: 654 errors + closure
2017 // cascade; the nested-side rename for this is the LOCAL-OK-gated
2018 // rule that explodes ungated — the FIELD side is safe because
2019 // dex field refs always carry the declaring class, so this
2020 // registry keys every consumer). Gate: the nested class has
2021 // static members (companion shape) and the field's descriptor
2022 // is exactly the nested type.
2023 if let Some(tails) = child_tails.get(name.as_str()) {
2024 for tail in tails {
2025 let nested = format!("{name}${tail}");
2026 let Some(nc) = pool.get_if_materialized(&nested) else {
2027 continue;
2028 };
2029 // Only CLEAN tails render dotted (`j.a`); flat-rendered
2030 // tails (`j$1`) carry the `$` and never clash.
2031 if !clean_member_tail(tail) {
2032 continue;
2033 }
2034 let has_statics = nc
2035 .static_fields
2036 .iter()
2037 .chain(nc.instance_fields.iter())
2038 .any(|f| f.is_static)
2039 || nc.all_methods().any(|m| m.access & crate::access::ACC_STATIC != 0);
2040 if !has_statics {
2041 continue;
2042 }
2043 // STRICT companion shape: the field's descriptor IS the
2044 // nested type (`static final j$a a`). The relaxed form
2045 // (any same-named field beside a statics-bearing nested)
2046 // minted thousands of renames on weibo and exploded the
2047 // battery to the 500k cap — the field funnel does not
2048 // hold at that scale (改名面 vs 引用覆盖面, again).
2049 let want_desc = format!("L{nested};");
2050 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
2051 if f.desc != want_desc {
2052 continue;
2053 }
2054 let disp = crate::classdec::java_ident(&f.name).into_owned();
2055 if disp != crate::classdec::java_ident(tail) {
2056 continue;
2057 }
2058 // Already renamed by the duplicate-group rule above?
2059 let already = out
2060 .get(&std::sync::Arc::from(name.as_str()))
2061 .is_some_and(|v| {
2062 v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
2063 });
2064 if already {
2065 continue;
2066 }
2067 let display = suffix_unique(&disp, &mut f_taken);
2068 f_taken.insert(display.clone());
2069 out.entry(std::sync::Arc::from(name.as_str()))
2070 .or_default()
2071 .push(jdc_core::rename::FieldRename {
2072 name: std::sync::Arc::from(f.name.as_str()),
2073 desc: std::sync::Arc::from(f.desc.as_str()),
2074 display: std::sync::Arc::from(display.as_str()),
2075 });
2076 }
2077 }
2078 }
2079 // ---- methods: display key = sanitized name + erased params ----
2080 let methods: Vec<&PoolMethod> = pc.all_methods().collect();
2081 let mut m_taken: jdc_core::FxHashSet<String> = methods
2082 .iter()
2083 .map(|m| crate::classdec::java_ident(&m.name).into_owned())
2084 .collect();
2085 let mut mgroups: HashMap<(String, String), Vec<&PoolMethod>> = HashMap::default();
2086 for m in &methods {
2087 let d: &str = &m.desc;
2088 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2089 let hi = d.find(')').unwrap_or(d.len());
2090 let key = (
2091 crate::classdec::java_ident(&m.name).into_owned(),
2092 d[lo..hi].to_string(),
2093 );
2094 mgroups.entry(key).or_default().push(m);
2095 }
2096 for ((base, _), group) in &mgroups {
2097 if group.len() < 2 {
2098 continue;
2099 }
2100 // A COVARIANT override pair (same ORIGINAL name, different
2101 // descriptors — the compiler's bridge shape: the interface's
2102 // `deserialize(e)` plus the narrower implementation). Renaming
2103 // either side breaks @Override ("is not abstract and does not
2104 // override abstract method deserialize(e)", ~2.2k on reqable);
2105 // the emitter's claim logic already renders only the
2106 // non-bridge member of the pair.
2107 let covariant = group
2108 .iter()
2109 .any(|m| group.iter().any(|o| o.name == m.name && o.desc != m.desc));
2110 if covariant {
2111 continue;
2112 }
2113 // Real re-declarations (same original name AND descriptor —
2114 // R8 duplicates) are already deduped by the emitter's claim
2115 // logic; only SANITIZER collapses reach here and every one of
2116 // them is a distinct method.
2117 let mut base = base.clone();
2118 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
2119 jdc_core::FxHashSet::default();
2120 for m in group {
2121 if !seen_orig.insert((&m.name, &m.desc)) {
2122 continue; // exact duplicate: emitter drops it
2123 }
2124 let display = suffix_unique(&base, &mut m_taken);
2125 out.entry(std::sync::Arc::from(name.as_str()))
2126 .or_default()
2127 .push(jdc_core::rename::FieldRename {
2128 name: m.name.clone(),
2129 desc: m.desc.clone(),
2130 display: std::sync::Arc::from(display.as_str()),
2131 });
2132 // The base stays the sanitized ORIGINAL so the first
2133 // occurrence keeps the plain name.
2134 let _ = &mut base;
2135 }
2136 }
2137 }
2138 out
2139}
2140
2141fn suffix_unique(base: &str, taken: &mut jdc_core::FxHashSet<String>) -> String {
2142 let mut k = 1u32;
2143 loop {
2144 k += 1;
2145 let cand = format!("{base}{k}");
2146 if taken.insert(cand.clone()) {
2147 return cand;
2148 }
2149 }
2150}
2151
2152/// Install member renames (call with the class rename install, before
2153/// workers spawn).
2154pub fn install_field_renames(pool: &DexPool) {
2155 jdc_core::rename::set_field_renames(combined_field_renames(pool));
2156}
2157
2158/// member_collision_renames + field_deshadow_renames, collision-registry
2159/// entries winning per (owner, name, desc).
2160fn combined_field_renames(
2161 pool: &DexPool,
2162) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
2163 let mut base = member_collision_renames(pool);
2164 let extra = field_deshadow_renames(pool);
2165 let mut merged = 0usize;
2166 for (owner, v) in extra {
2167 let e = base.entry(owner).or_default();
2168 for fr in v {
2169 if !e.iter().any(|x| x.name == fr.name && x.desc == fr.desc) {
2170 e.push(fr);
2171 merged += 1;
2172 }
2173 }
2174 }
2175 if std::env::var("DDC_STATS").is_ok() {
2176 eprintln!("[renames] field-deshadow renames: {merged}");
2177 }
2178 base
2179}
2180
2181/// Field deshadow (JLS 6.4.2, expression position): a FIELD whose display
2182/// name equals a root package first segment captures package-qualified
2183/// renders inside its family's file — androidx z2's field `j` binds the
2184/// `j` of `j.a.a` (找不到符号 变量 a). Rename through the field registry
2185/// (declaration and every reference funnel through field_display).
2186/// Gate: only when the family actually references that segment
2187/// (family_ref_segments body scan) or the segment is the family's OWN
2188/// package root (same-package refs fall back to `root.x` FQN renders) —
2189/// ungated field renames are the six-times-falsified blast radius.
2190fn field_deshadow_renames(
2191 pool: &DexPool,
2192) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
2193 let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
2194 HashMap::default();
2195 let root_segs = pool.root_pkg_segs();
2196 if root_segs.is_empty() {
2197 return out;
2198 }
2199 // Candidate fields per family (display, original name, desc).
2200 let mut cand: HashMap<
2201 String,
2202 Vec<(String, String, String, u32)>,
2203 > = HashMap::default();
2204 for name in &pool.order {
2205 let Some(pc) = pool.get_if_materialized(name) else {
2206 continue;
2207 };
2208 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
2209 let disp = crate::classdec::java_ident(&f.name).into_owned();
2210 if root_segs.contains(&disp) {
2211 cand
2212 .entry(name.clone())
2213 .or_default()
2214 .push((disp, f.name.clone(), f.desc.clone(), f.access));
2215 }
2216 }
2217 }
2218 if cand.is_empty() {
2219 return out;
2220 }
2221 let cand_set: jdc_core::FxHashSet<String> = cand.keys().cloned().collect();
2222 let t0 = std::time::Instant::now();
2223 let (fam_segs, fam_nest) =
2224 crate::refscan::family_ref_segments(&pool.dexes, &cand_set);
2225 if std::env::var("DDC_STATS").is_ok() {
2226 eprintln!(
2227 "[renames] field-deshadow scan: cands={} scan={:?}",
2228 cand_set.len(),
2229 t0.elapsed()
2230 );
2231 }
2232 for (fam, fields) in cand {
2233 let own_root = fam.find('/').map(|i| fam[..i].to_string());
2234 let scanned = fam_segs.get(&fam);
2235 // NO nested-tail collision trigger here: a private field named
2236 // like a nested class does capture `Fam.b.member` in expression
2237 // position (lark LKEvent static-ctx family, ~1k lines), but
2238 // renaming it is corpus-toxic even gated private+body-
2239 // referenced — lark +32k (43,643 renames), weixin/weibo −200.
2240 // Eighth falsification of loose rename gates: the resolution
2241 // perturbation surface of mass field renames dwarfs the target
2242 // family. The fam_nest scan stays wired for a future
2243 // render-side fix (the capture is only unfixable INSIDE the
2244 // family file; foreign files could import the nested type).
2245 let _ = &fam_nest;
2246 let hits: Vec<(String, String, String)> = fields
2247 .into_iter()
2248 .filter(|(disp, _, _, _access)| {
2249 scanned.is_some_and(|s| s.contains(disp))
2250 || own_root.as_deref() == Some(disp.as_str())
2251 })
2252 .map(|(d, n, de, _)| (d, n, de))
2253 .collect();
2254 if hits.is_empty() {
2255 continue;
2256 }
2257 // Names already taken in the family's member scope: field and
2258 // method displays plus direct nested-class tails (member types
2259 // share the lexical scope of the rendered file). Built only for
2260 // hit families — the per-family setup must stay off the 175k-
2261 // candidate loop (an O(pool.order) nested scan there cost 60s on
2262 // weixin).
2263 let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2264 if let Some(pc) = pool.get_if_materialized(&fam) {
2265 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
2266 taken.insert(crate::classdec::java_ident(&f.name).into_owned());
2267 }
2268 for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
2269 taken.insert(crate::classdec::java_ident(&m.name).into_owned());
2270 }
2271 }
2272 let mut nested_tails: jdc_core::FxHashSet<String> =
2273 jdc_core::FxHashSet::default();
2274 for child in pool.children_of(&fam) {
2275 if let Some(tail) = child.rsplit('$').next() {
2276 if !tail.is_empty() {
2277 taken.insert(tail.to_string());
2278 nested_tails.insert(tail.to_string());
2279 }
2280 }
2281 }
2282 for (disp, name, desc) in hits {
2283 let mut k = 0u32;
2284 let new_disp = loop {
2285 k += 1;
2286 let c = if k == 1 {
2287 format!("{disp}x")
2288 } else {
2289 format!("{disp}x{k}")
2290 };
2291 if !taken.contains(&c) && !root_segs.contains(&c) {
2292 break c;
2293 }
2294 };
2295 taken.insert(new_disp.clone());
2296 out.entry(std::sync::Arc::from(fam.as_str()))
2297 .or_default()
2298 .push(jdc_core::rename::FieldRename {
2299 name: std::sync::Arc::from(name.as_str()),
2300 desc: std::sync::Arc::from(desc.as_str()),
2301 display: std::sync::Arc::from(new_disp.as_str()),
2302 });
2303 }
2304 }
2305 out
2306}
2307pub use jdc_core::rename::apply_class_rename;