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;
10pub mod fwdb;
11
12pub use classdec::{sanitize_fq, ClassOptions};
13pub mod ctx;
14pub mod lift;
15pub mod method;
16pub mod passes;
17pub mod platform;
18mod refscan;
19pub mod ssa_census;
20
21use jdc_core::FxHashMap as HashMap;
22
23use ddc_dex::annotations::{self, EncodedValue};
24
25/// A resolved static-field initializer.
26#[derive(Debug, Clone)]
27pub enum StaticValue {
28 Int(i64),
29 Float(f32),
30 Double(f64),
31 Str(String),
32 Type(String),
33 Boolean(bool),
34 Null,
35 /// (declaring class, field name, field descriptor) — enum constants /
36 /// const field refs. The desc participates in the rename-registry key
37 /// (owner, name, desc) so the render site can consult field_display.
38 Field(String, String, String),
39 Other,
40}
41use ddc_dex::{ClassDef, DexFile};
42use jdc_core::types::{
43 parse_field_descriptor, parse_method_descriptor, JavaType, MethodDescriptor,
44};
45
46// ---------------------------------------------------------------------------
47// Access flags (Dalvik numbering, JVM-compatible subset).
48// ---------------------------------------------------------------------------
49pub mod access {
50 pub const ACC_PUBLIC: u32 = 0x1;
51 pub const ACC_PRIVATE: u32 = 0x2;
52 pub const ACC_PROTECTED: u32 = 0x4;
53 pub const ACC_STATIC: u32 = 0x8;
54 pub const ACC_FINAL: u32 = 0x10;
55 pub const ACC_SYNCHRONIZED: u32 = 0x20;
56 pub const ACC_BRIDGE: u32 = 0x40;
57 pub const ACC_VARARGS: u32 = 0x80;
58 pub const ACC_NATIVE: u32 = 0x100;
59 pub const ACC_INTERFACE: u32 = 0x200;
60 pub const ACC_ABSTRACT: u32 = 0x400;
61 pub const ACC_STRICT: u32 = 0x800;
62 pub const ACC_SYNTHETIC: u32 = 0x1000;
63 pub const ACC_ANNOTATION: u32 = 0x2000;
64 pub const ACC_ENUM: u32 = 0x4000;
65 pub const ACC_CONSTRUCTOR: u32 = 0x1_0000;
66 pub const ACC_DECLARED_SYNCHRONIZED: u32 = 0x2_0000;
67}
68
69/// One field of a pooled class.
70#[derive(Debug, Clone)]
71pub struct PoolField {
72 /// Shared with the dex string table (field names repeat massively
73 /// under R8's single-letter namespace — one Arc per unique name).
74 pub name: std::sync::Arc<str>,
75 /// Field descriptor (`I`, `Ljava/lang/String;`, ...) — shared per
76 /// unique descriptor through the same table.
77 pub desc: std::sync::Arc<str>,
78 pub access: u32,
79 pub is_static: bool,
80}
81
82/// One method of a pooled class.
83#[derive(Debug, Clone)]
84pub struct PoolMethod {
85 /// Shared with the dex string table (the idx IS the dedup key).
86 pub name: std::sync::Arc<str>,
87 /// Method descriptor, shared per proto (a dex's proto table is the
88 /// descriptor dedup layer — 1.9M methods on lark share ~1 proto
89 /// table's worth of unique descriptors).
90 pub desc: std::sync::Arc<str>,
91 pub access: u32,
92 pub code_off: u32,
93 pub debug_info_off: u32,
94 /// Which DEX image the body lives in.
95 pub dex_idx: usize,
96}
97
98impl PoolMethod {
99 pub fn parsed_desc(&self) -> Option<MethodDescriptor> {
100 parse_method_descriptor(&self.desc)
101 }
102 pub fn is_static(&self) -> bool {
103 self.access & access::ACC_STATIC != 0
104 }
105 pub fn is_abstract_or_native(&self) -> bool {
106 self.access & (access::ACC_ABSTRACT | access::ACC_NATIVE) != 0
107 }
108}
109
110/// One pool entry: either already built, or name-registered with its
111/// (dex index, class_def index) locator for on-demand construction.
112enum ClassEntry {
113 Eager(PoolClass),
114 Lazy {
115 at: (usize, usize),
116 pc: std::sync::OnceLock<PoolClass>,
117 },
118}
119
120/// A class materialized from one `class_def_item` (with ids resolved to
121/// names and the class data expanded).
122#[derive(Debug, Clone)]
123pub struct PoolClass {
124 pub name: String,
125 pub access: u32,
126 /// `None` for `java/lang/Object` roots.
127 pub super_name: Option<String>,
128 pub interfaces: Vec<String>,
129 pub source_file: Option<String>,
130 pub static_fields: Vec<PoolField>,
131 pub instance_fields: Vec<PoolField>,
132 pub direct_methods: Vec<PoolMethod>,
133 pub virtual_methods: Vec<PoolMethod>,
134 /// Static initial values aligned with the head of `static_fields`.
135 pub static_values: Vec<StaticValue>,
136 /// Nesting evidence with annotation ids resolved to names.
137 pub nesting: ResolvedNesting,
138 /// Which DEX image the class body lives in (provenance header).
139 pub dex_idx: usize,
140}
141
142/// Nesting evidence, names resolved at pooling time.
143#[derive(Debug, Clone, Default)]
144pub struct ResolvedNesting {
145 pub enclosing_class: Option<String>,
146 /// (class internal name, method name) of the enclosing method.
147 pub enclosing_method: Option<(String, String)>,
148 pub member_classes: Vec<String>,
149}
150
151impl PoolClass {
152 pub fn is_interface(&self) -> bool {
153 self.access & access::ACC_INTERFACE != 0
154 }
155
156 pub fn is_enum(&self) -> bool {
157 self.access & access::ACC_ENUM != 0
158 }
159
160 pub fn is_synthetic(&self) -> bool {
161 self.access & access::ACC_SYNTHETIC != 0
162 }
163
164 pub fn is_static_nested(&self) -> bool {
165 self.access & access::ACC_STATIC != 0
166 }
167
168 /// All methods in declaration order.
169 pub fn all_methods(&self) -> impl Iterator<Item = &PoolMethod> {
170 self.direct_methods
171 .iter()
172 .chain(self.virtual_methods.iter())
173 }
174
175 /// Find a method by name + descriptor.
176 pub fn find_method(&self, name: &str, desc: &str) -> Option<&PoolMethod> {
177 self.all_methods()
178 .find(|m| &*m.name == name && &*m.desc == desc)
179 }
180
181 /// Constructors `<init>` matching `arity` descriptor arguments.
182 pub fn ctors_by_arity(&self, arity: usize) -> Vec<&PoolMethod> {
183 self.all_methods()
184 .filter(|m| &*m.name == "<init>")
185 .filter(|m| {
186 m.parsed_desc()
187 .map(|d| d.args.len() == arity)
188 .unwrap_or(false)
189 })
190 .collect()
191 }
192
193 pub fn field_flags_of(&self, name: &str) -> Option<u32> {
194 self.static_fields
195 .iter()
196 .chain(self.instance_fields.iter())
197 .find(|f| f.name.as_ref() == name)
198 .map(|f| f.access)
199 }
200}
201
202/// Multi-DEX class pool: classes from all images, first definition wins.
203/// One facade-map entry: the public base plus, for PARTIALLY covered
204/// parts, the exact static members the base declares (None = the whole
205/// part rewrites; Some = per-member gate). Single map = one hash probe
206/// per static reference (two maps measured +2.4s on weibo).
207type FacadeTarget = (
208 String,
209 Option<
210 std::sync::Arc<(
211 jdc_core::FxHashSet<(String, String)>,
212 jdc_core::FxHashSet<String>,
213 )>,
214 >,
215);
216
217pub struct DexPool {
218 /// Images as Arcs: `dex()` hands out snapshots whose borrows stay
219 /// valid for the Arc's lifetime (decompile workers hold them), while
220 /// `release_images` empties the image data through exclusive access
221 /// when all of an image's classes are done (full-decompile driver
222 /// only — progressive callers keep images whole).
223 dexes: Vec<std::sync::Arc<DexFile>>,
224 /// Human label per image ("weibo!classes.dex") for the provenance
225 /// header; defaults to "dex N" until the CLI names the inputs.
226 pub dex_labels: Vec<String>,
227 /// Name → class entry. Eager pools materialize everything at add
228 /// time (full decompile, tests); lazy pools register names only and
229 /// materialize on first `get` (progressive `getclass` — one class's
230 /// PoolClass costs annotation reads, 145k of them cost ~0.2s).
231 classes: HashMap<String, ClassEntry>,
232 /// Class names in insertion order (stable for output).
233 pub order: Vec<String>,
234 /// Per-image remaining class count once `arm_retirement` fires.
235 retire_counts: std::sync::Mutex<Vec<u64>>,
236 retire_armed: std::sync::atomic::AtomicBool,
237 /// name → outer (computed once; `$` heuristic + dalvik annotations).
238 outers: std::sync::OnceLock<HashMap<String, Option<String>>>,
239 /// outer → direct children (computed once).
240 children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
241 /// Per-image hot-reference interning (parallel to `dexes`).
242 ref_caches: Vec<DexRefCache>,
243 /// Synthetic-static accessor code snapshots keyed by (dex_idx,
244 /// code_off). `inline_accessors` reads accessor bodies ACROSS images
245 /// during worker runs; image retirement can release those bytes
246 /// mid-run, making the inline decision — and with it var numbering
247 /// downstream — dependent on worker completion interleaving
248 /// (observed: reqable enum-constant args flipping `var0`/`var1`
249 /// between identical runs; DDC_NORETIRE=1 stable). The full-decompile
250 /// driver fills this BEFORE `arm_retirement`; progressive/lazy pools
251 /// never retire and fall through to live reads.
252 accessor_code: std::sync::OnceLock<AccessorSnapshots>,
253 /// Cached package → direct-class simple names (import-collision gate).
254 pkg_simples: std::sync::OnceLock<jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>>,
255 /// First path segments of all packages (`v2` for `v2/n`): local or
256 /// field names equal to one capture package-qualified renders
257 /// (`v2.n.a`) — deshadow_locals consults this.
258 root_segs: std::sync::OnceLock<jdc_core::FxHashSet<String>>,
259 /// Partially-covered facade parts (see kotlin_facade_partial_map).
260 /// Kotlin multi-file facade parts → public facade (see
261 /// `kotlin_facade_map`).
262 kotlin_facades: std::sync::OnceLock<jdc_core::FxHashMap<String, FacadeTarget>>,
263 /// Per class internal name: the set of `<init>` ARGUMENT descriptors
264 /// (the `(..)` inner text) REFERENCED through that class anywhere in
265 /// the pool. Dex resolves a `<init>` method-ref up the superclass
266 /// chain, so a subclass `super(..)` may target `C.<init>(sig)` that C
267 /// does NOT declare (an ancestor does) — Java has no inherited ctors,
268 /// so C needs a bridge. Computed once from the method-id tables
269 /// (parsed, survive image retirement). See `inherited_ctor_bridges`.
270 ctor_refs: std::sync::OnceLock<jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>>,
271}
272
273/// Raw code_item bytes of every synthetic-static accessor, keyed by
274/// (dex_idx, code_off) — see `snapshot_accessor_code`.
275type AccessorSnapshots = HashMap<(usize, u32), std::sync::Arc<[u8]>>;
276
277/// Lazily-filled, thread-shared reference interning for one image.
278///
279/// The lifter used to rebuild class names, field/method names and WHOLE
280/// method descriptors per INSTRUCTION (`method_ref` alone: N param
281/// Strings → `format!` descriptor → re-parse — 6-10 allocations per
282/// invoke). Every entry here is built at most once per (image, table
283/// index); lifts and the endless IR clones downstream are refcount
284/// bumps. Slots are `OnceLock` so worker threads fill them race-free
285/// without a lock on the hot path.
286pub(crate) struct DexRefCache {
287 proto_descs: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::MethodDescriptor>>]>,
288 type_names: Box<[std::sync::OnceLock<std::sync::Arc<str>>]>,
289 type_javas: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::JavaType>>]>,
290}
291
292impl DexRefCache {
293 fn new(protos: usize, types: usize) -> Self {
294 DexRefCache {
295 proto_descs: (0..protos).map(|_| std::sync::OnceLock::new()).collect(),
296 type_names: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
297 type_javas: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
298 }
299 }
300}
301
302impl DexPool {
303 pub fn new() -> Self {
304 DexPool {
305 dexes: Vec::new(),
306 retire_armed: std::sync::atomic::AtomicBool::new(false),
307 dex_labels: Vec::new(),
308 classes: HashMap::default(),
309 order: Vec::new(),
310 retire_counts: std::sync::Mutex::new(Vec::new()),
311 outers: std::sync::OnceLock::new(),
312 children: std::sync::OnceLock::new(),
313 ref_caches: Vec::new(),
314 accessor_code: std::sync::OnceLock::new(),
315 pkg_simples: std::sync::OnceLock::new(),
316 root_segs: std::sync::OnceLock::new(),
317 kotlin_facades: std::sync::OnceLock::new(),
318 ctor_refs: std::sync::OnceLock::new(),
319 }
320 }
321
322 /// Referenced `<init>` argument-descriptor set for a class internal
323 /// name (see `ctor_refs`). Builds the whole index on first call by
324 /// scanning every image's method-id table for `<init>` refs — cheap
325 /// (flat table, integer name-idx compare against the interned
326 /// "<init>" string) and done once for the whole decompile.
327 pub fn ctor_ref_sigs(&self, class: &str) -> Option<&jdc_core::FxHashSet<String>> {
328 self.ctor_refs
329 .get_or_init(|| {
330 let mut map: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
331 jdc_core::FxHashMap::default();
332 for dex in &self.dexes {
333 let n = dex.method_count();
334 for i in 0..n {
335 let m = dex.method(i as u32);
336 if dex.string(m.name_idx) != "<init>" {
337 continue;
338 }
339 let desc = dex.proto_desc(m.proto_idx);
340 let (Some(lo), Some(hi)) = (desc.find('('), desc.find(')')) else {
341 continue;
342 };
343 map.entry(dex.class_name(m.class_idx))
344 .or_default()
345 .insert(desc[lo + 1..hi].to_string());
346 }
347 }
348 map
349 })
350 .get(class)
351 }
352
353 /// Shared `MethodDescriptor` of a proto (one parse per image).
354 pub fn proto_desc_parsed(
355 &self,
356 di: usize,
357 proto_idx: u32,
358 ) -> std::sync::Arc<jdc_core::types::MethodDescriptor> {
359 let Some(dex) = self.dexes.get(di) else {
360 return std::sync::Arc::new(jdc_core::types::MethodDescriptor {
361 args: Vec::new(),
362 ret: jdc_core::types::JavaType::Void,
363 });
364 };
365 let build = || {
366 std::sync::Arc::new(parse_proto_desc(dex, proto_idx))
367 };
368 match self
369 .ref_caches
370 .get(di)
371 .and_then(|c| c.proto_descs.get(proto_idx as usize))
372 {
373 Some(slot) => slot.get_or_init(build).clone(),
374 None => build(),
375 }
376 }
377
378 /// Shared internal class name of a type id (one strip+alloc per image).
379 pub fn type_name_arc(&self, di: usize, type_idx: u32) -> std::sync::Arc<str> {
380 let Some(dex) = self.dexes.get(di) else {
381 return std::sync::Arc::from("");
382 };
383 let build = || std::sync::Arc::from(dex.class_name(type_idx).as_str());
384 match self
385 .ref_caches
386 .get(di)
387 .and_then(|c| c.type_names.get(type_idx as usize))
388 {
389 Some(slot) => slot.get_or_init(build).clone(),
390 None => build(),
391 }
392 }
393
394 /// Shared parsed type of a type id.
395 pub fn type_java(&self, di: usize, type_idx: u32) -> std::sync::Arc<jdc_core::types::JavaType> {
396 let Some(dex) = self.dexes.get(di) else {
397 return std::sync::Arc::new(jdc_core::types::JavaType::Object(
398 std::sync::Arc::from("java/lang/Object"),
399 ));
400 };
401 let build = || std::sync::Arc::new(desc_type(dex.type_name(type_idx)));
402 match self
403 .ref_caches
404 .get(di)
405 .and_then(|c| c.type_javas.get(type_idx as usize))
406 {
407 Some(slot) => slot.get_or_init(build).clone(),
408 None => build(),
409 }
410 }
411
412 /// name → outer map, computed lazily once.
413 fn outer_map(&self) -> &HashMap<String, Option<String>> {
414 self.outers.get_or_init(|| {
415 self.order
416 .iter()
417 .map(|n| (n.clone(), find_outer_name(self, n)))
418 .collect()
419 })
420 }
421
422 /// The outer class of `name`, from the cached map (borrowed).
423 pub fn outer_of(&self, name: &str) -> Option<&str> {
424 self.outer_map().get(name).and_then(|o| o.as_deref())
425 }
426
427 /// Direct nested children of `internal` (any `$` depth 1), cached.
428 /// (Perf: returns a borrowed slice — the owned-Vec clone ran once per
429 /// class on 98k-class runs.)
430 pub fn children_of(&self, internal: &str) -> &[String] {
431 if self.children.get().is_none() {
432 // Build FIRST, set ONCE: the previous shape set an empty map
433 // to claim the OnceLock and then silently failed to store the
434 // real index (`let _ = set(...)` on an initialized lock) —
435 // children_of returned [] forever, so member nested classes
436 // were neither inlined nor emitted as files (Guard$Report
437 // vanished whole; every corpus run since the borrow refactor
438 // dropped them, invisible to the syntax-only gate).
439 let mut idx: HashMap<String, Vec<String>> = HashMap::default();
440 for name in &self.order {
441 if let Some(outer) = self.outer_of(name).map(str::to_string) {
442 idx.entry(outer).or_default().push(name.clone());
443 }
444 }
445 // A racing thread may have set an identical map first — the
446 // index is a pure function of `order`, so either copy is right.
447 let _ = self.children.set(idx);
448 }
449 self.children
450 .get()
451 .and_then(|m| m.get(internal))
452 .map(|v| v.as_slice())
453 .unwrap_or(&[])
454 }
455
456 /// Adds one image; duplicate class names keep their first definition.
457 /// Returns the image's index — call `set_dex_label` with it to give the
458 /// provenance header a real origin instead of the default "dex N".
459 pub fn add_dex(&mut self, dex: DexFile) -> usize {
460 let dex_idx = self.dexes.len();
461 // Pre-size the name map: 240k-entry growth rehashed repeatedly
462 // (reserve_rehash showed up in corpus profiles).
463 self.classes.reserve(dex.class_defs.len());
464 // The annotation reader borrows the image; pool classes are built
465 // before ownership moves into `self.dexes` (no full-image copy).
466 for cd in &dex.class_defs {
467 let name = dex.class_name(cd.class_idx);
468 if self.classes.contains_key(&name) {
469 continue;
470 }
471 let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
472 self.classes.insert(name.clone(), ClassEntry::Eager(pc));
473 self.order.push(name);
474 }
475 self.ref_caches
476 .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
477 self.dexes.push(std::sync::Arc::new(dex));
478 self.retire_counts.lock().unwrap().push(0);
479 self.dex_labels.push(format!("dex {}", dex_idx));
480 dex_idx
481 }
482
483 /// Registers class NAMES only; the PoolClass (annotation reads
484 /// included) is materialized on first `get`. Duplicate class names
485 /// keep their first definition, matching `add_dex`.
486 pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
487 let dex_idx = self.dexes.len();
488 self.classes.reserve(dex.class_defs.len());
489 for (ci, cd) in dex.class_defs.iter().enumerate() {
490 let name = dex.class_name(cd.class_idx);
491 if self.classes.contains_key(&name) {
492 continue;
493 }
494 self.classes.insert(
495 name.clone(),
496 ClassEntry::Lazy {
497 at: (dex_idx, ci),
498 pc: std::sync::OnceLock::new(),
499 },
500 );
501 self.order.push(name);
502 }
503 self.ref_caches
504 .push(DexRefCache::new(dex.proto_count(), dex.type_count()));
505 self.dexes.push(std::sync::Arc::new(dex));
506 self.retire_counts.lock().unwrap().push(0);
507 self.dex_labels.push(format!("dex {}", dex_idx));
508 dex_idx
509 }
510
511 /// Label the image at `idx` (see `add_dex`).
512 pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
513 if let Some(slot) = self.dex_labels.get_mut(idx) {
514 *slot = label.into();
515 }
516 }
517
518 pub fn len(&self) -> usize {
519 self.classes.len()
520 }
521
522 pub fn is_empty(&self) -> bool {
523 self.classes.is_empty()
524 }
525
526 pub fn get(&self, internal: &str) -> Option<&PoolClass> {
527 self.get_inner(internal, None)
528 }
529
530 /// `get` with optional pre-taken raw-image snapshots (one per
531 /// image). `materialize_all` hoists the `DexFile::raw()` lock out of
532 /// the per-class loop — the parallel materialize used to take the
533 /// same per-image lock once PER CLASS across all threads.
534 fn get_inner(&self, internal: &str, raws: Option<&[&[u8]]>) -> Option<&PoolClass> {
535 match self.classes.get(internal)? {
536 ClassEntry::Eager(pc) => Some(pc),
537 ClassEntry::Lazy { at, pc } => {
538 if let Some(built) = pc.get() {
539 return Some(built);
540 }
541 let (di, ci) = *at;
542 let dex = self.dexes.get(di)?;
543 let cd = dex.class_defs.get(ci)?;
544 let raw = match raws {
545 Some(r) => r.get(di).copied().unwrap_or(&[]),
546 None => dex.raw(),
547 };
548 let built = pool_class_of(dex, raw, cd, di);
549 Some(pc.get_or_init(move || built))
550 }
551 }
552 }
553
554 /// Materialize every lazy entry (parallel across the given chunk
555 /// count). After this the pool is observationally identical to an
556 /// eagerly-built one — including the annotation-aware outer-map —
557 /// while the annotation reads ran on worker threads instead of the
558 /// serial pool build (~0.2s on weibo).
559 pub fn materialize_all(&self, threads: usize) {
560 let keys: Vec<String> = self
561 .classes
562 .iter()
563 .filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
564 .map(|(k, _)| k.clone())
565 .collect();
566 if keys.is_empty() {
567 return;
568 }
569 // One raw() snapshot per image for the whole sweep (see
570 // get_inner). Safe window: materialize_all completes before
571 // retirement is armed, so no image releases while these slices
572 // are alive.
573 let raws: Vec<&[u8]> = self.dexes.iter().map(|d| d.raw()).collect();
574 let threads = threads.max(1);
575 let next = std::sync::atomic::AtomicUsize::new(0);
576 std::thread::scope(|scope| {
577 for _ in 0..threads {
578 scope.spawn(|| loop {
579 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
580 let Some(k) = keys.get(i) else { break };
581 let _ = self.get_inner(k, Some(&raws));
582 });
583 }
584 });
585 }
586
587 /// `get` without materializing: None for not-yet-built lazy entries.
588 fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
589 match self.classes.get(internal)? {
590 ClassEntry::Eager(pc) => Some(pc),
591 ClassEntry::Lazy { pc, .. } => pc.get(),
592 }
593 }
594
595 /// Name registered (no materialization).
596 pub fn has_name(&self, internal: &str) -> bool {
597 self.classes.contains_key(internal)
598 }
599
600 /// One image as an Arc snapshot (borrows of the snapshot live as
601 /// long as the caller holds the Arc).
602 pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
603 self.dexes.get(idx).cloned()
604 }
605
606 pub fn dex_count(&self) -> usize {
607 self.dexes.len()
608 }
609
610 /// Raw image list (install-phase whole-program scans).
611 pub fn dexes(&self) -> &[std::sync::Arc<ddc_dex::DexFile>] {
612 &self.dexes
613 }
614
615 /// First path segments of every package in the pool (cached once).
616 pub fn root_pkg_segs(&self) -> &jdc_core::FxHashSet<String> {
617 self.root_segs.get_or_init(|| {
618 self.order
619 .iter()
620 .filter_map(|n| n.find('/').map(|i| n[..i].to_string()))
621 .collect()
622 })
623 }
624
625 /// Package → simple names of its direct classes, cached once per
626 /// pool (the import-collision gate consults it per class — the
627 /// uncached scan was O(classes²) and 10×'d weixin's wall time).
628 pub fn package_simples(&self) -> &jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> {
629 self.pkg_simples.get_or_init(|| {
630 let mut m: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
631 jdc_core::FxHashMap::default();
632 for n in &self.order {
633 if let Some((pkg, simple)) = n.rsplit_once('/') {
634 m.entry(pkg.to_string())
635 .or_default()
636 .insert(simple.to_string());
637 }
638 }
639 m
640 })
641 }
642
643 /// Kotlin multi-file class facades: the compiler splits a file facade
644 /// (`kotlin.text.StringsKt`) into package-private parts
645 /// (`StringsKt__StringsKt`, `StringsKt__StringsJVMKt`) and call sites
646 /// target the PARTS directly. Java cannot access a package-private
647 /// class cross-package ("StringsKt__StringsKt在kotlin.text中不是公共的"
648 /// — 1.4k lark root errors), but the public facade EXTENDS the parts,
649 /// so static members resolve through it: rewrite the reference's owner
650 /// class to the facade. part → base, gated on base existing, being
651 /// public, and transitively extending the part (static inheritance is
652 /// the resolution guarantee).
653 pub fn kotlin_facade_map(&self) -> &jdc_core::FxHashMap<String, FacadeTarget> {
654 self.kotlin_facades.get_or_init(|| {
655 let t_fb = std::time::Instant::now();
656 let mut m: jdc_core::FxHashMap<String, FacadeTarget> =
657 jdc_core::FxHashMap::default();
658 let mut npartial = 0usize;
659 for name in &self.order {
660 let Some(i) = name.find("__") else { continue };
661 let base = &name[..i];
662 if base.is_empty() {
663 continue;
664 }
665 let Some(basec) = self.get(base) else {
666 continue;
667 };
668 if basec.access & crate::access::ACC_PUBLIC == 0 {
669 continue;
670 }
671 // base must transitively extend the part...
672 let mut cur = basec.super_name.clone();
673 let mut hops = 0u32;
674 let mut reaches = false;
675 while let Some(c) = cur {
676 if c == *name {
677 reaches = true;
678 break;
679 }
680 if hops >= 8 {
681 break;
682 }
683 hops += 1;
684 cur = self.get(&c).and_then(|cc| cc.super_name.clone());
685 }
686 // ...OR cover the part's static members: the Kotlin
687 // compiler MERGES every part's statics into the public
688 // facade, but the facade's super chain only includes ONE
689 // part — lark's `CollectionsKt extends
690 // CollectionsKt___CollectionsKt` leaves
691 // `CollectionsKt__CollectionsJVMKt` uncovered (463
692 // 不是公共的 errors). Member coverage (name+desc for
693 // methods, name for fields) is the precise rewrite
694 // condition: every reference through the facade then
695 // resolves exactly as through the part.
696 if reaches {
697 m.insert(name.clone(), (base.to_string(), None));
698 continue;
699 }
700 // Member-coverage fallback: the Kotlin compiler MERGES
701 // the parts' statics into the public facade, but R8 can
702 // sever the super chain (lark: `CollectionsKt extends
703 // CollectionsKt___CollectionsKt extends
704 // kotlin/collections/s` — `__CollectionsJVMKt` is off
705 // the chain, 463 不是公共的 errors) and may drop facade
706 // members no direct facade call needed. Map at (name,
707 // desc) granularity then: covered members rewrite, the
708 // rest keep the part owner (a latent error either way —
709 // Java cannot access the package-private part).
710 let Some(part) = self.get(name) else {
711 continue;
712 };
713 let base_methods: jdc_core::FxHashSet<(&str, &str)> = basec
714 .direct_methods
715 .iter()
716 .chain(basec.virtual_methods.iter())
717 .map(|mm| (&*mm.name, &*mm.desc))
718 .collect();
719 let base_fields: jdc_core::FxHashSet<&str> =
720 basec.static_fields.iter().map(|f| &*f.name).collect();
721 let mut methods: jdc_core::FxHashSet<(String, String)> =
722 jdc_core::FxHashSet::default();
723 let mut fields: jdc_core::FxHashSet<String> =
724 jdc_core::FxHashSet::default();
725 let mut all = true;
726 for mm in part
727 .direct_methods
728 .iter()
729 .chain(part.virtual_methods.iter())
730 .filter(|mm| mm.access & crate::access::ACC_STATIC != 0)
731 {
732 if base_methods.contains(&(&*mm.name, &*mm.desc)) {
733 methods.insert((mm.name.to_string(), mm.desc.to_string()));
734 } else {
735 all = false;
736 }
737 }
738 for pf in &part.static_fields {
739 if base_fields.contains(&*pf.name) {
740 fields.insert(pf.name.to_string());
741 } else {
742 all = false;
743 }
744 }
745 if all {
746 m.insert(name.clone(), (base.to_string(), None));
747 } else if !methods.is_empty() || !fields.is_empty() {
748 npartial += 1;
749 m.insert(
750 name.clone(),
751 (base.to_string(), Some(std::sync::Arc::new((methods, fields)))),
752 );
753 }
754 }
755 if std::env::var("DDC_STATS").is_ok() {
756 eprintln!(
757 "[renames] facade map: entries={} partial={} build={:?}",
758 m.len(),
759 npartial,
760 t_fb.elapsed()
761 );
762 }
763 m
764 })
765 }
766
767 pub fn class_names(&self) -> impl Iterator<Item = &str> {
768 self.order.iter().map(|s| s.as_str())
769 }
770
771 /// True when `sub` is assignable to `sup` (internal names), walking the
772 /// pool's hierarchy. Unknown classes are never assignable (conservative).
773 pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
774 if sub == sup {
775 return true;
776 }
777 // Arrays: covariance by element type when both are arrays.
778 if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
779 if sub_elem.starts_with('L')
780 && sup_elem.starts_with('L')
781 && sub_elem.ends_with(';')
782 && sup_elem.ends_with(';')
783 {
784 return self.is_subtype(
785 &sub_elem[1..sub_elem.len() - 1],
786 &sup_elem[1..sup_elem.len() - 1],
787 );
788 }
789 // Multi-dimensional: recurse on the element descriptors
790 // (`[[LSavedState;` <: `[[LObject;` peels one dimension at a
791 // time); the equality fallback below stays for primitive
792 // elements (`[I` vs `[J` are invariant).
793 if sub_elem.starts_with('[') && sup_elem.starts_with('[') {
794 return self.is_subtype(sub_elem, sup_elem);
795 }
796 return sub_elem == sup_elem;
797 }
798 // Everything reference-typed is Object-assignable (the pool walk
799 // used to fall off the chain end and answer false for
800 // String <: Object — the hand-written Object-formal special case
801 // in the ambiguity pin existed for exactly this hole).
802 if sup == "java/lang/Object" {
803 return !matches!(
804 sub,
805 "I" | "Z" | "B" | "S" | "C" | "J" | "F" | "D" | "V"
806 );
807 }
808 let mut cur = self.get(sub);
809 if cur.is_none() {
810 // `sub` is a framework (or phantom) type: the embedded API
811 // database decides (phantoms are absent there too → false).
812 return crate::fwdb::is_subtype(sub, sup);
813 }
814 let mut hops = 0;
815 while let Some(c) = cur {
816 hops += 1;
817 if hops > 64 {
818 return false;
819 }
820 for i in &c.interfaces {
821 if i == sup || self.is_subtype(i, sup) {
822 return true;
823 }
824 }
825 match &c.super_name {
826 Some(s) if s == sup => return true,
827 Some(s) if s != "java/lang/Object" => {
828 match self.get(s) {
829 Some(sc) => cur = Some(sc),
830 // Framework boundary: continue in the embedded DB.
831 None => return crate::fwdb::is_subtype(s, sup),
832 }
833 }
834 _ => return false,
835 }
836 }
837 false
838 }
839}
840
841impl Default for DexPool {
842 fn default() -> Self {
843 Self::new()
844 }
845}
846
847fn array_elem(desc: &str) -> Option<&str> {
848 desc.strip_prefix('[')
849}
850
851fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
852 match v {
853 EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
854 EncodedValue::Short(x) => StaticValue::Int(*x as i64),
855 EncodedValue::Char(x) => StaticValue::Int(*x as i64),
856 EncodedValue::Int(x) => StaticValue::Int(*x as i64),
857 EncodedValue::Long(x) => StaticValue::Int(*x),
858 EncodedValue::Float(x) => StaticValue::Float(*x),
859 EncodedValue::Double(x) => StaticValue::Double(*x),
860 EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
861 EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
862 EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
863 EncodedValue::Null => StaticValue::Null,
864 EncodedValue::Enum(i) | EncodedValue::Field(i) => {
865 let f = dex.field(*i);
866 StaticValue::Field(
867 dex.class_name(f.class_idx),
868 dex.string(f.name_idx).to_string(),
869 dex.type_name(f.type_idx).to_string(),
870 )
871 }
872 _ => StaticValue::Other,
873 }
874}
875
876fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
877 let name = dex.class_name(cd.class_idx);
878 let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
879 None
880 } else {
881 let s = dex.class_name(cd.superclass_idx);
882 if s.is_empty() || s == "java/lang/Object" {
883 None
884 } else {
885 Some(s)
886 }
887 };
888 let interfaces = dex
889 .interfaces_of(cd)
890 .into_iter()
891 .map(|t| dex.class_name(t))
892 .collect();
893 let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
894 None
895 } else {
896 Some(dex.string(cd.source_file_idx).to_string())
897 };
898
899 let data = dex.class_data(cd);
900 let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
901 let f = dex.field(ef.field_idx);
902 PoolField {
903 name: dex.string_arc(f.name_idx),
904 desc: dex.type_name_arc(f.type_idx),
905 access: ef.access_flags,
906 is_static,
907 }
908 };
909 let static_fields: Vec<PoolField> = data
910 .static_fields
911 .iter()
912 .map(|f| mk_field(f, true))
913 .collect();
914 let instance_fields: Vec<PoolField> = data
915 .instance_fields
916 .iter()
917 .map(|f| mk_field(f, false))
918 .collect();
919 let mk_method = |em: &ddc_dex::EncodedMethod| {
920 let m = dex.method(em.method_idx);
921 let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
922 Some(d) => (em.code_off, d),
923 None => (0, 0),
924 };
925 PoolMethod {
926 name: dex.string_arc(m.name_idx),
927 desc: dex.proto_desc(m.proto_idx),
928 access: em.access_flags,
929 code_off,
930 debug_info_off,
931 dex_idx,
932 }
933 };
934 let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
935 let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
936
937 let static_values: Vec<StaticValue> = dex
938 .static_values(cd.static_values_off)
939 .into_iter()
940 .map(|v| resolve_static_value(&v, dex))
941 .collect();
942 let anns = if cd.annotations_off != 0 {
943 annotations::read_class_annotations(raw, cd.annotations_off)
944 } else {
945 Vec::new()
946 };
947 let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
948 let nesting = ResolvedNesting {
949 enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
950 enclosing_method: raw.enclosing_method.map(|m| {
951 let mid = dex.method(m);
952 (
953 dex.class_name(mid.class_idx),
954 dex.string(mid.name_idx).to_string(),
955 )
956 }),
957 member_classes: raw
958 .member_classes
959 .into_iter()
960 .map(|t| dex.class_name(t))
961 .collect(),
962 };
963
964 PoolClass {
965 name,
966 access: cd.access_flags,
967 super_name,
968 interfaces,
969 source_file,
970 static_fields,
971 instance_fields,
972 direct_methods,
973 virtual_methods,
974 static_values,
975 nesting,
976 dex_idx,
977 }
978}
979
980/// JavaType for a field/method descriptor segment.
981/// Parse a proto's descriptor into a MethodDescriptor straight from the
982/// tables (no string round-trip).
983fn parse_proto_desc(
984 dex: &ddc_dex::DexFile,
985 proto_idx: u32,
986) -> jdc_core::types::MethodDescriptor {
987 let proto = dex.proto(proto_idx);
988 let args = dex
989 .proto_params(proto_idx)
990 .iter()
991 .map(|&t| desc_type(dex.type_name(t)))
992 .collect();
993 jdc_core::types::MethodDescriptor {
994 args,
995 ret: desc_type(dex.type_name(proto.return_type_idx)),
996 }
997}
998
999pub fn desc_type(desc: &str) -> JavaType {
1000 parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
1001}
1002
1003/// Nesting evidence for `internal`: dalvik annotations first, then the
1004/// `$`-name heuristic against the pool.
1005pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
1006 // Annotation refinement only for ALREADY-MATERIALIZED classes: eager
1007 // pools (full decompile) behave exactly as before; lazy pools
1008 // (progressive getclass) fall straight to the `$` chain without
1009 // materializing the world.
1010 if let Some(pc) = pool.get_if_materialized(internal) {
1011 if let Some(enc) = &pc.nesting.enclosing_class {
1012 // The annotation outer must still EXIST: R8 deletes whole
1013 // outer classes and leaves the nestlings as flat `$` files
1014 // (weixin matrix JiffiesMonitorFeature deleted,
1015 // $JiffiesSnapshot survives) — an unresolved annotation
1016 // outer made refs render dotted THROUGH a missing type
1017 // ("是在不可访问的类或接口中定义的"). Fall through to the
1018 // existence-checked `$` walk.
1019 if pool.has_name(enc) {
1020 return Some(enc.clone());
1021 }
1022 }
1023 }
1024 let mut rest = internal;
1025 while let Some(d) = rest.rfind('$') {
1026 // A `$` followed by `/` is a PACKAGE-path dollar (desugar's
1027 // `j$/util/...`), never a nesting boundary: without this the
1028 // whole j$ tree attached to a root-package class `j` as family
1029 // members (deepseek's 62k-line j.java absorbing 549 desugar
1030 // classes, double-emitted against their flat j$/ files).
1031 if rest.as_bytes().get(d + 1) == Some(&b'/') {
1032 rest = &rest[..d];
1033 continue;
1034 }
1035 let cand = &rest[..d];
1036 if pool.has_name(cand) {
1037 return Some(cand.to_string());
1038 }
1039 rest = cand;
1040 }
1041 None
1042}
1043
1044/// True when the `$` tail names a plain member class (not anonymous/local/
1045/// lambda), i.e. the class renders inside its outer's compilation unit.
1046fn clean_member_tail(rest: &str) -> bool {
1047 if rest.starts_with('-') {
1048 return false;
1049 }
1050 // A member tail can never contain `/` — that is a package-path
1051 // fragment from a package-dollar name (`j$/util/X` stripped against
1052 // a root class `j`); defensive backstop for the find_outer_name
1053 // guard.
1054 if rest.contains('/') {
1055 return false;
1056 }
1057 // d8's synthetic outline/lambda/backport classes (`X$$ExternalSynthetic…`)
1058 // are TOP-LEVEL synthetics, not nested members: `$$` is d8's marker, and
1059 // find_outer_name's `$`-chain walk mistakes them for children of `X`.
1060 // Emitting them nested strands every cross-package reference on a bare
1061 // simple name ("找不到符号 变量 ExternalSyntheticBackportWithForwarding0",
1062 // weibo ×467). Targeted at `ExternalSynthetic` only — Kotlin's
1063 // `$$serializer` / `$$delegate` stay nested members.
1064 if rest.starts_with("$ExternalSynthetic") {
1065 return false;
1066 }
1067 let tail = rest.rsplit('$').next().unwrap_or(rest);
1068 if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
1069 return false;
1070 }
1071 true
1072}
1073
1074/// Classes to emit as their own compilation units: top-level classes plus
1075/// anonymous/local/lambda-shaped ones (clean members render inline).
1076impl DexPool {
1077 /// Arm image retirement (full-decompile driver only): count one
1078 /// pending class per image by pool ownership.
1079 /// Snapshot every synthetic-static accessor body (the exact predicate
1080 /// `inline_accessors` uses) as RAW code_item bytes, so cross-image
1081 /// accessor reads during worker runs survive image retirement.
1082 /// Call AFTER materialization and BEFORE `arm_retirement`; idempotent
1083 /// (first fill wins). Raw memcpy, no decode — accessors are numerous
1084 /// (weixin ~100k) but only a fraction are ever inlined, so decoding
1085 /// stays lazy at the use site. The slice bound covers the fixed
1086 /// header + insns + try items with generous handler slack (accessor
1087 /// tries are vanishingly rare; the snapshot is a deterministic
1088 /// function of the image either way).
1089 pub fn snapshot_accessor_code(&self) {
1090 let mut snap: AccessorSnapshots = HashMap::default();
1091 let mut bytes_total = 0usize;
1092 for name in &self.order {
1093 let Some(pc) = self.get_if_materialized(name) else {
1094 continue;
1095 };
1096 for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
1097 if m.code_off == 0
1098 || !m.is_static()
1099 || m.access & crate::access::ACC_SYNTHETIC == 0
1100 {
1101 continue;
1102 }
1103 if snap.contains_key(&(m.dex_idx, m.code_off)) {
1104 continue;
1105 }
1106 let Some(dex) = self.dex(m.dex_idx) else {
1107 continue;
1108 };
1109 let raw = dex.raw();
1110 let off = m.code_off as usize;
1111 if off + 16 > raw.len() {
1112 continue;
1113 }
1114 let u2 = |o: usize| u16::from_le_bytes([raw[o], raw[o + 1]]) as usize;
1115 let u4 = |o: usize| {
1116 u32::from_le_bytes([raw[o], raw[o + 1], raw[o + 2], raw[o + 3]]) as usize
1117 };
1118 let tries = u2(off + 6);
1119 let insns = u4(off + 12);
1120 let mut end = off + 16 + 2 * insns;
1121 if tries > 0 {
1122 // pad to 4-align + try_item[tries] + handler blobs
1123 // (uleb-coded; slack covers realistic accessor shapes).
1124 end = end.next_multiple_of(4) + 8 * tries + 256 + 64 * tries;
1125 }
1126 let end = end.min(raw.len());
1127 let slice: std::sync::Arc<[u8]> = raw[off..end].into();
1128 bytes_total += slice.len();
1129 snap.insert((m.dex_idx, m.code_off), slice);
1130 }
1131 }
1132 if std::env::var("DDC_WALL").is_ok() {
1133 eprintln!(
1134 "[wall] accessor snapshot: {} entries, {} KB",
1135 snap.len(),
1136 bytes_total / 1024
1137 );
1138 }
1139 let _ = self.accessor_code.set(snap);
1140 }
1141
1142 /// Snapshotted accessor code_item bytes, when the snapshot was taken.
1143 /// Parse with `CodeItem::parse(&bytes, 0)`.
1144 pub fn accessor_code(&self, dex_idx: usize, code_off: u32) -> Option<std::sync::Arc<[u8]>> {
1145 self.accessor_code.get()?.get(&(dex_idx, code_off)).cloned()
1146 }
1147
1148 pub fn arm_retirement(&self) {
1149 self.retire_armed
1150 .store(true, std::sync::atomic::Ordering::Release);
1151 let mut counts = self.retire_counts.lock().unwrap();
1152 for c in counts.iter_mut() {
1153 *c = 0;
1154 }
1155 for entry in self.classes.values() {
1156 let di = match entry {
1157 ClassEntry::Eager(pc) => pc.dex_idx,
1158 ClassEntry::Lazy { at, .. } => at.0,
1159 };
1160 if let Some(c) = counts.get_mut(di) {
1161 *c += 1;
1162 }
1163 }
1164 }
1165
1166 /// Report one class finished; Some(image) when it was the image's
1167 /// last class (driver batches these into release_images).
1168 pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
1169 if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
1170 return None;
1171 }
1172 let entry = self.classes.get(class_name)?;
1173 let di = match entry {
1174 ClassEntry::Eager(pc) => pc.dex_idx,
1175 ClassEntry::Lazy { at, .. } => at.0,
1176 };
1177 let mut counts = self.retire_counts.lock().unwrap();
1178 match counts.get_mut(di) {
1179 Some(c) => {
1180 *c = c.saturating_sub(1);
1181 if *c == 0 {
1182 Some(di)
1183 } else {
1184 None
1185 }
1186 }
1187 None => None,
1188 }
1189 }
1190
1191 /// Release the images' inflated bytes. Safe: the driver calls this
1192 /// only when every class of each image is emitted or failed; new
1193 /// `dex()` snapshots still work (tables valid, code accessors empty).
1194 pub fn release_images(&self, indexes: &[usize]) {
1195 let mut counts = self.retire_counts.lock().unwrap();
1196 for &i in indexes {
1197 if let Some(dex) = self.dexes.get(i) {
1198 // Mark the shared image: code accessors go empty
1199 // immediately; the bytes drop when the last snapshot
1200 // drops (workers hold snapshots only mid-class).
1201 dex.mark_released();
1202 }
1203 if let Some(c) = counts.get_mut(i) {
1204 *c = u64::MAX; // released marker
1205 }
1206 }
1207 }
1208}
1209
1210pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
1211 pool.class_names()
1212 .filter(|name| match pool.outer_of(name) {
1213 None => true,
1214 Some(outer) => {
1215 if pool.get(outer).is_none() {
1216 return true;
1217 }
1218 // The outer can come from an ANNOTATION (EnclosingClass)
1219 // with no naming relationship to this class — obfuscated
1220 // apps pair a 1-char name with a long enclosing descriptor
1221 // (weixin), which made the old blind slice panic. Only a
1222 // real `outer$tail` prefix yields a member tail; anything
1223 // else is a standalone unit.
1224 let rest = name
1225 .strip_prefix(outer)
1226 .and_then(|t| t.strip_prefix('$'))
1227 .unwrap_or("");
1228 !clean_member_tail(rest)
1229 }
1230 })
1231 .map(|n| n.to_string())
1232 .collect()
1233}
1234
1235/// Sanitized form of one internal-name segment — the shared mapping the
1236/// declaration site (`print_class_name` → `sanitize_ref`), the CLASS
1237/// lookup and the CLI writer must all agree on. Exported so the writer
1238/// cannot drift from the declaration (see `sanitize_fq`).
1239pub fn sanitize_seg(seg: &str) -> String {
1240 classdec::sanitize_fq(seg)
1241}
1242
1243/// Sanitized internal PATH form of a class name: the exact on-disk
1244/// identity `source_path` writes and the declaration claims.
1245pub fn sanitize_internal(internal: &str) -> String {
1246 internal
1247 .split('/')
1248 .map(classdec::sanitize_fq)
1249 .collect::<Vec<_>>()
1250 .join("/")
1251}
1252
1253/// Deterministic case-collision renames over the FILE-emission set:
1254/// classes whose internal names differ only in letter case cannot share
1255/// one case-insensitive directory; the first (sorted) member of each
1256/// group keeps its name, the others gain `_2`, `_3`, … on the simple
1257/// segment. The map carries identity entries for unrenamed file-level
1258/// classes (they anchor nested prefix walks in apply_class_rename).
1259pub fn case_rename_map(pool: &DexPool) -> HashMap<String, String> {
1260 use std::collections::HashMap;
1261 let mut groups: HashMap<String, Vec<String>> = HashMap::default();
1262 for t in top_level_classes(pool) {
1263 groups.entry(t.to_lowercase()).or_default().push(t);
1264 }
1265 // No `folds` clone: the fold check consults `groups` itself (the old
1266 // full HashSet<String> of every lowercase key was a ~25-30MB pure
1267 // duplicate on the large corpora). Sort in place first, then an
1268 // IMMUTABLE walk keeps the keys queryable for the fold lookup.
1269 for v in groups.values_mut() {
1270 v.sort();
1271 }
1272 let mut map = HashMap::default();
1273 for members in groups.values() {
1274 for (i, m) in members.iter().enumerate() {
1275 if i == 0 {
1276 map.insert(m.clone(), m.clone());
1277 continue;
1278 }
1279 // Suffix the simple segment; keep incrementing if the fold
1280 // of the suffixed name is already taken by a real class.
1281 let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
1282 let mut n = i + 1;
1283 loop {
1284 let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
1285 let fold = cand.to_lowercase();
1286 if !groups.contains_key(&fold) {
1287 map.insert(m.clone(), cand);
1288 break;
1289 }
1290 n += 1;
1291 }
1292 }
1293 }
1294 map
1295}
1296
1297/// Display-level renames for two nested-class shapes that render
1298/// uncompilable or inconsistent output:
1299///
1300/// 1. A member class whose simple tail equals its rendered parent's
1301/// tail (`x0$a$a` inside `x0$a`): javac rejects a member class with
1302/// the same simple name as its immediately enclosing class ("已在类
1303/// x0中定义了类 x0.a") — a shape Kotlin lambda families hit by the
1304/// thousand (lark alone: 22k). Bump the tail: `a` → `a2`, `a3`, …
1305/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent from the
1306/// pool): the member DECLARES as `b` inside `x0` but references
1307/// printed `x0.a$b` — a type that exists nowhere. Flatten the
1308/// display name to `x0$b` so every site agrees.
1309///
1310/// Both are pure display renames keyed by internal name; declarations,
1311/// ctor names, file names and every type reference funnel through
1312/// apply_class_rename, so one map keeps all sites consistent. Children
1313/// Display-level renames for nested member classes that render
1314/// uncompilable or inconsistent output:
1315///
1316/// 1. A member whose simple tail equals ANY enclosing class's simple
1317/// name in its nesting chain. javac rejects more than the immediate
1318/// parent (`class a { static class a2 { static class a {} } }` —
1319/// member `a` two levels under `a` — is "已在类 X中定义了类 X.a");
1320/// Kotlin lambda families (`x0$a$a` inside `x0$a`) hit it by the
1321/// thousand (lark alone: 22k). Bump the tail until it clears every
1322/// ancestor name: `a` → `a3`.
1323/// 2. An orphaned intermediate (`x0$a$b` with `x0$a` absent): the
1324/// member DECLARES as `b` inside `x0` but references printed
1325/// `x0.a$b` — a type that exists nowhere. Flatten the display to
1326/// `x0$b` so every site agrees.
1327///
1328/// Candidates build on the parent's DISPLAY name (ancestors are
1329/// renamed first — ascending `$`-depth order), so declarations,
1330/// constructor names, file names and every type reference stay
1331/// consistent through apply_class_rename. All are pure display
1332/// renames keyed by internal name.
1333/// In-progress-map display composition, mirroring
1334/// `rename::apply_class_rename`'s ancestor walk (the registry is not
1335/// installed yet during map construction, so mint rules must compose
1336/// through renamed ancestors themselves). A nested name whose DIRECT
1337/// parent has no entry still inherits every renamed higher prefix:
1338/// grandparent `yN5` -> `yN5_3` must turn minted `yN5$a$a2` into
1339/// `yN5_3$a$a2` (xingye case-quads: the raw-prefix entry short-
1340/// circuited the render walk — 程序包ddcroot.yN5.a不存在 ×134-family).
1341fn display_of(name: &str, map: &HashMap<String, String>) -> String {
1342 // EXACT hit first — the walk truncates the full name before ever
1343 // consulting it, and an identity entry on an ancestor prefix
1344 // (case_rename_map seeds those for every fold-group first member)
1345 // would short-circuit to the raw name (lark aa2/d$c17 minted, then
1346 // aa2/d$c$a composed against the identity of aa2/d — the child
1347 // stranded on the dead `c` prefix, 找不到符号 类 a ×1,874).
1348 if let Some(d) = map.get(name) {
1349 return d.clone();
1350 }
1351 let mut s = name;
1352 while let Some(i) = s.rfind('$') {
1353 s = &s[..i];
1354 match map.get(s) {
1355 Some(d) if d.as_str() != s => return format!("{d}{}", &name[s.len()..]),
1356 Some(_) => return name.to_string(),
1357 None => continue,
1358 }
1359 }
1360 map.get(name).cloned().unwrap_or_else(|| name.to_string())
1361}
1362
1363fn nested_collision_renames(
1364 pool: &DexPool,
1365 map: &mut HashMap<String, String>,
1366 fam_segs: &SegMap,
1367) {
1368 // Ancestors before descendants: a parent's rename must be in the
1369 // map when its children compute their display chains.
1370 let mut names: Vec<&String> = pool.order.iter().collect();
1371 names.sort_by_key(|n| n.matches('$').count());
1372 let mut assigned: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1373 // Top-level package segments. A nested member type DISPLAYED as `a2`
1374 // shadows package `a2` for every qualified `a2.x` reference in its
1375 // file (member types outrank package names in class scope), so a
1376 // renamed tail must not become a package shadow — round 47's failure
1377 // mode at nested level: candidate `a2` (for j2/g$a) broke the SAME
1378 // file's `extends a2.a` ("找不到符号 类 a" + a 9450-error Object
1379 // cascade as every dependent of the corrupt head failed too).
1380 let mut pkg_segments: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1381 for n in &pool.order {
1382 if let Some((seg, _)) = n.split_once('/') {
1383 pkg_segments.insert(seg.to_string());
1384 }
1385 }
1386 // LOCAL-OK gate (round-59 Design B): a field-obscuring rename may
1387 // only fire when every class referencing the candidate lives inside
1388 // the root file's family, so the rename perturbs exactly ONE rendered
1389 // file. Ungated (Design A) the rule netted reqable −1653 / lark −5221
1390 // but weibo +36916: renames there flipped ambiguous type-vs-package
1391 // resolutions across thousands of pre-existing conflict families and
1392 // javac's error recovery turned "missing supertype, body suppressed"
1393 // files into full Object cascades. Lazy (progressive-browse) pools
1394 // skip the gate AND the rule — the multi-second image scan must not
1395 // stall single-class queries; the full-decompile pipeline always
1396 // materializes everything before installing renames.
1397 // Round-59 Design B stands: ungated field-clash renames were
1398 // retried AFTER the package-obscuring rules landed (weibo gate,
1399 // DDC_FC_UNGATED A/B) and still exploded to the 500k error cap —
1400 // fifth falsification of the ungated rename route.
1401 let local_ok: jdc_core::FxHashSet<String> = if pool_majority_materialized(pool) {
1402 refscan::local_ok(&pool.dexes, &field_clash_cands(pool, map))
1403 } else {
1404 jdc_core::FxHashSet::default()
1405 };
1406 let mut taken_sibling_displays: jdc_core::FxHashSet<String> =
1407 map.values().cloned().collect();
1408 // Stage-2 queue: nested tails equal to a top-level package segment
1409 // whose family showed NO descriptor-level trigger — the victim ref
1410 // may live in method bodies only (weixin e32/a2's `a.b.c()` bound
1411 // to its own nested `class a`, 位置: 类 a ×169).
1412 let mut body_cands: Vec<(String, String, String)> = Vec::new();
1413 for name in names {
1414 if !name.contains('$') || map.contains_key(name) {
1415 continue;
1416 }
1417 let Some(parent) = find_outer_name(pool, name) else {
1418 continue;
1419 };
1420 // Only a REAL `parent$rest` name renders as an inline member;
1421 // anonymous/local/lambda tails keep their flat own-file names.
1422 let Some(rest) = name
1423 .strip_prefix(parent.as_str())
1424 .and_then(|t| t.strip_prefix('$'))
1425 else {
1426 continue;
1427 };
1428 if !clean_member_tail(rest) {
1429 continue;
1430 }
1431 // Display simple names of every enclosing level, bottom-up; `cur`
1432 // ends at the ROOT (the top-level class whose file renders this
1433 // nested type — the scope the renamed tail lives in).
1434 let mut chain: Vec<String> = Vec::new();
1435 let mut cur = parent.clone();
1436 loop {
1437 let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
1438 let simple = disp.rsplit('/').next().unwrap_or(&disp);
1439 chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
1440 match find_outer_name(pool, &cur) {
1441 Some(o) if o != cur => cur = o,
1442 _ => break,
1443 }
1444 }
1445 let root: &str = &cur;
1446 let disp_parent = display_of(&parent, map);
1447 let tail = rest.rsplit('$').next().unwrap_or(rest);
1448 let orphan = rest.contains('$');
1449 // Field obscuring (JLS 6.4.2): a nested type whose simple name
1450 // equals an enclosing-class field is OBSCURED by it in a
1451 // `<Parent>.<tail>` qualified reference — `io/flutter/view/g$i`
1452 // (a `static enum i`) renders `g.i.t`, but g also declares an
1453 // instance field `i`, so javac resolves `g.i` to the field:
1454 // "无法从静态上下文中引用非静态 变量 i" (975 of 1140 reqable
1455 // static-context errors). The fix renames the NESTED TYPE (the
1456 // registry carries every render path: shorten, inner_simple —
1457 // whose bypass was the round-55 blowup, fixed in jdc-core —
1458 // classdec declarations), gated to LOCAL-OK candidates by the
1459 // refscan above. PoC: in-file rename of flutter g's `enum i`
1460 // took the class from 2452 errors to 1.
1461 let field_clash = local_ok.contains(name.as_str());
1462 // Package shadow (JLS 6.4.2 at nested level): a nested type
1463 // DISPLAYED with a simple name equal to the first segment of a
1464 // package its FAMILY references binds every `s.x` qualified ref
1465 // in the root file to the member type — the family's own outer
1466 // rename can EXPOSE this (e/b → e/b2 un-collided the nested `b`,
1467 // which then hijacked `b.b2` — 循环继承). The round-47 candidate
1468 // guard only kept RENAMED tails off package segments; pre-
1469 // existing shadows need the rename too, family-gated: fire only
1470 // when this family's own descriptors reference the segment.
1471 // Orphans INCLUDED: an `Outer$$x` orphan normalizes to the
1472 // nested display `x` (k=1 invisible rename), and a clean tail
1473 // renders BARE in the family file — same package shadow as a
1474 // true nested (weixin li/u0$$r → `class r` hijacked `r.a` for
1475 // root package r).
1476 let pkg_shadow = fam_segs
1477 .get(root)
1478 .is_some_and(|segs| segs.contains(tail));
1479 let clash = chain.iter().any(|c| c == tail) || field_clash || pkg_shadow;
1480 // Stage-2 queue: NO descriptor trigger, but the tail equals a
1481 // top-level package segment — the victim ref may be body-level
1482 // only. ORPHANS included: their pass-1 k=1 mint is the invisible
1483 // normalization (`a$$a` → display `a`), which leaves the package
1484 // shadow fully alive (weixin e32/a$$a hijacking `a.b.c()` ×169).
1485 if !clash && pkg_segments.contains(tail) {
1486 body_cands.push((name.clone(), root.to_string(), tail.to_string()));
1487 }
1488 if !orphan && !clash {
1489 continue;
1490 }
1491 // The enclosing class's field names (sanitized like the renderer)
1492 // — only needed once a rename actually fires, to keep the new
1493 // tail from being obscured again. Computed lazily so progressive
1494 // pools don't materialize parents for rules that don't rename.
1495 let mut enc_fields: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1496 if field_clash || pkg_shadow {
1497 if let Some(pc) = pool.get(&parent) {
1498 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
1499 enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
1500 }
1501 }
1502 }
1503 let mut k = 0u32;
1504 loop {
1505 k += 1;
1506 let cand_tail = if k == 1 {
1507 tail.to_string()
1508 } else {
1509 format!("{tail}{k}")
1510 };
1511 // The candidate must clear every ancestor display name (javac
1512 // checks the whole chain) AND every enclosing field name (else
1513 // the renamed type is just obscured again). Sibling nested
1514 // tails are covered by the pool.has_name check below.
1515 if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
1516 continue;
1517 }
1518 // Package shadow: the renamed tail becomes a member type of
1519 // the root file's class, outranking any package of the same
1520 // name for qualified refs in that file. Same-package top-level
1521 // classes likewise outrank-then-break in-file simple refs.
1522 // BOTH checks apply to field-clash renames only: for the
1523 // orphan rule the k=1 candidate is the natural display name
1524 // (`g0$$r` → `g0$r` renders identically — an invisible rename),
1525 // and rejecting it forced weixin's whole `g0$$X` Runnable
1526 // family to visible `X17` renames, breaking every lucky
1527 // `g0.r`-style resolution corpus-wide (+2911 `变量 r` — the
1528 // orphan rule is ungated by design, predating LOCAL-OK).
1529 if field_clash || pkg_shadow {
1530 if pkg_segments.contains(&cand_tail) {
1531 continue;
1532 }
1533 if pkg_shadow
1534 && fam_segs
1535 .get(root)
1536 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1537 {
1538 continue;
1539 }
1540 if let Some((root_pkg, _)) = root.rsplit_once('/') {
1541 let sib = format!("{root_pkg}/{cand_tail}");
1542 if pool.has_name(&sib) || taken_sibling_displays.contains(&sib)
1543 {
1544 // A top-level sibling exists — or an EARLIER rule
1545 // minted that display (obscuring_class_renames
1546 // moved ssosdk/b → ssosdk/b2 while this loop was
1547 // about to mint nested $b → b2: two `b2`s in one
1548 // scope, member type outranks the package sibling
1549 // and hijacked every bare `b2.b(..)` call).
1550 continue;
1551 }
1552 taken_sibling_displays.insert(sib);
1553 }
1554 }
1555 let cand = format!("{disp_parent}${cand_tail}");
1556 if pool.has_name(&cand) || assigned.contains(&cand) {
1557 continue;
1558 }
1559 assigned.insert(cand.clone());
1560 map.insert(name.clone(), cand);
1561 break;
1562 }
1563 }
1564 // Stage 2 (body-gated package shadow): one body-level image scan
1565 // over the queued families (family_ref_segments — the field-
1566 // deshadow mechanism; instruction-level, cross-package refs), then
1567 // mint for the families whose members actually reference the
1568 // segment. Mirrors obscuring_class_renames' two-stage design at
1569 // family level. Lazy pools skip (same gate as stage 1's scan).
1570 let mut body_renamed = 0usize;
1571 if !body_cands.is_empty() && pool_majority_materialized(pool) {
1572 let roots: jdc_core::FxHashSet<&str> =
1573 body_cands.iter().map(|(_, r, _)| r.as_str()).collect();
1574 let mut members: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1575 for n in &pool.order {
1576 let r = match n.find('$') {
1577 Some(i) => &n[..i],
1578 None => n.as_str(),
1579 };
1580 if roots.contains(r) {
1581 members.insert(n.clone());
1582 }
1583 }
1584 let t0 = std::time::Instant::now();
1585 let (by_referrer, _) = refscan::family_ref_segments(&pool.dexes, &members);
1586 // Referrer-keyed → family-root-keyed: every member renders in
1587 // the root file's scope.
1588 let mut root_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
1589 jdc_core::FxHashMap::default();
1590 for (referrer, segs) in &by_referrer {
1591 let r = referrer.split('$').next().unwrap_or(referrer);
1592 root_segs.entry(r).or_default().extend(segs.iter().map(|s| s.as_str()));
1593 }
1594 if std::env::var("DDC_STATS").is_ok() {
1595 eprintln!(
1596 "[renames] nested body-shadow scan: cands={} families={} scan={:?}",
1597 body_cands.len(),
1598 root_segs.len(),
1599 t0.elapsed()
1600 );
1601 }
1602 for (name, root, tail) in &body_cands {
1603 if !root_segs
1604 .get(root.as_str())
1605 .is_some_and(|s| s.contains(tail.as_str()))
1606 {
1607 continue;
1608 }
1609 if let Some(d) = map.get(name) {
1610 // Pass-1 orphan mints are invisible (display tail ==
1611 // tail) — those get re-minted visibly here. Anything
1612 // already visibly renamed stays as the earlier rule set it.
1613 let cur_tail = d.rsplit(['/', '$']).next().unwrap_or("");
1614 if cur_tail != tail.as_str() {
1615 continue;
1616 }
1617 }
1618 // Fresh display context: stage-1 renames may have moved the
1619 // parent/chain since this candidate was queued.
1620 let Some(parent) = find_outer_name(pool, name) else {
1621 continue;
1622 };
1623 let Some(rest) = name
1624 .strip_prefix(parent.as_str())
1625 .and_then(|t| t.strip_prefix('$'))
1626 else {
1627 continue;
1628 };
1629 if !clean_member_tail(rest) {
1630 continue;
1631 }
1632 let mut chain: Vec<String> = Vec::new();
1633 let mut cur = parent.clone();
1634 loop {
1635 let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
1636 let simple = disp.rsplit('/').next().unwrap_or(&disp);
1637 chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
1638 match find_outer_name(pool, &cur) {
1639 Some(o) if o != cur => cur = o,
1640 _ => break,
1641 }
1642 }
1643 let disp_parent = display_of(&parent, map);
1644 let mut enc_fields: jdc_core::FxHashSet<String> =
1645 jdc_core::FxHashSet::default();
1646 if let Some(pc) = pool.get(&parent) {
1647 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
1648 enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
1649 }
1650 }
1651 let mut k = 0u32;
1652 loop {
1653 k += 1;
1654 let cand_tail = if k == 1 {
1655 tail.clone()
1656 } else {
1657 format!("{tail}{k}")
1658 };
1659 if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
1660 continue;
1661 }
1662 // The renamed tail stays a member type of the root
1663 // file — it must not shadow ANOTHER package segment the
1664 // family references (descriptor OR body level) nor a
1665 // same-package top-level sibling.
1666 if pkg_segments.contains(&cand_tail) {
1667 continue;
1668 }
1669 if fam_segs
1670 .get(root.as_str())
1671 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1672 {
1673 continue;
1674 }
1675 if root_segs
1676 .get(root.as_str())
1677 .is_some_and(|segs| segs.contains(cand_tail.as_str()))
1678 {
1679 continue;
1680 }
1681 if let Some((root_pkg, _)) = root.rsplit_once('/') {
1682 let sib = format!("{root_pkg}/{cand_tail}");
1683 if pool.has_name(&sib) || taken_sibling_displays.contains(&sib) {
1684 continue;
1685 }
1686 taken_sibling_displays.insert(sib);
1687 }
1688 let cand = format!("{disp_parent}${cand_tail}");
1689 if pool.has_name(&cand) || assigned.contains(&cand) {
1690 continue;
1691 }
1692 // Repair pass-1 mints whose display chain ran through
1693 // THIS name's old display: stage 2 renames ancestors
1694 // after descendants were minted (weibo mobileads k$a →
1695 // a5 left pass-1's k$a$b$a → "k$a$b$a2" pointing through
1696 // the dead `a` — 找不到符号 类 a ×48). Ancestor-first
1697 // within each pass, but stage 2 as a whole runs last.
1698 let old_disp = format!("{disp_parent}${tail}");
1699 if old_disp != cand {
1700 for (mk, mv) in map.iter_mut() {
1701 if mk == name {
1702 continue;
1703 }
1704 if let Some(rest) = mv.strip_prefix(old_disp.as_str()) {
1705 if rest.starts_with('$') {
1706 let repaired = format!("{cand}{rest}");
1707 assigned.insert(repaired.clone());
1708 *mv = repaired;
1709 }
1710 }
1711 }
1712 }
1713 assigned.insert(cand.clone());
1714 map.insert(name.clone(), cand);
1715 body_renamed += 1;
1716 break;
1717 }
1718 }
1719 }
1720 if body_renamed > 0 && std::env::var("DDC_STATS").is_ok() {
1721 eprintln!("[renames] nested body-shadow renames: {body_renamed}");
1722 }
1723}
1724
1725/// Field-obscuring rename candidates: nested member types whose simple
1726/// name equals a field of the enclosing class, mapped to the top-level
1727/// class whose file renders them (the LOCAL-OK family root). Mirrors the
1728/// skip conditions of `nested_collision_renames` (same-name entries must
1729/// agree, or the gate proves locality for a rename that never fires — or
1730/// worse, skips one that does).
1731fn field_clash_cands(pool: &DexPool, map: &HashMap<String, String>) -> HashMap<String, String> {
1732 let mut out: HashMap<String, String> = HashMap::default();
1733 for name in &pool.order {
1734 if !name.contains('$') || map.contains_key(name) {
1735 continue;
1736 }
1737 let Some(parent) = find_outer_name(pool, name) else {
1738 continue;
1739 };
1740 let Some(rest) = name
1741 .strip_prefix(parent.as_str())
1742 .and_then(|t| t.strip_prefix('$'))
1743 else {
1744 continue;
1745 };
1746 if !clean_member_tail(rest) {
1747 continue;
1748 }
1749 let tail = rest.rsplit('$').next().unwrap_or(rest);
1750 let Some(pc) = pool.get(&parent) else {
1751 continue;
1752 };
1753 let clash = pc
1754 .static_fields
1755 .iter()
1756 .chain(pc.instance_fields.iter())
1757 .any(|f| crate::classdec::java_ident(&f.name).as_ref() == tail);
1758 if !clash {
1759 continue;
1760 }
1761 let mut root = parent.clone();
1762 loop {
1763 match find_outer_name(pool, &root) {
1764 Some(o) if o != root => root = o,
1765 _ => break,
1766 }
1767 }
1768 out.insert(name.clone(), root);
1769 }
1770 out
1771}
1772
1773/// Whether the pool is (majority-)materialized: full-decompile pipelines
1774/// call `materialize_all` before installing renames, progressive browse
1775/// pools materialize a handful of classes on demand. The refscan gate
1776/// must not run (or stall startup) in the progressive case.
1777fn pool_majority_materialized(pool: &DexPool) -> bool {
1778 let (mut mat, mut lazy) = (0usize, 0usize);
1779 for e in pool.classes.values() {
1780 match e {
1781 ClassEntry::Eager(_) => mat += 1,
1782 ClassEntry::Lazy { pc, .. } => {
1783 if pc.get().is_some() {
1784 mat += 1;
1785 } else {
1786 lazy += 1;
1787 }
1788 }
1789 }
1790 }
1791 lazy == 0 || mat >= lazy
1792}
1793
1794/// Compute and install the registry (call before worker threads spawn).
1795/// CURRENT resident size (MB) — ru_maxrss is monotone (historical peak),
1796/// so purge effects are invisible there; live-vs-retained attribution
1797/// needs the instantaneous figure (macOS mach task_info).
1798#[cfg(target_vendor = "apple")]
1799fn current_rss_mb() -> f64 {
1800 #[repr(C)]
1801 struct TaskBasicInfo {
1802 virtual_size: u64,
1803 resident_size: u64,
1804 resident_size_max: u64,
1805 // time_value_t = {i32 secs, i32 usec} = 8B each.
1806 total_user_time: u64,
1807 total_sys_time: u64,
1808 policy: i32,
1809 suspend_count: i32,
1810 }
1811 extern "C" {
1812 fn mach_task_self() -> u32;
1813 fn task_info(
1814 target_port: u32,
1815 flavor: i32,
1816 task_info_out: *mut TaskBasicInfo,
1817 task_info_out_cnt: *mut u32,
1818 ) -> i32;
1819 }
1820 let mut info = TaskBasicInfo {
1821 virtual_size: 0,
1822 resident_size: 0,
1823 resident_size_max: 0,
1824 total_user_time: 0,
1825 total_sys_time: 0,
1826 policy: 0,
1827 suspend_count: 0,
1828 };
1829 // count is in natural_t (4B) units; a smaller-than-kernel version
1830 // still fills the prefix fields we read.
1831 let mut cnt = (std::mem::size_of::<TaskBasicInfo>() / 4) as u32;
1832 let kr = unsafe { task_info(mach_task_self(), 20, &mut info, &mut cnt) };
1833 if kr == 0 {
1834 info.resident_size as f64 / (1024.0 * 1024.0)
1835 } else {
1836 0.0
1837 }
1838}
1839#[cfg(not(target_vendor = "apple"))]
1840fn current_rss_mb() -> f64 {
1841 0.0
1842}
1843
1844/// ru_maxrss peak so far (MB). macOS reports bytes, Linux KB; Windows
1845/// has no getrusage (the old not(apple) fallback referenced it and
1846/// failed the MSVC link — LNK2019 — in the v0.1.18 CI).
1847#[cfg(unix)]
1848fn peak_rss_mb() -> f64 {
1849 #[repr(C)]
1850 struct Rusage {
1851 pad: [i64; 4],
1852 maxrss: i64,
1853 // Oversized on purpose: the kernel writes sizeof(struct rusage)
1854 // (~144B on macOS) regardless of what we read back.
1855 tail: [i64; 16],
1856 }
1857 extern "C" {
1858 fn getrusage(who: i32, r: *mut Rusage) -> i32;
1859 }
1860 let mut r = Rusage {
1861 pad: [0; 4],
1862 maxrss: 0,
1863 tail: [0; 16],
1864 };
1865 unsafe { getrusage(0, &mut r) };
1866 #[cfg(target_vendor = "apple")]
1867 let scale = 1024.0 * 1024.0;
1868 #[cfg(not(target_vendor = "apple"))]
1869 let scale = 1024.0;
1870 r.maxrss as f64 / scale
1871}
1872#[cfg(not(unix))]
1873fn peak_rss_mb() -> f64 {
1874 0.0
1875}
1876
1877/// DDC_STATS phase attribution of the RSS high-water mark: peak AND
1878/// instantaneous live (ru_maxrss is monotone, so purge effects are
1879/// invisible there).
1880pub(crate) fn stats_rss(phase: &str) {
1881 if std::env::var("DDC_STATS").is_ok() {
1882 eprintln!(
1883 "[rss] {phase}: peak {:.0} MB, live {:.0} MB",
1884 peak_rss_mb(),
1885 current_rss_mb()
1886 );
1887 }
1888}
1889
1890pub fn install_case_renames(pool: &DexPool) {
1891 let mut map = case_rename_map(pool);
1892 crate::stats_rss(" case_rename_map");
1893 // Cross-package reference first segments (descriptor level). Lazy
1894 // (progressive-browse) pools skip the scan — materializing every
1895 // class must not stall single-class queries; the obscuring rules
1896 // then simply never fire (browse mode has no whole-program javac).
1897 let (pkg_segs, fam_segs) = if pool_majority_materialized(pool) {
1898 ref_segments(pool)
1899 } else {
1900 (
1901 jdc_core::FxHashMap::default(),
1902 jdc_core::FxHashMap::default(),
1903 )
1904 };
1905 // Package-leaf shadows FIRST: the nested-collision renames compute
1906 // display chains off the (renamed) parent display names — running
1907 // them before the parent rename left both rules minting the same
1908 // display (`a2` twice in weibo's AIDL families).
1909 crate::stats_rss(" ref_segments");
1910 pkg_leaf_shadow_renames(pool, &mut map);
1911 class_pkg_collision_renames(pool, &mut map);
1912 obscuring_class_renames(pool, &mut map, &pkg_segs);
1913 nested_collision_renames(pool, &mut map, &fam_segs);
1914 // LAST: lossy-sanitize collisions key on the display form every rule
1915 // above has already settled (`l.᩻ܶ` → `l/__.java`, 22,636 classes onto
1916 // 3 paths on bin.mt.plus — the silent-overwrite data loss).
1917 crate::stats_rss(" shadow+collision+obscuring+nested rules");
1918 lossy_sanitize_renames(pool, &mut map);
1919 // Default-package relocation keys on the settled displays too (it
1920 // PREFIXES them with the synthetic package) — runs after every
1921 // simple-name rule has minted.
1922 root_pkg_relocation(pool, &mut map);
1923 fw_shadow_relocation(pool, &mut map);
1924 crate::stats_rss(" lossy+root-pkg rules");
1925 jdc_core::rename::set_class_renames(map);
1926 crate::stats_rss("class-renames installed");
1927 // Field-rename registry build and the access-widening image scan are
1928 // independent: both only READ the pool, they install into disjoint
1929 // statics (FIELD_RENAMES vs WIDEN), the WIDEN accessors are
1930 // is_some_and-guarded with no reader before render time, and lazy
1931 // materialization is OnceLock-safe. Run them concurrently — the 1.6s
1932 // QQ widening scan fully overlaps the field-rename build.
1933 if pool_majority_materialized(pool) {
1934 std::thread::scope(|s| {
1935 let h = s.spawn(|| crate::classdec::install_access_widening(pool));
1936 jdc_core::rename::set_field_renames(combined_field_renames(pool));
1937 let _ = h.join();
1938 });
1939 crate::stats_rss("field-renames + widening installed");
1940 } else {
1941 jdc_core::rename::set_field_renames(combined_field_renames(pool));
1942 }
1943}
1944
1945/// A top-level class `P/s` whose simple name equals the FIRST PACKAGE
1946/// SEGMENT of types that other classes in P reference cross-package.
1947/// Such a reference renders as the FQN `s.rest...`, and javac binds the
1948/// first segment to the nearest type in scope — the same-package class
1949/// `P/s` (JLS 6.4.2): `n91.f.a(x)` inside pc5/bd looks up member `f` OF
1950/// CLASS pc5.n91 (weixin's n0/n91/uc6/vc6 protobuf cluster: ~12k root
1951/// files, ~90k cascade lines; weibo's `a.g.b.a.b` chains root the same
1952/// way — class a beside subpackage a/ inside package a/g/b). No Java
1953/// syntax escapes an obscured first segment (imports can bypass it, but
1954/// single-type imports are blocked exactly where obfuscated leaf names
1955/// collide with same-package siblings), so the class moves: the r66
1956/// rename mechanism at a new trigger. Gated on DESCRIPTOR-level refs of
1957/// the candidate's own package (supers/fields/protos) — body-only refs
1958/// are missed by design; the import layer covers unblocked leaves there.
1959/// Descriptor-level cross-package reference first segments, aggregated
1960/// per PACKAGE (top-level obscurers: every file in P shares the scope)
1961/// and per FAMILY root (nested obscurers: a nested simple name is in
1962/// scope only inside its own root file). Raw `L..;` scans — no JavaType
1963/// parsing. Body-level refs are missed by design (the import layer
1964/// catches unblocked leaves there); descriptors cover supers, field
1965/// types and method protos — the header positions whose breakage starts
1966/// the error-type cascades.
1967/// Package (or family) → first segments of the cross-package types its
1968/// classes reference at descriptor level. Obscuring-rename trigger sets.
1969type SegMap<'a> = jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>;
1970
1971fn ref_segments(pool: &DexPool) -> (SegMap<'_>, SegMap<'_>) {
1972 fn pkg_of(n: &str) -> &str {
1973 match n.rsplit_once('/') {
1974 Some((p, _)) => p,
1975 None => "",
1976 }
1977 }
1978 fn note<'a>(
1979 pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
1980 fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
1981 pkg: &'a str,
1982 fam: &'a str,
1983 t: &'a str,
1984 ) {
1985 if !t.contains('/') {
1986 return; // root-package target: unreferenceable from named pkgs
1987 }
1988 let _ = pkg; // same-package refs COUNT: ddc renders headers/field
1989 // types fully qualified (print_class_name), so `a.g.b.a.b`
1990 // inside package a/g/b is obscured by class a/g/b/a just
1991 // like a cross-package FQN (weibo's whole `a`-tree family).
1992 let seg = t.split('/').next().unwrap_or("");
1993 if !seg.is_empty() {
1994 pkg_segs.entry(pkg).or_default().insert(seg);
1995 fam_segs.entry(fam).or_default().insert(seg);
1996 }
1997 }
1998 fn scan<'a>(
1999 pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
2000 fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
2001 pkg: &'a str,
2002 fam: &'a str,
2003 desc: &'a str,
2004 ) {
2005 let b = desc.as_bytes();
2006 let mut i = 0usize;
2007 while i < b.len() {
2008 if b[i] == b'L' {
2009 if let Some(end) = desc[i + 1..].find(';') {
2010 note(pkg_segs, fam_segs, pkg, fam, &desc[i + 1..i + 1 + end]);
2011 i += end + 2;
2012 continue;
2013 }
2014 }
2015 i += 1;
2016 }
2017 }
2018 let mut pkg_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
2019 jdc_core::FxHashMap::default();
2020 let mut fam_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
2021 jdc_core::FxHashMap::default();
2022 for name in &pool.order {
2023 let Some(c) = pool.get(name) else { continue };
2024 let pkg = pkg_of(name);
2025 let fam = match name.find('$') {
2026 Some(i) => &name[..i],
2027 None => name.as_str(),
2028 };
2029 if let Some(sup) = &c.super_name {
2030 note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
2031 }
2032 for sup in c.interfaces.iter() {
2033 note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
2034 }
2035 for f in c.static_fields.iter().chain(c.instance_fields.iter()) {
2036 scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &f.desc);
2037 }
2038 for m in c.direct_methods.iter().chain(c.virtual_methods.iter()) {
2039 scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &m.desc);
2040 }
2041 }
2042 (pkg_segs, fam_segs)
2043}
2044
2045/// A top-level class `P/s` whose simple name equals the FIRST PACKAGE
2046/// SEGMENT of types that classes in P reference cross-package. Such a
2047/// reference renders as the FQN `s.rest...`, and javac binds the first
2048/// segment to the nearest type in scope — the same-package class `P/s`
2049/// (JLS 6.4.2, no package fallback — verified): `n91.f.a(x)` inside
2050/// pc5/bd looks up member `f` OF CLASS pc5.n91 (weixin's n0/n91/uc6/vc6
2051/// protobuf cluster: ~12k root files, ~90k cascade lines; weibo's
2052/// `a.g.b.a.b` chains root the same way). No Java syntax escapes an
2053/// obscured first segment, so the class moves: the r66 rename mechanism
2054/// at a new trigger, gated on DESCRIPTOR-level refs of the candidate's
2055/// package (body-only refs go to the import layer). Nested obscurers
2056/// (a nested simple name shadowing a package inside its family file)
2057/// are the companion trigger in nested_collision_renames.
2058fn obscuring_class_renames(
2059 pool: &DexPool,
2060 map: &mut HashMap<String, String>,
2061 pkg_segs: &SegMap,
2062) {
2063 fn pkg_of(n: &str) -> &str {
2064 match n.rsplit_once('/') {
2065 Some((p, _)) => p,
2066 None => "",
2067 }
2068 }
2069 // Root package first segments (segments some class lives under).
2070 let mut root_segs: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
2071 for n in &pool.order {
2072 if let Some(i) = n.find('/') {
2073 root_segs.insert(&n[..i]);
2074 }
2075 }
2076 let mut cands: Vec<&String> = Vec::new();
2077 for name in &pool.order {
2078 let simple = name.rsplit('/').next().unwrap_or(name);
2079 let pkg = pkg_of(name);
2080 if simple.is_empty() || simple.contains('$') || pkg.is_empty() {
2081 continue;
2082 }
2083 if !root_segs.contains(simple) {
2084 continue;
2085 }
2086 if map.get(name).is_some_and(|v| v != name) {
2087 continue; // an earlier rule moved it
2088 }
2089 // NB: NO "class lives under the s tree" exclusion. weibo's
2090 // a/a/b/c/l/a shadowed the FQN refs `a.a.a.a2` that its own
2091 // package files render for cross-subpackage targets (the
2092 // starts_with("a/") skip left the WHOLE a-tree unrenamed —
2093 // 找不到符号 类 a chains). The two-stage refsegs gates below
2094 // require an actual cross-package FQN ref for the trigger.
2095 cands.push(name);
2096 }
2097 let ncands = cands.len();
2098 if std::env::var("DDC_STATS").is_ok() {
2099 eprintln!(
2100 "[renames] obscuring candidates={ncands} pkgs-with-segs={}",
2101 pkg_segs.len()
2102 );
2103 }
2104 if cands.is_empty() {
2105 return;
2106 }
2107 // Fast collision oracle: sorted names + prefix range scan (the r66
2108 // candidate loop's `order.iter().any(starts_with)` is O(n) per
2109 // candidate — this rule can mint tens of thousands of renames).
2110 let mut sorted: Vec<&String> = pool.order.iter().collect();
2111 sorted.sort_unstable();
2112 let prefix_free = |cand: &str| -> bool {
2113 // No class named cand, nothing nested under cand$..., no package cand/...
2114 let idx = sorted.partition_point(|n| n.as_str() < cand);
2115 !sorted[idx..].first().is_some_and(|n| n.starts_with(cand))
2116 };
2117 let pkg_simples = pool.package_simples();
2118 let mut taken_displays: jdc_core::FxHashSet<String> = map.values().cloned().collect();
2119 let mut renamed = 0usize;
2120 // Stage 1: descriptor-level hits rename now; the rest queue for the
2121 // BODY-level image scan (weixin pc5's `invoke-static Ln91/f;.a` —
2122 // 7.8k+6.6k error lines the descriptor gate cannot see).
2123 let mut stage2: Vec<&String> = Vec::new();
2124 for name in cands {
2125 let pkg = pkg_of(name);
2126 let simple = name.rsplit('/').next().unwrap_or(name);
2127 if !pkg_segs.get(pkg).is_some_and(|segs| segs.contains(simple)) {
2128 stage2.push(name);
2129 continue;
2130 }
2131 let siblings = pkg_simples.get(pkg);
2132 let mut k = 1u32;
2133 loop {
2134 k += 1;
2135 let new_simple = format!("{simple}{k}");
2136 let cand = if pkg.is_empty() {
2137 new_simple.clone()
2138 } else {
2139 format!("{pkg}/{new_simple}")
2140 };
2141 // Exact/prefix pool clash, another rule's display target, the
2142 // package's own referenced segments (the new name must not
2143 // obscure ANOTHER package the same files reference), or a
2144 // case-insensitive clash with a same-package sibling
2145 // (case-insensitive filesystems fold the files).
2146 let clash = !prefix_free(&cand)
2147 || taken_displays.contains(&cand)
2148 || pkg_segs
2149 .get(pkg)
2150 .is_some_and(|segs| segs.contains(new_simple.as_str()))
2151 || siblings.is_some_and(|s| {
2152 s.iter()
2153 .any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
2154 });
2155 if !clash {
2156 taken_displays.insert(cand.clone());
2157 map.insert(name.clone(), cand);
2158 renamed += 1;
2159 break;
2160 }
2161 }
2162 }
2163 let mut body_renamed = 0usize;
2164 if !stage2.is_empty() && pool_majority_materialized(pool) {
2165 let stage2_pkgs: jdc_core::FxHashSet<String> =
2166 stage2.iter().map(|n| pkg_of(n).to_string()).collect();
2167 let body_segs = crate::refscan::body_ref_segments(&pool.dexes, &stage2_pkgs);
2168 for name in stage2 {
2169 let pkg = pkg_of(name);
2170 let simple = name.rsplit('/').next().unwrap_or(name);
2171 if !body_segs
2172 .get(pkg)
2173 .is_some_and(|segs| segs.contains(simple))
2174 {
2175 continue;
2176 }
2177 let siblings = pkg_simples.get(pkg);
2178 let mut k = 1u32;
2179 loop {
2180 k += 1;
2181 let new_simple = format!("{simple}{k}");
2182 let cand = format!("{pkg}/{new_simple}");
2183 let clash = !prefix_free(&cand)
2184 || taken_displays.contains(&cand)
2185 || pkg_segs
2186 .get(pkg)
2187 .is_some_and(|segs| segs.contains(new_simple.as_str()))
2188 || body_segs
2189 .get(pkg)
2190 .is_some_and(|segs| segs.contains(new_simple.as_str()))
2191 || siblings.is_some_and(|s| {
2192 s.iter()
2193 .any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
2194 });
2195 if !clash {
2196 taken_displays.insert(cand.clone());
2197 map.insert(name.clone(), cand);
2198 renamed += 1;
2199 body_renamed += 1;
2200 break;
2201 }
2202 }
2203 }
2204 }
2205 if std::env::var("DDC_STATS").is_ok() {
2206 eprintln!(
2207 "[renames] obscuring-class renames: {renamed} (body-gated {body_renamed}; pkgs-with-segs={}, cands={ncands})",
2208 pkg_segs.len()
2209 );
2210 }
2211}
2212
2213/// Class-level counterpart of the non-ASCII rule already applied to
2214/// MEMBERS by `member_collision_renames`, kept as the SAFETY NET behind
2215/// the injective `sanitize_fq`.
2216///
2217/// `sanitize_fq` escapes non-identifier characters as `_u<hex>`, so two
2218/// distinct classes normally reach two distinct file names. It cannot be
2219/// injective against *lookalikes*, because ident-safe ASCII must pass
2220/// through unchanged: a literal class named `_u1a7b` collides with the
2221/// class whose single character is U+1A7B, and the pre-existing keyword
2222/// repair makes `_do` collide with `do`. Without this pass those pairs
2223/// repeat the original disaster — the writer opens with `create_new`,
2224/// takes EEXIST, unlinks and rewrites, so one class is destroyed with
2225/// exit code 0 (on bin.mt.plus the lossy predecessor of this rule hid
2226/// 22,633 of 30,768 classes exactly this way).
2227///
2228/// Groups are keyed by the SANITIZED path — the exact string both the
2229/// declaration site (`sanitize_ref`) and the writer (`source_path`)
2230/// produce. The first (sorted) member of each group keeps its name; the
2231/// rest become `<sanitized>_2`, `<sanitized>_3`, … Pure ASCII, so a
2232/// minted name is a fixed point of the sanitizer: it cannot silently
2233/// join a second collision group, and the declaration, every reference
2234/// and the file name agree by construction through `apply_class_rename`.
2235///
2236/// Costs nothing on a healthy pool: one pass over the emission set, and
2237/// it returns before touching the registry when every key is unique.
2238/// Runs LAST in `install_case_renames`: it keys on the display form the
2239/// earlier rules have already settled.
2240fn lossy_sanitize_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
2241 let names = top_level_classes(pool);
2242 // Current display of an emission-set name. `case_rename_map` has
2243 // already inserted an entry for every one of these (identity or
2244 // renamed) and an exact registry hit short-circuits
2245 // `apply_class_rename`, so `map` already holds the final display —
2246 // no `$`-prefix walk is needed here.
2247 fn key_of(map: &HashMap<String, String>, n: &str) -> String {
2248 let disp = map.get(n).map(|s| s.as_str()).unwrap_or(n);
2249 sanitize_internal(disp)
2250 }
2251 let mut counts: HashMap<String, u32> = HashMap::default();
2252 for n in &names {
2253 *counts.entry(key_of(map, n)).or_insert(0) += 1;
2254 }
2255 if counts.values().all(|c| *c == 1) {
2256 return;
2257 }
2258 // Every sanitized path in the POOL is off-limits for a minted name,
2259 // not just the emission set: an inlined nested class still owns the
2260 // display it renders under inside its outer's file.
2261 let mut taken: jdc_core::FxHashSet<String> = pool
2262 .order
2263 .iter()
2264 .map(|n| sanitize_internal(map.get(n).map(|s| s.as_str()).unwrap_or(n)))
2265 .collect();
2266 let mut sorted: Vec<&String> = names.iter().collect();
2267 sorted.sort();
2268 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2269 let mut renamed = 0usize;
2270 for n in sorted {
2271 let key = key_of(map, n);
2272 if counts.get(&key).copied().unwrap_or(0) < 2 {
2273 continue;
2274 }
2275 if seen.insert(key.clone()) {
2276 continue; // first (sorted) member of the group keeps its name
2277 }
2278 let (pkg, simple) = match key.rsplit_once('/') {
2279 Some((p, s)) => (p, s),
2280 None => ("", key.as_str()),
2281 };
2282 let mut i = 1u32;
2283 loop {
2284 i += 1;
2285 let cand = if pkg.is_empty() {
2286 format!("{simple}_{i}")
2287 } else {
2288 format!("{pkg}/{simple}_{i}")
2289 };
2290 if !taken.insert(cand.clone()) {
2291 continue; // already owned by a real class or another mint
2292 }
2293 // The minted display IS the sanitized name, so
2294 // sanitize_internal(cand) == cand and the writer's file name
2295 // matches the declaration it writes into it.
2296 map.insert(n.clone(), cand);
2297 renamed += 1;
2298 break;
2299 }
2300 }
2301 if std::env::var("DDC_STATS").is_ok() {
2302 eprintln!("[renames] lossy-sanitize collisions renamed={renamed}");
2303 }
2304}
2305
2306/// A class named `P/s` while OTHER classes live under `P/s/` — the dex
2307/// namespace is flat, so obfuscators freely mint a class whose simple
2308/// name equals a subpackage of its own package (`ptr.a` +
2309/// `ptr.a.a`, weibo rsplay/ptr families). That namespace is
2310/// unrepresentable in Java: the declaration itself is
2311/// "类 a与带有相同名称的程序包冲突" and every qualified reference
2312/// through the segment becomes unresolvable (the 53.8k weibo
2313/// missing-class errors root here, with Object/getClass cascades on
2314/// top). Rename the class — the registry carries declarations and
2315/// references — with a candidate that cannot recreate the shape (not a
2316/// class, not a package, not a nested-class prefix, not another rule's
2317/// target).
2318fn class_pkg_collision_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
2319 // Every package that exists in the pool — ALL ancestor prefixes of
2320 // every class path, not just direct parents: Java sees package
2321 // `z` as existing when any class lives at `z/a/b` (the weibo
2322 // `com.sina.weibo.z` interface collides with a subpackage-only
2323 // `z/` that has no directly-resident class).
2324 let mut pkgs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2325 for n in &pool.order {
2326 let mut rest = n.as_str();
2327 while let Some(i) = rest.rfind('/') {
2328 rest = &rest[..i];
2329 pkgs.insert(rest.to_string());
2330 }
2331 }
2332 for name in &pool.order {
2333 let Some((pkg, simple)) = name.rsplit_once('/') else {
2334 continue;
2335 };
2336 if simple.is_empty() || pkg.is_empty() || simple.contains('$') {
2337 // Nested classes render dotted (`Outer.a`) — no package
2338 // effect from their `$` tail.
2339 continue;
2340 }
2341 if !pkgs.contains(name) || map.get(name).is_some_and(|v| v != name) {
2342 continue; // no subpackage under it, or an earlier rule moved it
2343 }
2344 let mut k = 1u32;
2345 loop {
2346 k += 1;
2347 let cand = format!("{pkg}/{simple}{k}");
2348 let clash = pkgs.contains(&cand)
2349 || pool.order.iter().any(|n| n.starts_with(&cand))
2350 || map.values().any(|v| *v == cand);
2351 if !clash {
2352 map.insert(name.clone(), cand);
2353 break;
2354 }
2355 }
2356 }
2357}
2358
2359/// Obfuscators can name a class after its own package leaf (`package k;`
2360/// `class k extends EditText`). Every SAME-PACKAGE reference to `k.Anything`
2361/// then resolves the first segment to the CLASS, not the package —
2362/// javac looks for `Anything` as a member of class k ("找不到符号 class p2,
2363/// location: class k"). Renaming the class (display-level, the registry
2364/// carries declarations and references) makes the package the only
2365/// thing the leaf name resolves to.
2366fn pkg_leaf_shadow_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
2367 let taken: jdc_core::FxHashSet<String> = pool.order.iter().cloned().collect();
2368 for name in &pool.order {
2369 let Some((pkg, simple)) = name.rsplit_once('/') else {
2370 continue;
2371 };
2372 if simple.is_empty() || pkg.is_empty() {
2373 continue;
2374 }
2375 let leaf = pkg.rsplit('/').next().unwrap_or(pkg);
2376 // class name == package leaf, and the package really exists
2377 // (more classes than just this one live under it).
2378 if simple != leaf {
2379 continue;
2380 }
2381 let prefix = format!("{pkg}/");
2382 let siblings = pool
2383 .order
2384 .iter()
2385 .filter(|n| n.starts_with(&prefix) && **n != *name)
2386 .count();
2387 if siblings == 0 || map.get(name).is_some_and(|v| v != name) {
2388 continue; // already renamed by an earlier rule; identity anchors are fine to overwrite
2389 }
2390 let mut k = 1u32;
2391 loop {
2392 k += 1;
2393 let cand = format!("{pkg}/{simple}{k}");
2394 if !taken.contains(&cand) && !map.values().any(|v| *v == cand) {
2395 map.insert(name.clone(), cand);
2396 break;
2397 }
2398 }
2399 }
2400}
2401
2402/// Member-level collision renames (fields AND methods of one class).
2403/// Two sources collapse onto one display name: obfuscators renaming a
2404/// synthetic outer reference (`this$0`) onto a real field's name, and
2405/// the identifier sanitizer mapping non-ASCII to `_` (whole
2406/// `ERROR_中文` families render one `ERROR________`).
2407/// Both are legal in bytecode (members resolve by index) and both are
2408/// javac "already defined" errors in source. Policy per colliding group
2409/// (fields keep the first non-synthetic name; a synthetic outer
2410/// reference restores `this$0`; everything else gets `2`/`3` suffixes;
2411/// method keys include erased parameter types):
2412/// Keys stay ORIGINAL (name, descriptor) so references resolve exactly;
2413/// only the rendered identifier changes.
2414/// Descriptor type string → internal class name (arrays/primitives →
2415/// None).
2416/// Can a method returning descriptor `m_ret` override an ancestor
2417/// declaration returning `r`? Equal, or a covariant subtype. ARRAY
2418/// returns must reach is_subtype in DESCRIPTOR form (it peels `[` for
2419/// element covariance): the old `strip L…;`-only comparison returned
2420/// false for `SavedState[] <: Object[]`, so Parcelable.Creator's real
2421/// `newArray(I)[LSavedState;` was judged an IMPOSTOR and renamed
2422/// (newArray3 — m_taken already held newArray + the bridge's newArray2),
2423/// breaking the interface override: "X$1不是抽象的, 并且未覆盖…
2424/// newArray(int)" (weibo ×504, battery ~789, rimet/news/uuyc ~655).
2425/// Object returns keep the internal-name form is_subtype's pool/fwdb
2426/// walk expects; an array m_ret against an object r resolves through
2427/// is_subtype's `sup == Object` / fwdb path (arrays ARE Objects).
2428fn ret_satisfies(pool: &DexPool, m_ret: &str, r: &str) -> bool {
2429 if m_ret == r {
2430 return true;
2431 }
2432 if m_ret.starts_with('[') {
2433 let sup: std::borrow::Cow<str> = if r.starts_with('[') {
2434 std::borrow::Cow::Borrowed(r)
2435 } else {
2436 match r.strip_prefix('L').and_then(|x| x.strip_suffix(';')) {
2437 Some(rc) => std::borrow::Cow::Borrowed(rc),
2438 None => return false, // primitive sup: only equality (above)
2439 }
2440 };
2441 return pool.is_subtype(m_ret, &sup);
2442 }
2443 if r.starts_with('[') {
2444 return false; // nothing but an array satisfies an array return
2445 }
2446 match (
2447 r.strip_prefix('L').and_then(|x| x.strip_suffix(';')),
2448 m_ret.strip_prefix('L').and_then(|x| x.strip_suffix(';')),
2449 ) {
2450 (Some(rc), Some(mc)) => pool.is_subtype(mc, rc),
2451 _ => false,
2452 }
2453}
2454
2455/// Raw name → (argsig, ret) JVM-descriptor slices of one class's own
2456/// virtual methods — the key space the collision queries use.
2457type AncMethods = jdc_core::FxHashMap<
2458 std::sync::Arc<str>,
2459 Vec<(std::sync::Arc<str>, std::sync::Arc<str>)>,
2460>;
2461
2462/// One provenance bucket of an analyzed class's ancestor chain (cached
2463/// layers, no merge — see `layer_rets`).
2464type AncLayers = Vec<std::sync::Arc<AncEntry>>;
2465
2466/// Parsed direct virtual methods of one ancestor class + its chain
2467/// edges, cached per class name for the member-collision walk. The
2468/// framework chains (View/Activity/Fragment…) and pool hub supers repeat
2469/// across thousands of analyzed classes — parsing each ancestor's
2470/// descriptors ONCE per run (instead of per analyzed subclass) removes
2471/// the dominant main-loop cost.
2472struct AncEntry {
2473 methods: AncMethods,
2474 /// Provenance bucket: FRAMEWORK ancestors (fwdb classes AND
2475 /// desugared `java.*`/`javax.*` pool copies — javac compiles against
2476 /// the android.jar originals, the covariant source truth) vs POOL
2477 /// ancestors (rendered RAW — the erased bridge is what satisfies the
2478 /// raw interface). The keeper preference flips on this axis (weibo
2479 /// Function0.invoke bridge must keep the name; WhatsApp SortedMap
2480 /// .keySet real override must).
2481 is_fw: bool,
2482 /// super + direct interfaces (chain continuation), Object-stripped.
2483 supers: Vec<std::sync::Arc<str>>,
2484}
2485
2486fn parse_anc_entry(pool: &DexPool, cn: &str) -> std::sync::Arc<AncEntry> {
2487 let mut methods: AncMethods = jdc_core::FxHashMap::default();
2488 let mut supers: Vec<std::sync::Arc<str>> = Vec::new();
2489 let is_fw;
2490 if let Some(ac) = pool.get_if_materialized(cn) {
2491 // Desugared `java.*`/`javax.*` copies in the dex are
2492 // pool-materialized, but javac compiles against the android.jar
2493 // ORIGINALS (covariant generics truth) — their declarations
2494 // belong to the fw bucket (WhatsApp bundles desugared
2495 // java.util.SortedMap; the pool bucket kept the
2496 // flattened-erasure bridge and renamed the real SortedSet
2497 // keySet).
2498 is_fw = cn.starts_with("java/") || cn.starts_with("javax/");
2499 for m in ac.all_methods() {
2500 if m.is_static() || &*m.name == "<init>" || &*m.name == "<clinit>"
2501 {
2502 continue;
2503 }
2504 let d: &str = &m.desc;
2505 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2506 let hi = d.find(')').unwrap_or(d.len());
2507 methods
2508 .entry(m.name.clone())
2509 .or_default()
2510 .push((
2511 std::sync::Arc::from(&d[lo..hi]),
2512 std::sync::Arc::from(&d[hi + 1..]),
2513 ));
2514 }
2515 if let Some(s) = &ac.super_name {
2516 if s != "java/lang/Object" {
2517 supers.push(std::sync::Arc::from(s.as_str()));
2518 }
2519 }
2520 supers.extend(ac.interfaces.iter().map(|i| std::sync::Arc::from(i.as_str())));
2521 } else {
2522 // FRAMEWORK ancestors bind names too: a group member
2523 // implementing a framework declaration must keep the original
2524 // name (renaming WhatsApp's Map.keySet-style overrides broke
2525 // the interface chain — cannot-find:变量 ×18.6k). fwdb
2526 // descriptors are erased JVM form — directly comparable with
2527 // dex descs. A class that is NEITHER pool nor fwdb (phantom)
2528 // parses to an empty entry — a chain dead-end, same as before.
2529 is_fw = true;
2530 crate::fwdb::for_each_method(cn, |d, f| {
2531 if f & crate::fwdb::MF_STATIC != 0 {
2532 return;
2533 }
2534 let Some(open) = d.find('(') else {
2535 return;
2536 };
2537 let lo = open + 1;
2538 let hi = d.find(')').unwrap_or(0);
2539 // NOTE: hi == lo is the zero-arg case, not malformed; and
2540 // the name slice ends at `open` (a `d[..lo]` cut kept the
2541 // `(` — every fw key missed its gkey lookup).
2542 if hi == 0 || hi + 1 >= d.len() {
2543 return;
2544 }
2545 methods
2546 .entry(std::sync::Arc::from(&d[..open]))
2547 .or_default()
2548 .push((
2549 std::sync::Arc::from(&d[lo..hi]),
2550 std::sync::Arc::from(&d[hi + 1..]),
2551 ));
2552 });
2553 if let Some(sup) = crate::fwdb::super_of(cn) {
2554 if sup != "java/lang/Object" {
2555 supers.push(std::sync::Arc::from(sup));
2556 }
2557 }
2558 crate::fwdb::for_each_interface(cn, |i| supers.push(std::sync::Arc::from(i)));
2559 }
2560 std::sync::Arc::new(AncEntry {
2561 methods,
2562 is_fw,
2563 supers,
2564 })
2565}
2566
2567/// Rets declared for `(name, argsig)` by any layer of a chain — lazy,
2568/// no merge (every consumer is an "any ancestor declares this" query,
2569/// so a per-layer scan replaces the old per-class eager map build).
2570/// Alloc-free: `Arc<str>: Borrow<str>` keys the hash lookup by `&str`.
2571fn layer_rets<'a>(
2572 layers: &'a [std::sync::Arc<AncEntry>],
2573 name: &'a str,
2574 argsig: &'a str,
2575) -> impl Iterator<Item = &'a str> {
2576 layers
2577 .iter()
2578 .filter_map(move |e| e.methods.get(name))
2579 .flatten()
2580 .filter(move |(a, _)| &**a == argsig)
2581 .map(|(_, r)| &**r)
2582}
2583
2584fn member_collision_renames(
2585 pool: &DexPool,
2586) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
2587 let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
2588 HashMap::default();
2589 // Direct nested tails per base class (`C$a` → a for base C), for
2590 // the companion-field rule below.
2591 let mut child_tails: jdc_core::FxHashMap<&str, Vec<&str>> =
2592 jdc_core::FxHashMap::default();
2593 // Per-package RENAMED class display heads: a MINTED field display
2594 // equal to one of these shadows same-package qualified type refs
2595 // (`a2.changeQuickRedirect` binds to the field when the class a2's
2596 // display is a2 — the inherited rule minting field a→a2 ONTO the
2597 // pkg-renamed class display created exactly this: weibo +194
2598 // cannot-find). Keyed on the POST-rename display (what refs render),
2599 // covering each class's own display too.
2600 let root_segs = pool.root_pkg_segs();
2601 let mut pkg_displays: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<String>> =
2602 jdc_core::FxHashMap::default();
2603 for n in &pool.order {
2604 let renamed = crate::apply_class_rename(n);
2605 let (pkg, simple) = match renamed.rfind('/') {
2606 Some(i) => (renamed[..i].to_string(), renamed[i + 1..].to_string()),
2607 None => (String::new(), renamed.to_string()),
2608 };
2609 let head = simple.split('$').next().unwrap_or(&simple).to_string();
2610 if !head.is_empty() {
2611 let pkg_key = n.rfind('/').map(|i| &n[..i]).unwrap_or("");
2612 let _ = pkg;
2613 pkg_displays.entry(pkg_key).or_default().insert(head);
2614 }
2615 }
2616 // Reverse super map — the obscuring rename must register under every
2617 // transitive subclass (dex refs of an inherited field may name any
2618 // hierarchy class as owner).
2619 let mut subs: jdc_core::FxHashMap<&str, Vec<&str>> = jdc_core::FxHashMap::default();
2620 for n in &pool.order {
2621 // EVERY `$` boundary: each prefix is an owner and the segment
2622 // after it one of its direct member-type tails. The old
2623 // first-segment-only map left NESTED owners (ta8/t$a, androidx
2624 // i0$e) with no member displays — their fields kept or minted
2625 // names colliding with their own member types, and in qualified
2626 // chains a VARIABLE OBSCURES a member type (JLS 6.4.2):
2627 // `ta8.t.a.a100.a` (Kotlin-object singleton sget, nova
2628 // static-ctx ×1,281) and `i0.e.b.ADDING` (lark ×145) bound to
2629 // the field instead of the nested class.
2630 for (i, _) in n.match_indices('$') {
2631 let owner = &n[..i];
2632 let rest = &n[i + 1..];
2633 let tail = match rest.find('$') {
2634 Some(j) => &rest[..j],
2635 None => rest,
2636 };
2637 if owner.is_empty() || tail.is_empty() {
2638 continue;
2639 }
2640 let v = child_tails.entry(owner).or_default();
2641 if !v.contains(&tail) {
2642 v.push(tail);
2643 }
2644 }
2645 if let Some(pc) = pool.get_if_materialized(n) {
2646 if let Some(sup) = &pc.super_name {
2647 if sup != "java/lang/Object" {
2648 subs.entry(sup.as_str()).or_default().push(n.as_str());
2649 }
2650 }
2651 }
2652 }
2653 // Per-ancestor parse cache (function-local: lives exactly as long
2654 // as this rename install, so materialization state is consistent by
2655 // construction; freed on return — no cross-run staleness, no RSS
2656 // tail).
2657 let mut anc_cache: jdc_core::FxHashMap<
2658 std::sync::Arc<str>,
2659 std::sync::Arc<AncEntry>,
2660 > = jdc_core::FxHashMap::default();
2661 for name in &pool.order {
2662 let Some(pc) = pool.get_if_materialized(name) else {
2663 continue;
2664 };
2665 let outer_ref_desc = find_outer_name(pool, name)
2666 .filter(|o| o != name)
2667 .map(|o| format!("L{o};"));
2668 // ---- fields: one shared namespace (static + instance) ----
2669 let fields: Vec<&PoolField> = pc
2670 .static_fields
2671 .iter()
2672 .chain(pc.instance_fields.iter())
2673 .collect();
2674 let fgroups: HashMap<String, Vec<&PoolField>> = HashMap::default();
2675 let mut fgroups = fgroups;
2676 for f in &fields {
2677 let disp = crate::classdec::java_ident(&f.name).into_owned();
2678 fgroups.entry(disp).or_default().push(f);
2679 }
2680 let mut f_taken: jdc_core::FxHashSet<String> = fields
2681 .iter()
2682 .map(|f| crate::classdec::java_ident(&f.name).into_owned())
2683 .collect();
2684 // Displays already registered under THIS class by ancestor
2685 // subs-propagation (an ancestor processed earlier in pool.order
2686 // pushed its renames down): the own mints must avoid them or
2687 // two fields in one class render the same display
2688 // ("已在类中定义了变量 a2", weibo bx$b: subs-pushed bv→a2 beside
2689 // its own String→a2).
2690 if let Some(v) = out.get(&std::sync::Arc::from(name.as_str())) {
2691 for fr in v {
2692 f_taken.insert(fr.display.to_string());
2693 }
2694 }
2695 for group in fgroups.values() {
2696 if group.len() < 2 {
2697 continue;
2698 }
2699 let keeper_pos = group
2700 .iter()
2701 .position(|f| f.access & crate::access::ACC_SYNTHETIC == 0)
2702 .unwrap_or(0);
2703 for (gi, f) in group.iter().enumerate() {
2704 if gi == keeper_pos {
2705 continue;
2706 }
2707 let base = crate::classdec::java_ident(&f.name).into_owned();
2708 let display: String = match &outer_ref_desc {
2709 Some(ord)
2710 if f.access & crate::access::ACC_SYNTHETIC != 0
2711 && !f.is_static
2712 && *ord == *f.desc =>
2713 {
2714 let mut k = 0u32;
2715 loop {
2716 let cand = if k == 0 {
2717 "this$0".to_string()
2718 } else {
2719 format!("this$0{k}")
2720 };
2721 if f_taken.insert(cand.clone()) {
2722 break cand;
2723 }
2724 k += 1;
2725 }
2726 }
2727 _ => suffix_unique(&base, &mut f_taken),
2728 };
2729 // Register with subclass propagation: dex refs of an
2730 // inherited field may name ANY hierarchy class as owner
2731 // (R8 writes the referrer), and without the propagation
2732 // the declaring class's rename does not reach the
2733 // subclass-keyed lookups — the ref renders the RAW name
2734 // against a renamed declaration (bx$b read its inherited
2735 // `a:Lcom/xiaomi/push/bv;` as `this.a` while bx$a
2736 // declared it `a2`). Mirrors the method-side hierarchy
2737 // consistency rule.
2738 register_field_rename_with_subs(
2739 pool, &subs, &mut out, name, &f.name, &f.desc, &display,
2740 );
2741 }
2742 }
2743 // ---- fields obscuring nested types (JLS 6.4.2) ----
2744 // `C.b.a(..)` with BOTH a field `b` and a member class `C$b`
2745 // binds to the FIELD — the qualified type reading is obscured
2746 // ("无法从静态上下文中引用非静态 变量 b" + the paired cannot-find
2747 // cascade; lark LKEvent/MotionLayout family, static-ctx ×1,154).
2748 // The CHILD-side rule (nested_collision_renames) already renames
2749 // colliding children when its local_ok gate passes (`a$a` renders
2750 // as `a2`); this field-side rule covers the gate-REJECTED residual
2751 // (child keeps display `b`, program-wide refs block its rename).
2752 // A field's refs all funnel through field_display (lift call
2753 // sites, declarations, const inits), so the field side needs no
2754 // locality gate. TWO invariants the first attempt violated (lark
2755 // +8,129): fire on the child's FINAL DISPLAY last segment, not
2756 // the raw tail (a child the child-side rule already renamed does
2757 // NOT obscure), and mint avoiding every child display (`a`→`a2`
2758 // landed on the existing `interface a2`, minting a fresh
2759 // obscuring pair).
2760 {
2761 let tails: &[&str] = child_tails
2762 .get(name.as_str())
2763 .map(|v| v.as_slice())
2764 .unwrap_or(&[]);
2765 let mut member_displays: jdc_core::FxHashSet<String> =
2766 jdc_core::FxHashSet::default();
2767 let mut obscuring: jdc_core::FxHashSet<String> =
2768 jdc_core::FxHashSet::default();
2769 for t in tails {
2770 let full = format!("{name}${t}");
2771 let renamed = crate::apply_class_rename(&full);
2772 let last = renamed
2773 .rsplit('/')
2774 .next()
2775 .unwrap_or(&renamed)
2776 .rsplit('$')
2777 .next()
2778 .unwrap_or(t)
2779 .to_string();
2780 member_displays.insert(last.clone());
2781 // Only an INLINE-rendered member is reachable by the bare
2782 // simple name (own-file children are referenced by their
2783 // flat `$` form, which fields cannot obscure).
2784 let member_rendered = clean_member_tail(t)
2785 && crate::find_outer_name(pool, &full).as_deref()
2786 == Some(name.as_str());
2787 if member_rendered {
2788 obscuring.insert(last);
2789 }
2790 }
2791 // The class's OWN simple display name joins the obscuring
2792 // set: a field named like its class captures every
2793 // qualified self-static ref from nested scopes
2794 // (`a2.changeQuickRedirect` bound to the String FIELD a2 —
2795 // weibo robust refs ×317; bare refs survive via the
2796 // variable namespace, the qualified form does not).
2797 let self_renamed = crate::apply_class_rename(name);
2798 let self_simple = self_renamed
2799 .rsplit('/')
2800 .next()
2801 .unwrap_or(&self_renamed)
2802 .rsplit('$')
2803 .next()
2804 .unwrap_or("")
2805 .to_string();
2806 if !self_simple.is_empty() {
2807 obscuring.insert(crate::classdec::java_ident(&self_simple).into_owned());
2808 }
2809 let self_pkg = name.rfind('/').map(|i| &name[..i]).unwrap_or("");
2810 let pkg_names = pkg_displays.get(self_pkg);
2811 for f in &fields {
2812 let fdisp = crate::classdec::java_ident(&f.name).into_owned();
2813 if fdisp.starts_with("this$") || !obscuring.contains(fdisp.as_str()) {
2814 continue;
2815 }
2816 // Already renamed by the same-class duplicate-group rule?
2817 // Its display no longer equals the obscuring simple name,
2818 // so this rule's target collision does not exist — and
2819 // minting a SECOND entry for the same (name, desc) with
2820 // subs-propagation put a CONFLICTING display under every
2821 // subclass: refs naming the subclass resolved to one name
2822 // while the declaring class's declaration rendered the
2823 // first entry's (weibo bx$b read its inherited
2824 // `a:Lcom/xiaomi/push/bv;` as `this.a8` while bx$a
2825 // declared it `a2` — the R8 same-name-field family).
2826 let already = out
2827 .get(&std::sync::Arc::from(name.as_str()))
2828 .is_some_and(|v| {
2829 v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
2830 });
2831 if already {
2832 continue;
2833 }
2834 let mut k = 1u32;
2835 let display = loop {
2836 k += 1;
2837 let cand = format!("{fdisp}{k}");
2838 // root_segs: a minted display equal to a PACKAGE
2839 // first segment captures every qualified ref to
2840 // that package inside the family's file (field e2
2841 // ate `e2.c.b(..)` — lark pq/e "变量 c" ×47). The
2842 // deshadow builder cannot repair this after the
2843 // fact: it scans RAW names, and the collision is
2844 // born here in display space.
2845 if !f_taken.contains(&cand)
2846 && !member_displays.contains(&cand)
2847 && !root_segs.contains(&cand)
2848 && !pkg_names.is_some_and(|p| p.contains(cand.as_str()))
2849 {
2850 f_taken.insert(cand.clone());
2851 member_displays.insert(cand.clone());
2852 break cand;
2853 }
2854 };
2855 register_field_rename_with_subs(
2856 pool, &subs, &mut out, name, &f.name, &f.desc, &display,
2857 );
2858 if std::env::var("DDC_OBSCURE_STATS").is_ok() {
2859 eprintln!("[obscure] {name} field {} -> {}", f.name, display);
2860 }
2861 }
2862 }
2863 // ---- companion-holder fields vs nested types (JLS 6.4.2 in
2864 // expression context): Kotlin compiles `object`/companion
2865 // holders as a static field whose name EQUALS the nested class
2866 // it instances (`static final j$a a`), and static members of the
2867 // nested are invoked as `j.a.c(..)` — but `j.a` in an EXPRESSION
2868 // resolves to the FIELD (variables outrank member types), so the
2869 // call becomes a member lookup on the instance type... which is
2870 // the same class, yet javac reports 找不到符号 for statics
2871 // reached through the shadowed path, and the whole file
2872 // error-types (weibo feed/business/j: 654 errors + closure
2873 // cascade; the nested-side rename for this is the LOCAL-OK-gated
2874 // rule that explodes ungated — the FIELD side is safe because
2875 // dex field refs always carry the declaring class, so this
2876 // registry keys every consumer). Gate: the nested class has
2877 // static members (companion shape) and the field's descriptor
2878 // is exactly the nested type.
2879 if let Some(tails) = child_tails.get(name.as_str()) {
2880 for tail in tails {
2881 let nested = format!("{name}${tail}");
2882 let Some(nc) = pool.get_if_materialized(&nested) else {
2883 continue;
2884 };
2885 // Only CLEAN tails render dotted (`j.a`); flat-rendered
2886 // tails (`j$1`) carry the `$` and never clash.
2887 if !clean_member_tail(tail) {
2888 continue;
2889 }
2890 let has_statics = nc
2891 .static_fields
2892 .iter()
2893 .chain(nc.instance_fields.iter())
2894 .any(|f| f.is_static)
2895 || nc.all_methods().any(|m| m.access & crate::access::ACC_STATIC != 0);
2896 if !has_statics {
2897 continue;
2898 }
2899 // STRICT companion shape: the field's descriptor IS the
2900 // nested type (`static final j$a a`). The relaxed form
2901 // (any same-named field beside a statics-bearing nested)
2902 // minted thousands of renames on weibo and exploded the
2903 // battery to the 500k cap — the field funnel does not
2904 // hold at that scale (改名面 vs 引用覆盖面, again).
2905 let want_desc = format!("L{nested};");
2906 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
2907 if f.desc.as_ref() != want_desc.as_str() {
2908 continue;
2909 }
2910 let disp = crate::classdec::java_ident(&f.name).into_owned();
2911 if disp != crate::classdec::java_ident(tail) {
2912 continue;
2913 }
2914 // Already renamed by the duplicate-group rule above?
2915 let already = out
2916 .get(&std::sync::Arc::from(name.as_str()))
2917 .is_some_and(|v| {
2918 v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
2919 });
2920 if already {
2921 continue;
2922 }
2923 let display = suffix_unique(&disp, &mut f_taken);
2924 f_taken.insert(display.clone());
2925 out.entry(std::sync::Arc::from(name.as_str()))
2926 .or_default()
2927 .push(jdc_core::rename::FieldRename {
2928 name: f.name.clone(),
2929 desc: f.desc.clone(),
2930 display: std::sync::Arc::from(display.as_str()),
2931 });
2932 }
2933 }
2934 }
2935 // ---- methods: display key = sanitized name + erased params ----
2936 let methods: Vec<&PoolMethod> = pc.all_methods().collect();
2937 // Ancestor virtual-method returns per (name, argsig) — one
2938 // chain walk per non-interface class, shared by the
2939 // covariant-keeper selection and the impostor-override
2940 // detection below. Layers stay PER-ANCESTOR (cached Arcs, no
2941 // merge): every consumer is an "any ancestor declares
2942 // (name, argsig) → ret" query, so a per-layer scan replaces
2943 // the old per-class eager map build. Provenance splits the
2944 // chain into fw / pool buckets (see AncEntry.is_fw).
2945 let mut anc_chain: Option<(AncLayers, AncLayers)> = None;
2946 if pc.access & crate::access::ACC_INTERFACE == 0 {
2947 let mut fw_layers: AncLayers = Vec::new();
2948 let mut pool_layers: AncLayers = Vec::new();
2949 let mut stack: Vec<std::sync::Arc<str>> = Vec::new();
2950 if let Some(sup) = &pc.super_name {
2951 if sup != "java/lang/Object" {
2952 stack.push(std::sync::Arc::from(sup.as_str()));
2953 }
2954 }
2955 stack.extend(
2956 pc.interfaces
2957 .iter()
2958 .map(|i| std::sync::Arc::from(i.as_str())),
2959 );
2960 let mut seen_cls: jdc_core::FxHashSet<std::sync::Arc<str>> =
2961 jdc_core::FxHashSet::default();
2962 while let Some(cn) = stack.pop() {
2963 if !seen_cls.insert(cn.clone()) {
2964 continue;
2965 }
2966 let entry = match anc_cache.get(cn.as_ref()) {
2967 Some(e) => e.clone(),
2968 None => {
2969 let e = parse_anc_entry(pool, &cn);
2970 anc_cache.insert(cn.clone(), e.clone());
2971 e
2972 }
2973 };
2974 if !entry.methods.is_empty() {
2975 if entry.is_fw {
2976 fw_layers.push(entry.clone());
2977 } else {
2978 pool_layers.push(entry.clone());
2979 }
2980 }
2981 stack.extend(entry.supers.iter().cloned());
2982 }
2983 anc_chain = Some((fw_layers, pool_layers));
2984 }
2985 let mut m_taken: jdc_core::FxHashSet<String> = methods
2986 .iter()
2987 .map(|m| crate::classdec::java_ident(&m.name).into_owned())
2988 .collect();
2989 let mut mgroups: HashMap<(String, String), Vec<&PoolMethod>> = HashMap::default();
2990 for m in &methods {
2991 let d: &str = &m.desc;
2992 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2993 let hi = d.find(')').unwrap_or(d.len());
2994 let key = (
2995 crate::classdec::java_ident(&m.name).into_owned(),
2996 d[lo..hi].to_string(),
2997 );
2998 mgroups.entry(key).or_default().push(m);
2999 }
3000 for ((base, _), group) in &mgroups {
3001 if group.len() < 2 {
3002 continue;
3003 }
3004 // R8 return-type specialization: a non-isolated class carries
3005 // `M(args)void` (the real logic) beside the interface bridge
3006 // `M(args)R` (unbox → call void → return Unit). Same erasure,
3007 // so Java source can't hold both, and `void` never satisfies
3008 // the interface's `R` — the claim map keeps the void side and
3009 // drops the bridge, yielding "X不是抽象的, 并且未覆盖…invoke"
3010 // (Kotlin suspend lambdas, weixin/lark ×1.1k). This is NOT the
3011 // covariant-override shape below (void has no subtype
3012 // relation): keep the BRIDGE under the original name so it
3013 // satisfies the interface, and rename the void specialization
3014 // so both render and the bridge's call to it resolves to the
3015 // renamed method.
3016 // `)Ljava/lang/Void;` counts as void-like: a Kotlin `Nothing`
3017 // function (an unsupported-op that always throws) compiles to
3018 // the Void CLASS return, not the `V` primitive, but it IS the
3019 // real-logic side of the same specialization pair. Missing it
3020 // sent the group down the covariant path, which kept the Void
3021 // method and renamed the bridge, then the impostor pass renamed
3022 // the Void method too — leaving NO `addAll` to satisfy Set
3023 // (weibo compose SnapshotMapEntrySet "未覆盖Set中的addAll").
3024 let ret_void = |m: &PoolMethod| {
3025 m.desc.ends_with(")V") || m.desc.ends_with(")Ljava/lang/Void;")
3026 };
3027 let isolated = pc
3028 .super_name
3029 .as_ref()
3030 .is_none_or(|s| s == "java/lang/Object")
3031 && pc.interfaces.is_empty();
3032 let has_void = group.iter().any(|m| ret_void(m) && &*m.name != "<init>");
3033 let has_nonvoid = group.iter().any(|m| !ret_void(m));
3034 if !isolated && has_void && has_nonvoid {
3035 let keeper_desc: Option<&str> = group
3036 .iter()
3037 .copied()
3038 .find(|m| {
3039 m.access & crate::access::ACC_BRIDGE != 0
3040 && !ret_void(m)
3041 && &*m.name != "<init>"
3042 })
3043 .or_else(|| {
3044 group
3045 .iter()
3046 .copied()
3047 .find(|m| !ret_void(m) && &*m.name != "<init>")
3048 })
3049 .map(|m| &*m.desc);
3050 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
3051 jdc_core::FxHashSet::default();
3052 for m in group.iter().copied() {
3053 if keeper_desc == Some(&*m.desc) {
3054 seen_orig.insert((&*m.name, &*m.desc));
3055 continue; // bridge keeps the interface name
3056 }
3057 if !seen_orig.insert((&*m.name, &*m.desc)) {
3058 continue; // exact duplicate: emitter drops it
3059 }
3060 let display = suffix_unique(base, &mut m_taken);
3061 out.entry(std::sync::Arc::from(name.as_str()))
3062 .or_default()
3063 .push(jdc_core::rename::FieldRename {
3064 name: m.name.clone(),
3065 desc: m.desc.clone(),
3066 display: std::sync::Arc::from(display.as_str()),
3067 });
3068 }
3069 continue;
3070 }
3071 // A COVARIANT override pair (same ORIGINAL name, different
3072 // descriptors — the compiler's bridge shape: the interface's
3073 // `deserialize(e)` plus the narrower implementation). Renaming
3074 // either side breaks @Override ("is not abstract and does not
3075 // override abstract method deserialize(e)", ~2.2k on reqable);
3076 // the emitter's claim logic already renders only the
3077 // non-bridge member of the pair. An ISOLATED class (extends
3078 // Object, implements nothing — d8's ExternalSyntheticApiModel
3079 // Outline shells) overrides nothing: its same-(name,params)
3080 // pairs differing only by RETURN type are dex-legal distinct
3081 // methods, and dropping one strands its callers on the
3082 // survivor (`HalfKt$$…0.m()Class` vs `.m()V` — "void无法
3083 // 转换为Class", weibo ×85+).
3084 let covariant = !isolated
3085 && group
3086 .iter()
3087 .any(|m| group.iter().any(|o| o.name == m.name && o.desc != m.desc));
3088 if covariant {
3089 // Return-type-only clash in a class WITH a hierarchy:
3090 // Java cannot declare both, and the claim logic would
3091 // keep an arbitrary one — when that is NOT the member
3092 // an ancestor declares, every concrete subclass fails
3093 // "X不是抽象的, 并且未覆盖…" (weibo ×590: viewmodel.a
3094 // kept getUsageInfo()e while interface d declares
3095 // getUsageInfo()g; the kept side also strands the
3096 // dropped side's callers). Keep the ancestor-declared
3097 // member under the original name and rename the rest —
3098 // the registry carries every call site, and the
3099 // propagation below shares the display down the family.
3100 // 0 or 2+ ancestor-matched members = the shape Java
3101 // cannot express at all (two interfaces demanding
3102 // different returns): keep the old claim behavior.
3103 let keepers: Vec<&&PoolMethod> = group
3104 .iter()
3105 .filter(|m| {
3106 let d: &str = &m.desc;
3107 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3108 let hi = d.find(')').unwrap_or(d.len());
3109 if let Some((fwl, pll)) = &anc_chain {
3110 layer_rets(fwl, &m.name, &d[lo..hi])
3111 .chain(layer_rets(pll, &m.name, &d[lo..hi]))
3112 .any(|r| r == &d[hi + 1..])
3113 } else {
3114 false
3115 }
3116 })
3117 .collect();
3118 if keepers.len() >= 2 {
3119 // The shape Java cannot express at all (2+
3120 // ancestors demanding different returns): no
3121 // rename target — keep the claim behavior.
3122 continue;
3123 }
3124 // fwdb flattens JDK covariant overrides to the erased
3125 // declaring return (SortedMap.keySet records Set, the
3126 // source return is SortedSet) — the sole "keeper" can
3127 // be the synthetic BRIDGE while the real override is
3128 // the non-bridge sibling whose return is a subtype of
3129 // the recorded one. Keeping the bridge renamed the
3130 // real method and broke the interface chain (WhatsApp
3131 // X/Jl2 SortedMap, cannot-find:变量 ×18.6k). Prefer the
3132 // non-bridge in that shape: the claim map dropped the
3133 // bridge before this machinery existed anyway.
3134 // Keeper selection by ancestor PROVENANCE:
3135 // - POOL ancestor matches (raw-rendered): the member
3136 // whose return EXACTLY matches a pool erasure that
3137 // is NOT purely framework-sourced keeps the name —
3138 // the erased bridge is what satisfies the raw
3139 // interface (weibo Function0.invoke / GlideRequest
3140 // .load; renaming the real one instead cost +375).
3141 // - FRAMEWORK-only match: javac reads the compiled
3142 // covariant truth (SortedMap.keySet→SortedSet) while
3143 // fwdb records the flattened erasure (→Set) that the
3144 // BRIDGE matches — prefer the non-bridge sibling
3145 // whose return is a subtype (WhatsApp X/Jl2).
3146 // - no ancestor: first non-bridge (the old claim
3147 // survivor), else dex order.
3148 fn ret_desc(m: &PoolMethod) -> &str {
3149 let d: &str = &m.desc;
3150 &d[d.find(')').map(|i| i + 1).unwrap_or(d.len())..]
3151 }
3152 let gkey = {
3153 let d: &str = &group[0].desc;
3154 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3155 let hi = d.find(')').unwrap_or(d.len());
3156 (
3157 crate::classdec::java_ident(&group[0].name).into_owned(),
3158 d[lo..hi].to_string(),
3159 )
3160 };
3161 // fw match: a NON-BRIDGE member whose return is
3162 // assignable to a framework ret (covariant source
3163 // truth — it satisfies the raw pool ancestor too,
3164 // overrides being covariant-legal in Java).
3165 let fw_i = anc_chain.as_ref().and_then(|(fwl, _)| {
3166 let fws: Vec<&str> =
3167 layer_rets(fwl, &gkey.0, &gkey.1).collect();
3168 if fws.is_empty() {
3169 return None;
3170 }
3171 group.iter().position(|m| {
3172 m.access & crate::access::ACC_BRIDGE == 0
3173 && fws.iter().any(|fr| {
3174 // ret_satisfies: descriptor-level, so
3175 // ARRAY covariant returns match too (the
3176 // old ret_internal strip returned None
3177 // for `[LX;` and fell back to descriptor
3178 // equality — Creator newArray's real
3179 // `[LSavedState;` never matched fw's
3180 // `[LObject;`).
3181 ret_satisfies(pool, ret_desc(m), fr)
3182 })
3183 })
3184 });
3185 let pool_i = anc_chain.as_ref().and_then(|(_, pll)| {
3186 let rets: Vec<&str> =
3187 layer_rets(pll, &gkey.0, &gkey.1).collect();
3188 if rets.is_empty() {
3189 return None;
3190 }
3191 group
3192 .iter()
3193 .position(|m| rets.iter().any(|r| *r == ret_desc(m)))
3194 });
3195 let keeper_idx = match (fw_i, pool_i) {
3196 (Some(fi), _) => fi,
3197 (None, Some(pi)) => pi,
3198 (None, None) => group
3199 .iter()
3200 .position(|m| m.access & crate::access::ACC_BRIDGE == 0)
3201 .unwrap_or(0),
3202 };
3203 // keepers==1: the ancestor-declared member keeps the
3204 // original name. keepers==0: NOTHING in the POOL binds
3205 // the name — keep the member the old claim map would
3206 // have kept (first NON-BRIDGE, else dex order) and
3207 // rename the rest through the registry: letting the
3208 // claim map silently drop a side stranded its callers
3209 // on the survivor's incompatible return (weibo xiaomi
3210 // push gk/dq/hj/hf/gt: `boolean b()Z` dropped beside
3211 // `String b()` — every `boolean v = this.b()` bound to
3212 // the String twin, "String无法转换为boolean" ×300+ in
3213 // the push package alone). NON-bridge preference is
3214 // load-bearing: a framework-ancestor covariant pair
3215 // (emoji2 n extends SpannableStringBuilder, Editable-
3216 // bridge beside the real override) satisfies the
3217 // ancestor through the REAL method — bridge-preference
3218 // renamed it and broke the override chain (reqable +8
3219 // "无法覆盖…返回类型不兼容").
3220 let keep_desc: &str = &group[keeper_idx].desc;
3221 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
3222 jdc_core::FxHashSet::default();
3223 for m in group.iter().copied() {
3224 if m.desc.as_ref() == keep_desc {
3225 seen_orig.insert((&*m.name, &*m.desc));
3226 continue;
3227 }
3228 if !seen_orig.insert((&*m.name, &*m.desc)) {
3229 continue;
3230 }
3231 let display = suffix_unique(base, &mut m_taken);
3232 out.entry(std::sync::Arc::from(name.as_str()))
3233 .or_default()
3234 .push(jdc_core::rename::FieldRename {
3235 name: m.name.clone(),
3236 desc: m.desc.clone(),
3237 display: std::sync::Arc::from(display.as_str()),
3238 });
3239 }
3240 continue;
3241 }
3242 // Real re-declarations (same original name AND descriptor —
3243 // R8 duplicates) are already deduped by the emitter's claim
3244 // logic; only SANITIZER collapses reach here and every one of
3245 // them is a distinct method.
3246 let mut base = base.clone();
3247 let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
3248 jdc_core::FxHashSet::default();
3249 for m in group {
3250 if !seen_orig.insert((&m.name, &m.desc)) {
3251 continue; // exact duplicate: emitter drops it
3252 }
3253 let display = suffix_unique(&base, &mut m_taken);
3254 out.entry(std::sync::Arc::from(name.as_str()))
3255 .or_default()
3256 .push(jdc_core::rename::FieldRename {
3257 name: m.name.clone(),
3258 desc: m.desc.clone(),
3259 display: std::sync::Arc::from(display.as_str()),
3260 });
3261 // The base stays the sanitized ORIGINAL so the first
3262 // occurrence keeps the plain name.
3263 let _ = &mut base;
3264 }
3265 }
3266 // ---- impostor overrides (R8 generic specialization) ----
3267 // A class method whose (name, argsig) matches an ancestor
3268 // declaration but whose return type satisfies NONE of the
3269 // ancestor returns is not an override Java can express: the
3270 // pair shares an erasure, so the class cannot declare both, and
3271 // ART never resolved it either (interface dispatch matches
3272 // name+descriptor — the missing proto throws AbstractMethodError
3273 // at runtime; weibo IStreamPresenter.getView()IStreamView vs the
3274 // impl's getView()IPageView with no subtype edge, ×258 across
3275 // getView/getWidget/getListView families). Rename the impostor
3276 // (the registry carries every caller and the propagation below
3277 // carries subclasses) and let synth_missing_interface_stubs add
3278 // the ART-faithful stub — rename-aware provision makes the
3279 // requirement missing again.
3280 if let Some((fwl, pll)) = &anc_chain {
3281 for m in &methods {
3282 if m.is_static()
3283 || &*m.name == "<init>"
3284 || &*m.name == "<clinit>"
3285 {
3286 continue;
3287 }
3288 let d: &str = &m.desc;
3289 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
3290 let hi = d.find(')').unwrap_or(d.len());
3291 let m_ret = &d[hi + 1..];
3292 // Lazy per-layer scan: `found` replaces the old
3293 // `map.get(key)` presence gate (no ancestor declares
3294 // (name, argsig) ⇒ nothing to violate ⇒ skip).
3295 let mut found = false;
3296 let mut satisfied = false;
3297 'lay: for e in fwl.iter().chain(pll.iter()) {
3298 let Some(rets) = e.methods.get(m.name.as_ref()) else {
3299 continue;
3300 };
3301 for (a, r) in rets {
3302 if **a != d[lo..hi] {
3303 continue;
3304 }
3305 found = true;
3306 // ret_satisfies handles ARRAY covariance in
3307 // descriptor form — the old L…;-strip match
3308 // answered false for `SavedState[] <: Object[]`
3309 // and renamed the REAL Parcelable.Creator
3310 // newArray as an impostor (newArray3), breaking
3311 // the override ("不是抽象的…未覆盖 newArray(int)",
3312 // weibo ×504 / battery ~789).
3313 if ret_satisfies(pool, m_ret, r) {
3314 satisfied = true;
3315 break 'lay;
3316 }
3317 }
3318 }
3319 if !found || satisfied {
3320 continue;
3321 }
3322 // Already renamed by a clash-group rule above?
3323 let already = out
3324 .get(&std::sync::Arc::from(name.as_str()))
3325 .is_some_and(|v| {
3326 v.iter().any(|fr| *fr.name == *m.name && *fr.desc == *m.desc)
3327 });
3328 if already {
3329 continue;
3330 }
3331 let base = crate::classdec::java_ident(&m.name).into_owned();
3332 let display = suffix_unique(&base, &mut m_taken);
3333 out.entry(std::sync::Arc::from(name.as_str()))
3334 .or_default()
3335 .push(jdc_core::rename::FieldRename {
3336 name: m.name.clone(),
3337 desc: m.desc.clone(),
3338 display: std::sync::Arc::from(display.as_str()),
3339 });
3340 }
3341 }
3342 }
3343 // ---- method-rename propagation down the hierarchy ----
3344 // A renamed method must keep ONE display across its whole override
3345 // family: the parent's abstract declaration and every subclass
3346 // implementation/caller render through the registry, and a missed
3347 // link re-breaks the override ("未覆盖" at each concrete subclass)
3348 // or the call site (cannot-find). Register each method entry on the
3349 // full transitive subclass + implementer closure — a subclass that
3350 // does NOT declare the method still serves as a dex ref owner for
3351 // the inherited method, so it needs the entry too (same reasoning
3352 // as register_field_rename_with_subs for fields). Ancestor-forced
3353 // displays win; a colliding local entry re-mints off the original
3354 // method name and re-propagates.
3355 {
3356 let mut down: jdc_core::FxHashMap<&str, Vec<&str>> =
3357 jdc_core::FxHashMap::default();
3358 for (sup, children) in subs.iter() {
3359 down.entry(*sup).or_default().extend(children.iter().copied());
3360 }
3361 for n in &pool.order {
3362 if let Some(pc) = pool.get_if_materialized(n) {
3363 for i in pc.interfaces.iter() {
3364 down.entry(i.as_str()).or_default().push(n.as_str());
3365 }
3366 }
3367 }
3368 type PropEntry = (
3369 std::sync::Arc<str>,
3370 std::sync::Arc<str>,
3371 std::sync::Arc<str>,
3372 std::sync::Arc<str>,
3373 );
3374 let mut queue: std::collections::VecDeque<PropEntry> =
3375 std::collections::VecDeque::new();
3376 {
3377 // Sorted seed: `out` is a std HashMap (RandomState) — an
3378 // unsorted seed would make conflicting ancestor forces
3379 // resolve in per-process order (display names flipping
3380 // run-to-run).
3381 let mut seed: Vec<PropEntry> = Vec::new();
3382 for (cls, entries) in out.iter() {
3383 for fr in entries {
3384 if fr.desc.contains('(') {
3385 seed.push((
3386 cls.clone(),
3387 fr.name.clone(),
3388 fr.desc.clone(),
3389 fr.display.clone(),
3390 ));
3391 }
3392 }
3393 }
3394 seed.sort();
3395 for t in seed {
3396 queue.push_back(t);
3397 }
3398 }
3399 let mut guard = 0usize;
3400 while let Some((cls, n, d, disp)) = queue.pop_front() {
3401 guard += 1;
3402 if guard > 2_000_000 {
3403 break;
3404 }
3405 let Some(children) = down.get(cls.as_ref()).cloned() else {
3406 continue;
3407 };
3408 for child in children {
3409 let mut changed = false;
3410 let mut reminted: Vec<PropEntry> = Vec::new();
3411 {
3412 let child_key: std::sync::Arc<str> = std::sync::Arc::from(child);
3413 let entries = out.entry(child_key.clone()).or_default();
3414 // A raw method of the child literally named `disp`
3415 // with a different signature blocks the force —
3416 // renaming that raw method instead is the over-reach
3417 // class (500k blowup history); skip this child.
3418 let raw_blocked = pool
3419 .get_if_materialized(child)
3420 .is_some_and(|pc| {
3421 pc.all_methods().any(|m| {
3422 crate::classdec::java_ident(&m.name) == disp.as_ref()
3423 && m.desc != d
3424 })
3425 });
3426 if raw_blocked {
3427 continue;
3428 }
3429 match entries
3430 .iter_mut()
3431 .find(|fr| *fr.name == *n && *fr.desc == *d)
3432 {
3433 Some(e) if e.display == disp => {}
3434 Some(e) => {
3435 e.display = disp.clone();
3436 changed = true;
3437 }
3438 None => {
3439 entries.push(jdc_core::rename::FieldRename {
3440 name: n.clone(),
3441 desc: d.clone(),
3442 display: disp.clone(),
3443 });
3444 changed = true;
3445 }
3446 }
3447 if changed {
3448 // Display collisions inside the child: the
3449 // ancestor-forced entry keeps `disp`; others
3450 // re-mint from their original method name.
3451 let mut taken: jdc_core::FxHashSet<String> =
3452 jdc_core::FxHashSet::default();
3453 if let Some(pc) = pool.get_if_materialized(child) {
3454 for m in pc.all_methods() {
3455 taken.insert(
3456 crate::classdec::java_ident(&m.name).into_owned(),
3457 );
3458 }
3459 }
3460 for e in entries.iter() {
3461 taken.insert(e.display.to_string());
3462 }
3463 let mut seen: jdc_core::FxHashSet<String> =
3464 jdc_core::FxHashSet::default();
3465 seen.insert(disp.to_string());
3466 for e in entries.iter_mut() {
3467 // FIELD entries never re-mint here: this is
3468 // the METHOD-name propagation, and a field
3469 // display colliding with a forced method
3470 // display is resolved at the field's own mint
3471 // (f_taken seeding above), not by renaming the
3472 // field out from under its subs-registered
3473 // references (bx$b's inherited bv→a2 was
3474 // reminted to a7 while every ref kept a2).
3475 if !e.desc.contains('(') {
3476 continue;
3477 }
3478 if *e.name == *n && *e.desc == *d {
3479 continue;
3480 }
3481 if seen.insert(e.display.to_string()) {
3482 continue;
3483 }
3484 let base =
3485 crate::classdec::java_ident(&e.name).into_owned();
3486 let nd: std::sync::Arc<str> = std::sync::Arc::from(
3487 suffix_unique(&base, &mut taken).as_str(),
3488 );
3489 e.display = nd.clone();
3490 reminted.push((
3491 child_key.clone(),
3492 e.name.clone(),
3493 e.desc.clone(),
3494 nd,
3495 ));
3496 }
3497 }
3498 }
3499 if changed {
3500 let child_key: std::sync::Arc<str> = std::sync::Arc::from(child);
3501 queue.push_back((child_key.clone(), n.clone(), d.clone(), disp.clone()));
3502 for r in reminted {
3503 queue.push_back(r);
3504 }
3505 }
3506 }
3507 }
3508 }
3509 inherited_obscuring_renames(pool, &mut out, &pkg_displays);
3510 out
3511}
3512
3513/// Inherited-field variant of the obscuring rule: class C renders a
3514/// member nested type `d` while INHERITING a field `d` from a pool
3515/// superclass — `C.d.b(..)` binds to the inherited variable (fields
3516/// obscure types, JLS 6.4.2) and fails ("无法从静态上下文中引用非静态
3517/// 变量 d" + "无法取消引用int" pairs, LynxBaseScrollViewDragging ×84 —
3518/// latent behind the broken-superclass hub until the same-class rule
3519/// healed it). The rename lands on the DECLARING class (one declaration
3520/// to re-render) and is registered under every transitive subclass as
3521/// well, because dex field refs may name any subclass as owner and the
3522/// lift-time consult keys on the ref's owner.
3523/// Register a field rename under the declaring class AND every transitive
3524/// subclass: dex field refs for an inherited access may name ANY class in
3525/// the hierarchy as owner (`iput Lh$a;->a` inside the constant-subclass
3526/// ctor of `h`), and the lift-time consult keys on the ref's owner — a
3527/// declaring-class-only entry leaves those refs stale (org/a/c/h ×3).
3528/// Subclasses that REDECLARE (name, desc) hide the field — their own
3529/// field is distinct and must keep its name (and their own obscuring,
3530/// if any, is handled when the loop reaches them).
3531fn register_field_rename_with_subs(
3532 pool: &DexPool,
3533 subs: &jdc_core::FxHashMap<&str, Vec<&str>>,
3534 out: &mut HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>>,
3535 owner: &str,
3536 fname: &str,
3537 fdesc: &str,
3538 display: &str,
3539) {
3540 let mut queue: Vec<&str> = vec![owner];
3541 let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
3542 seen.insert(owner);
3543 let mut first = true;
3544 while let Some(c) = queue.pop() {
3545 let redeclares = if first {
3546 first = false;
3547 false // the declaring class itself: always register
3548 } else {
3549 pool.get_if_materialized(c)
3550 .map(|pc| {
3551 pc.static_fields
3552 .iter()
3553 .chain(pc.instance_fields.iter())
3554 .any(|f| f.name.as_ref() == fname && f.desc.as_ref() == fdesc)
3555 })
3556 .unwrap_or(false)
3557 };
3558 if redeclares {
3559 continue; // hidden below this point: its subtree refs bind to its own field
3560 }
3561 out.entry(std::sync::Arc::from(c))
3562 .or_default()
3563 .push(jdc_core::rename::FieldRename {
3564 name: std::sync::Arc::from(fname),
3565 desc: std::sync::Arc::from(fdesc),
3566 display: std::sync::Arc::from(display),
3567 });
3568 if let Some(children) = subs.get(c) {
3569 for ch in children {
3570 if seen.insert(*ch) {
3571 queue.push(*ch);
3572 }
3573 }
3574 }
3575 }
3576}
3577
3578fn inherited_obscuring_renames(
3579 pool: &DexPool,
3580 out: &mut HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>>,
3581 pkg_displays: &jdc_core::FxHashMap<&str, jdc_core::FxHashSet<String>>,
3582) {
3583 let root_segs = pool.root_pkg_segs();
3584 // Reverse super map over materialized classes.
3585 let mut subs: jdc_core::FxHashMap<&str, Vec<&str>> = jdc_core::FxHashMap::default();
3586 for n in &pool.order {
3587 let Some(pc) = pool.get_if_materialized(n) else {
3588 continue;
3589 };
3590 if let Some(sup) = &pc.super_name {
3591 if sup != "java/lang/Object" {
3592 subs.entry(sup.as_str()).or_default().push(n.as_str());
3593 }
3594 }
3595 }
3596 let mut renamed: jdc_core::FxHashSet<(String, String, String)> =
3597 jdc_core::FxHashSet::default();
3598 for n in &pool.order {
3599 let Some(pc) = pool.get_if_materialized(n) else {
3600 continue;
3601 };
3602 if pc.is_interface() {
3603 continue;
3604 }
3605 // Member-rendered child displays of THIS class.
3606 let mut child_displays: Vec<String> = Vec::new();
3607 for child in pool.children_of(n.as_str()) {
3608 let Some(rest) = child
3609 .strip_prefix(n.as_str())
3610 .and_then(|t| t.strip_prefix('$'))
3611 else {
3612 continue;
3613 };
3614 if !clean_member_tail(rest) {
3615 continue;
3616 }
3617 if crate::find_outer_name(pool, child).as_deref() != Some(n.as_str()) {
3618 continue;
3619 }
3620 let renamed_disp = crate::apply_class_rename(child);
3621 let last = renamed_disp
3622 .rsplit('/')
3623 .next()
3624 .unwrap_or(&renamed_disp)
3625 .rsplit('$')
3626 .next()
3627 .unwrap_or(rest)
3628 .to_string();
3629 child_displays.push(last);
3630 }
3631 if child_displays.is_empty() {
3632 continue;
3633 }
3634 // Walk the super chain for an inherited field whose display
3635 // collides; own fields are the same-class rule's job.
3636 let own: jdc_core::FxHashSet<String> = pc
3637 .static_fields
3638 .iter()
3639 .chain(pc.instance_fields.iter())
3640 .map(|f| crate::classdec::java_ident(&f.name).into_owned())
3641 .collect();
3642 let mut sup = pc.super_name.clone();
3643 while let Some(sname) = sup {
3644 if sname == "java/lang/Object" {
3645 break;
3646 }
3647 let Some(sc) = pool.get_if_materialized(&sname) else {
3648 break;
3649 };
3650 for f in sc.static_fields.iter().chain(sc.instance_fields.iter()) {
3651 let fdisp = crate::classdec::java_ident(&f.name).into_owned();
3652 if fdisp.starts_with("this$") || !child_displays.contains(&fdisp) {
3653 continue;
3654 }
3655 if !renamed.insert((sname.clone(), f.name.to_string(), f.desc.to_string())) {
3656 continue;
3657 }
3658 // Mint in the DECLARING class's namespace: avoid its
3659 // fields + its own member-rendered child displays.
3660 let mut avoid: jdc_core::FxHashSet<String> = sc
3661 .static_fields
3662 .iter()
3663 .chain(sc.instance_fields.iter())
3664 .map(|g| crate::classdec::java_ident(&g.name).into_owned())
3665 .collect();
3666 // The declaring class's PACKAGE class displays (post-
3667 // rename): minting onto one shadows its qualified refs
3668 // from inside the class (the a→a2-onto-display-a2 bug).
3669 if let Some(names) = pkg_displays
3670 .get(sname.rfind('/').map(|i| &sname[..i]).unwrap_or(""))
3671 {
3672 avoid.extend(names.iter().cloned());
3673 }
3674 for child in pool.children_of(sname.as_str()) {
3675 let Some(rest) = child
3676 .strip_prefix(sname.as_str())
3677 .and_then(|t| t.strip_prefix('$'))
3678 else {
3679 continue;
3680 };
3681 if !clean_member_tail(rest) {
3682 continue;
3683 }
3684 let rd = crate::apply_class_rename(child);
3685 avoid.insert(
3686 rd.rsplit('/')
3687 .next()
3688 .unwrap_or(&rd)
3689 .rsplit('$')
3690 .next()
3691 .unwrap_or(rest)
3692 .to_string(),
3693 );
3694 }
3695 let mut k = 1u32;
3696 let display = loop {
3697 k += 1;
3698 let cand = format!("{fdisp}{k}");
3699 // root_segs: see the own-field mint site — a
3700 // package first segment must never be minted.
3701 if !avoid.contains(&cand) && !root_segs.contains(&cand) {
3702 break cand;
3703 }
3704 };
3705 register_field_rename_with_subs(
3706 pool, &subs, out, &sname, &f.name, &f.desc, &display,
3707 );
3708 if std::env::var("DDC_OBSCURE_STATS").is_ok() {
3709 eprintln!("[obscure-inh] {sname} field {} -> {display} (child of {n})", f.name);
3710 }
3711 }
3712 sup = sc.super_name.clone();
3713 }
3714 let _ = &own;
3715 }
3716}
3717
3718fn suffix_unique(base: &str, taken: &mut jdc_core::FxHashSet<String>) -> String {
3719 let mut k = 1u32;
3720 loop {
3721 k += 1;
3722 let cand = format!("{base}{k}");
3723 if taken.insert(cand.clone()) {
3724 return cand;
3725 }
3726 }
3727}
3728
3729/// member_collision_renames + field_deshadow_renames, collision-registry
3730/// entries winning per (owner, name, desc).
3731fn combined_field_renames(
3732 pool: &DexPool,
3733) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
3734 let mut base = member_collision_renames(pool);
3735 crate::stats_rss(" member_collision_renames");
3736 let extra = field_deshadow_renames(pool);
3737 let mut merged = 0usize;
3738 for (owner, v) in extra {
3739 let e = base.entry(owner).or_default();
3740 for fr in v {
3741 if !e.iter().any(|x| x.name == fr.name && x.desc == fr.desc) {
3742 e.push(fr);
3743 merged += 1;
3744 }
3745 }
3746 }
3747 if std::env::var("DDC_STATS").is_ok() {
3748 eprintln!("[renames] field-deshadow renames: {merged}");
3749 }
3750 crate::stats_rss(" field_deshadow merged");
3751 base
3752}
3753
3754/// Field deshadow (JLS 6.4.2, expression position): a FIELD whose display
3755/// name equals a root package first segment captures package-qualified
3756/// renders inside its family's file — androidx z2's field `j` binds the
3757/// `j` of `j.a.a` (找不到符号 变量 a). Rename through the field registry
3758/// (declaration and every reference funnel through field_display).
3759/// Gate: only when the family actually references that segment
3760/// (family_ref_segments body scan) or the segment is the family's OWN
3761/// package root (same-package refs fall back to `root.x` FQN renders) —
3762/// ungated field renames are the six-times-falsified blast radius.
3763fn field_deshadow_renames(
3764 pool: &DexPool,
3765) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
3766 let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
3767 HashMap::default();
3768 let root_segs = pool.root_pkg_segs();
3769 if root_segs.is_empty() {
3770 return out;
3771 }
3772 // Candidate fields per family (display, original name, desc).
3773 let mut cand: HashMap<
3774 String,
3775 Vec<(String, String, String, u32)>,
3776 > = HashMap::default();
3777 for name in &pool.order {
3778 let Some(pc) = pool.get_if_materialized(name) else {
3779 continue;
3780 };
3781 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
3782 let disp = crate::classdec::java_ident(&f.name).into_owned();
3783 if root_segs.contains(&disp) {
3784 cand
3785 .entry(name.clone())
3786 .or_default()
3787 .push((disp, f.name.to_string(), f.desc.to_string(), f.access));
3788 }
3789 }
3790 }
3791 if cand.is_empty() {
3792 return out;
3793 }
3794 let cand_set: jdc_core::FxHashSet<String> = cand.keys().cloned().collect();
3795 let t0 = std::time::Instant::now();
3796 let (fam_segs, fam_nest) =
3797 crate::refscan::family_ref_segments(&pool.dexes, &cand_set);
3798 if std::env::var("DDC_STATS").is_ok() {
3799 eprintln!(
3800 "[renames] field-deshadow scan: cands={} scan={:?}",
3801 cand_set.len(),
3802 t0.elapsed()
3803 );
3804 }
3805 for (fam, fields) in cand {
3806 let own_root = fam.find('/').map(|i| fam[..i].to_string());
3807 let scanned = fam_segs.get(&fam);
3808 // NO nested-tail collision trigger here: a private field named
3809 // like a nested class does capture `Fam.b.member` in expression
3810 // position (lark LKEvent static-ctx family, ~1k lines), but
3811 // renaming it is corpus-toxic even gated private+body-
3812 // referenced — lark +32k (43,643 renames), weixin/weibo −200.
3813 // Eighth falsification of loose rename gates: the resolution
3814 // perturbation surface of mass field renames dwarfs the target
3815 // family. The fam_nest scan stays wired for a future
3816 // render-side fix (the capture is only unfixable INSIDE the
3817 // family file; foreign files could import the nested type).
3818 let _ = &fam_nest;
3819 let hits: Vec<(String, String, String)> = fields
3820 .into_iter()
3821 .filter(|(disp, _, _, _access)| {
3822 scanned.is_some_and(|s| s.contains(disp))
3823 || own_root.as_deref() == Some(disp.as_str())
3824 })
3825 .map(|(d, n, de, _)| (d, n, de))
3826 .collect();
3827 if hits.is_empty() {
3828 continue;
3829 }
3830 // Names already taken in the family's member scope: field and
3831 // method displays plus direct nested-class tails (member types
3832 // share the lexical scope of the rendered file). Built only for
3833 // hit families — the per-family setup must stay off the 175k-
3834 // candidate loop (an O(pool.order) nested scan there cost 60s on
3835 // weixin).
3836 let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3837 if let Some(pc) = pool.get_if_materialized(&fam) {
3838 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
3839 taken.insert(crate::classdec::java_ident(&f.name).into_owned());
3840 }
3841 for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
3842 taken.insert(crate::classdec::java_ident(&m.name).into_owned());
3843 }
3844 }
3845 let mut nested_tails: jdc_core::FxHashSet<String> =
3846 jdc_core::FxHashSet::default();
3847 for child in pool.children_of(&fam) {
3848 if let Some(tail) = child.rsplit('$').next() {
3849 if !tail.is_empty() {
3850 taken.insert(tail.to_string());
3851 nested_tails.insert(tail.to_string());
3852 }
3853 }
3854 }
3855 for (disp, name, desc) in hits {
3856 let mut k = 0u32;
3857 let new_disp = loop {
3858 k += 1;
3859 let c = if k == 1 {
3860 format!("{disp}x")
3861 } else {
3862 format!("{disp}x{k}")
3863 };
3864 if !taken.contains(&c) && !root_segs.contains(&c) {
3865 break c;
3866 }
3867 };
3868 taken.insert(new_disp.clone());
3869 out.entry(std::sync::Arc::from(fam.as_str()))
3870 .or_default()
3871 .push(jdc_core::rename::FieldRename {
3872 name: std::sync::Arc::from(name.as_str()),
3873 desc: std::sync::Arc::from(desc.as_str()),
3874 display: std::sync::Arc::from(new_disp.as_str()),
3875 });
3876 }
3877 }
3878 out
3879}
3880pub use jdc_core::rename::apply_class_rename;
3881
3882// ---------------------------------------------------------------------------
3883// Default-package relocation.
3884// ---------------------------------------------------------------------------
3885
3886/// The synthetic package the default-package cohort currently relocates
3887/// to (Mutex, not OnceLock: `set_class_renames` replaces the registry per
3888/// pool, so a second pool in one process must be able to re-key this).
3889static ROOT_PKG: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
3890
3891/// The synthetic package display root-package classes were moved under
3892/// (`None` when no relocation ran). Consumers keying per-package caches
3893/// by the ORIGINAL package map this display back to `""`.
3894pub fn root_pkg_display() -> Option<String> {
3895 ROOT_PKG
3896 .lock()
3897 .unwrap_or_else(|e| e.into_inner())
3898 .clone()
3899}
3900
3901/// Default-package relocation: javac cannot resolve ANY reference from a
3902/// named package to an unnamed-package class (JLS 7.4.2 — `private it0 f;`
3903/// inside com.amap.X is 找不到符号 whatever the syntax; xingye ~198k +
3904/// Amap ~81k errors ≈ 73% of the whole-directory sweep total, and
3905/// kotlin.jvm.Function0 even EXTENDS a root class on xingye). No source
3906/// syntax escapes it — but the rename registry escapes the NAME: move the
3907/// whole root cohort under a synthetic package (`ddcroot/a.java`,
3908/// `package ddcroot;`) and every cross-package ref renders the resolvable
3909/// FQN `ddcroot.a`. Cohort-uniform: in-cohort refs keep resolving (one
3910/// new shared package), the case/lossy dedupes inside the cohort still
3911/// hold (a directory move preserves relative collisions), and the
3912/// synthetic segment is collision-bumped against every pool package and
3913/// class so it can never shadow or be shadowed. Runs LAST (prefixes the
3914/// settled displays). Fidelity note: declared package no longer matches
3915/// the binary name — the same trade every rename rule here makes, on a
3916/// cohort whose cross-package render is otherwise dead code.
3917/// Pool classes that shadow a FRAMEWORK class (d8 desugar stubs at the
3918/// framework FQN). The FILE identity relocates to the synthetic package,
3919/// but every REFERENCE renders the raw framework name — the bytecode's
3920/// FQN means the platform class (the stub is dead code the boot
3921/// classpath outranks).
3922pub(crate) static FW_SHADOW: std::sync::OnceLock<
3923 std::sync::Arc<jdc_core::FxHashSet<String>>,
3924> = std::sync::OnceLock::new();
3925
3926/// True when `internal` is a framework-shadow stub (fast empty-set
3927/// check when no corpus stubs exist).
3928pub fn is_fw_shadow(internal: &str) -> bool {
3929 let Some(s) = FW_SHADOW.get() else { return false };
3930 // Exact hit, or any $-chain PREFIX hits: a nested FRAMEWORK member
3931 // of a stub outer (`ImageDecoder$Source` with the stub
3932 // `ImageDecoder` in the pool) renders through the $-walk of the
3933 // stub's map entry — it must keep the raw framework name too.
3934 if s.contains(internal) {
3935 return true;
3936 }
3937 let mut idx = internal.len();
3938 while let Some(i) = internal[..idx].rfind('$') {
3939 if s.contains(&internal[..i]) {
3940 return true;
3941 }
3942 idx = i;
3943 }
3944 false
3945}
3946
3947/// d8's library/API desugaring embeds STUB classes under the framework's
3948/// own names (`android.graphics.Insets`, `android.app.Person` — minimal
3949/// shapes the runtime never loads, the boot classpath wins). Emitted at
3950/// their FQN they SHADOW the real framework class for every OTHER file
3951/// at compile time: javac resolves `android.graphics.Insets` to the
3952/// field-less stub and `insets.top` dies (reqable r2: the whole Insets
3953/// family). Relocate every pool class whose head resolves in the
3954/// embedded framework DB to a synthetic package; the rename registry
3955/// carries every reference.
3956fn fw_shadow_relocation(pool: &DexPool, map: &mut HashMap<String, String>) {
3957 let root_segs = pool.root_pkg_segs();
3958 // ANDROID-namespace only: a framework-shadow stub in another
3959 // namespace is usually a REAL bundled implementation (weibo ships
3960 // its own full org.json — relocating it broke every org.json user,
3961 // +86). d8's API-desugar stubs live under android/.
3962 let shadow_set: std::sync::Arc<jdc_core::FxHashSet<String>> =
3963 std::sync::Arc::new(
3964 pool.order
3965 .iter()
3966 .filter(|n| n.starts_with("android/"))
3967 .filter(|n| {
3968 let head = n.split('$').next().unwrap_or(n);
3969 crate::fwdb::find(head).is_some()
3970 })
3971 .map(|n| n.to_string())
3972 .collect(),
3973 );
3974 let _ = FW_SHADOW.set(shadow_set.clone());
3975 jdc_core::rename::set_fw_shadow(
3976 shadow_set.iter().cloned().collect(),
3977 );
3978 let mut pkg = "ddcfwstub".to_string();
3979 while pool.has_name(&pkg) || root_segs.contains(&pkg) {
3980 pkg.push('_');
3981 }
3982 let mut moved = 0usize;
3983 for name in &pool.order {
3984 // The relocation decision keys on the $-chain HEAD (a nested
3985 // stub rides its outer's identity); nested-of-pool-outer shapes
3986 // stay entry-less so apply_class_rename's $-walk composes them
3987 // through the relocated outer (the VasUIToken exact-entry
3988 // lesson from root_pkg_relocation). Android-namespace only —
3989 // see the set build above.
3990 if !name.starts_with("android/") {
3991 continue;
3992 }
3993 let head = name.split('$').next().unwrap_or(name);
3994 if crate::fwdb::find(head).is_none() {
3995 continue;
3996 }
3997 if head != name && pool.has_name(head) {
3998 continue;
3999 }
4000 let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
4001 if disp != *name {
4002 // An earlier rule renamed this class (obscuring/collision
4003 // family) — its display already diverges from the FQN and
4004 // the relocation prefix applies to the settled display.
4005 map.insert(name.clone(), format!("{pkg}/{disp}"));
4006 } else {
4007 map.insert(name.clone(), format!("{pkg}/{name}"));
4008 }
4009 moved += 1;
4010 }
4011 if std::env::var("DDC_STATS").is_ok() {
4012 eprintln!("[renames] fw-shadow relocation: {moved} classes -> {pkg}");
4013 }
4014}
4015
4016fn root_pkg_relocation(pool: &DexPool, map: &mut HashMap<String, String>) {
4017 // Gate on NAMES only (identical verdict in lazy browse and full
4018 // decompile — the display must not depend on materialization):
4019 // relocation matters only when BOTH cohorts exist — the JLS-dead
4020 // direction is named-package → root-package, so an all-root app
4021 // (hello fixtures, pure-demo dexes) keeps the faithful layout.
4022 let has_root = pool.order.iter().any(|n| !n.contains('/'));
4023 let has_named = pool.order.iter().any(|n| n.contains('/'));
4024 if !has_root || !has_named {
4025 *ROOT_PKG.lock().unwrap_or_else(|e| e.into_inner()) = None;
4026 return;
4027 }
4028 let root_segs = pool.root_pkg_segs();
4029 let mut pkg = "ddcroot".to_string();
4030 while pool.has_name(&pkg) || root_segs.contains(&pkg) {
4031 pkg.push('_');
4032 }
4033 // A root class whose simple name IS a top-level package segment
4034 // (`com`, `org`, ...) cannot simply MOVE into the synthetic
4035 // package: a same-package type shadows the package name in every
4036 // qualified ref from every cohort file (JLS 6.4.1 — rimet's root
4037 // `com`/`org` hijacked 235k `com.alibaba...` refs as 类 alibaba
4038 // 位置: 类 com). Those get a bumped first segment (`com` →
4039 // ddcroot/com2) — a display rename the registry carries through
4040 // every ref path, like the obscuring renames.
4041 const FW_SEGS: [&str; 10] = [
4042 "java", "javax", "android", "org", "junit", "dalvik", "libcore", "androidx", "kotlin",
4043 "kotlinx",
4044 ];
4045 let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4046 for name in &pool.order {
4047 if name.contains('/') {
4048 continue;
4049 }
4050 let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
4051 taken.insert(format!("{pkg}/{disp}"));
4052 }
4053 let mut moved = 0usize;
4054 let mut bumped = 0usize;
4055 for name in &pool.order {
4056 if name.contains('/') {
4057 continue;
4058 }
4059 let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
4060 if disp.contains('/') {
4061 continue; // defensive: some rule already packaged it
4062 }
4063 if disp == *name && name.contains('$') && find_outer_name(pool, name).is_some() {
4064 // An UNRENAMED nested name with a pool outer must stay
4065 // entry-less so apply_class_rename's $-walk composes it
4066 // through the outer's relocated+renamed display — an exact
4067 // entry short-circuits the walk and freezes the outer's
4068 // pre-rename simple (xingye's case-collision quads:
4069 // `ue5$a` exact-mapped to ddcroot/ue5$a instead of
4070 // ddcroot/ue5_4$a — 程序包ddcroot.ue5不存在 ×61k).
4071 // The case-collision machinery pre-inserts an IDENTITY
4072 // entry for every emission-set name, which short-circuits
4073 // the same walk and froze `$`-tails in the ROOT package
4074 // while their outer moved to ddcroot — family split: QQ's
4075 // VasUIToken$1..$1xx husks stayed at tree root, and every
4076 // `VasUIToken$N.INSTANCE` ref from ddcroot/VasUIToken.java
4077 // died (找不到符号 变量 VasUIToken$N ×202 in that one file).
4078 // Remove the pin; the walk composes through the outer.
4079 map.remove(name);
4080 continue;
4081 }
4082 let head = disp.split('$').next().unwrap_or(&disp);
4083 let collide = root_segs.contains(head) || FW_SEGS.contains(&head);
4084 if !collide {
4085 map.insert(name.clone(), format!("{pkg}/{disp}"));
4086 moved += 1;
4087 continue;
4088 }
4089 let rest = &disp[head.len()..];
4090 let mut k = 1u32;
4091 loop {
4092 k += 1;
4093 let nh = format!("{head}{k}");
4094 if root_segs.contains(&nh) || FW_SEGS.contains(&nh.as_str()) {
4095 continue;
4096 }
4097 let cand = format!("{pkg}/{nh}{rest}");
4098 if taken.contains(&cand) {
4099 continue;
4100 }
4101 taken.insert(cand.clone());
4102 map.insert(name.clone(), cand);
4103 moved += 1;
4104 bumped += 1;
4105 break;
4106 }
4107 }
4108 *ROOT_PKG.lock().unwrap_or_else(|e| e.into_inner()) = Some(pkg.clone());
4109 if std::env::var("DDC_STATS").is_ok() {
4110 eprintln!("[renames] root-pkg relocation: {moved} classes -> {pkg}/ ({bumped} segment-bumped)");
4111 }
4112}