ddc_dec/classdec.rs
1//! Class-level rendering: header, fields (with static initializers),
2//! constructors, methods and nested member classes.
3
4use jdc_core::emit::{escape_string, format_float, Printer};
5use jdc_core::types::JavaType;
6use jdc_core::Ctx;
7
8use crate::access::*;
9use crate::PoolField;
10use crate::ctx::{java_type_to_generic, DexCtx};
11use crate::method::decompile_method;
12use crate::{desc_type, DexPool, PoolClass, PoolMethod, StaticValue};
13use ddc_dex::insn::InsnKind;
14
15#[derive(Debug, Clone)]
16pub struct ClassOptions {
17 /// Prefix each file with a provenance comment.
18 pub provenance: bool,
19}
20
21impl Default for ClassOptions {
22 fn default() -> Self {
23 ClassOptions { provenance: true }
24 }
25}
26
27/// Decompile one class (plus nested member classes) to a Java source file.
28///
29/// Classes with large method bodies run in a monitored thread with a
30/// deadline: pathological CFGs can drive the shared structurer's walk into
31/// an exponential exploration that never returns. On timeout the class is
32/// reported failed and the thread is abandoned (reaped at process exit).
33/// One registered monitored decompile: the receiver the worker polls at
34/// the tail, the class name (for diagnostics), and the deadline counted
35/// from the spawn.
36pub type PendingMonitor = (
37 std::sync::mpsc::Receiver<Result<String, String>>,
38 String,
39 std::time::Instant,
40);
41
42pub fn decompile_class(
43 pool: &std::sync::Arc<DexPool>,
44 class: &PoolClass,
45 opts: &ClassOptions,
46 pending: &std::sync::Mutex<Vec<PendingMonitor>>,
47) -> anyhow::Result<String> {
48 if class_is_risky(pool, class) {
49 // Detached monitored thread: the CALLER registers the receiver and
50 // moves on (awaiting happens at the end of the run) — a spinning
51 // pathological method no longer stalls its worker.
52 let pool2 = pool.clone();
53 let cls = class.clone();
54 let opts2 = opts.clone();
55 let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
56 let name = class.name.clone();
57 // 10s: pure insurance — the structurer's walk budgets and the
58 // ladder giant short-circuit bound pathological methods
59 // deterministically now (weixin cdp/l1: 4.2s single-class), but
60 // full-run worker contention can ~2× that; the old 5s dropped
61 // l1's file entirely (every referrer cannot-finds).
62 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
63 let _ = std::thread::Builder::new()
64 .stack_size(64 * 1024 * 1024)
65 .spawn(move || {
66 let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
67 let _ = tx.send(r);
68 });
69 pending.lock().unwrap().push((rx, name, deadline));
70 return Err(anyhow::anyhow!(
71 "deferred to monitored thread (pathological-CFG guard)"
72 ));
73 }
74 decompile_class_impl(pool, class, opts)
75}
76
77/// Any method in the exponential-walk danger zone → run under the
78/// deadline. Calibrated between real workloads: routine R8 classes top out
79/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
80/// (weibo's gson TypeAdapters) sit at 24k+.
81fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
82 // Header peek only — decoding every body here would double the APK's
83 // total decode work.
84 for m in class.all_methods() {
85 if m.code_off == 0 {
86 continue;
87 }
88 if let Some(dex) = pool.dex(m.dex_idx) {
89 if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
90 if insns > 16_000 {
91 return true;
92 }
93 }
94 }
95 }
96 false
97}
98
99fn decompile_class_impl(
100 pool: &DexPool,
101 class: &PoolClass,
102 opts: &ClassOptions,
103) -> anyhow::Result<String> {
104 let ctx = DexCtx::new(pool, class);
105 // Size the class buffer up front: corpus-average classes render to
106 // ~1KB per method — without the reserve the String doubles through
107 // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
108 // Cap the reserve: weixin's monster classes (hundreds of methods)
109 // would reserve megabytes of untouched capacity per class — large
110 // blocks take mimalloc's commit/purge path (madvise churn showed up
111 // in profiles). 256KB covers the corpus-average class dozens of
112 // times over; bigger classes pay a few extra doublings.
113 let mut out = String::with_capacity(
114 (class.all_methods().count() * 1024).min(256 * 1024) + 512,
115 );
116 if opts.provenance {
117 static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
118 out.push_str(BANNER.get_or_init(|| {
119 format!(
120 "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
121 env!("CARGO_PKG_VERSION")
122 )
123 }));
124 // Provenance: which input image this class was lifted from (jadx's
125 // `loaded from: classes.dex` pattern). Byte-stable across runs —
126 // deliberately NO timestamp so outputs diff cleanly. Direct
127 // push_str: the per-class format! temporaries were 3 allocations
128 // × every class in the corpus.
129 if let Some(dex) = pool.dex(class.dex_idx) {
130 if let Some(label) = pool.dex_labels.get(class.dex_idx) {
131 out.push_str("// From: ");
132 out.push_str(label);
133 out.push_str(" (DEX ");
134 out.push_str(&dex.version);
135 out.push_str(")\n");
136 }
137 }
138 }
139 if let Some(src) = &class.source_file {
140 out.push_str("// Source file: ");
141 out.push_str(src);
142 out.push('\n');
143 }
144 if class.is_synthetic() {
145 out.push_str("// synthetic\n");
146 }
147 // The DISPLAY package: the root-package relocation rule moves the
148 // default-package cohort under a synthetic package, and the emitted
149 // `package` line + file path must agree with the renamed refs.
150 // Every pre-existing rename rule keeps the package, so this is a
151 // no-op for them.
152 let own_display_full = crate::apply_class_rename(&class.name);
153 let (pkg, _) = split_name(&own_display_full);
154 // Per-package caches below are keyed by the ORIGINAL package — the
155 // relocated cohort's same-package set is still the root one.
156 let pkg_lookup: &str = match crate::root_pkg_display() {
157 Some(rp) if rp == pkg => "",
158 _ => pkg.as_str(),
159 };
160 // Two-pass import assembly: the body renders FIRST (into a temp
161 // buffer) with the obscured render state installed — expression
162 // positions DISCOVER additional obscured refs on the fly — then the
163 // file assembles as provenance + package + imports + body. The one
164 // extra body copy is the price of correct import placement.
165 let shadow = inherited_field_shadows(pool, class);
166 let mut obscured_map = compute_obscured_renders(pool, class, &shadow);
167 // Same-package simple-name collisions: an import shadows every
168 // same-package use of that simple in this file — drop those from
169 // the map (their refs stay qualified, erroring honestly).
170 let mut blocked: jdc_core::FxHashSet<String> = pool
171 .package_simples()
172 .get(pkg_lookup)
173 .cloned()
174 .unwrap_or_default();
175 // JLS 7.5.1 narrowing: importing simple `n` legally shadows the
176 // same-package sibling throughout THIS file — it only breaks refs
177 // the file itself makes to that sibling. Drop sibling simples the
178 // file never references (exhaustive family scan: metadata + raw
179 // insn refs), unblocking the import route for field-shadowed
180 // package quals (lark pq/e: field `e2` captures `e2.c.b(..)` —
181 // "变量 c" ×47 — while sibling pq.c is never used). The scan runs
182 // ONLY when an obscured candidate is sibling-blocked.
183 // Scan gate: an obscured ref can only exist when some in-scope
184 // field name (or the own simple) equals a PACKAGE first segment —
185 // and the candidates include BODY refs (lark pq/e's `e2.c.b(..)`
186 // is invoke-static-only, invisible to the metadata map), so the
187 // gate must not key off obscured_map.
188 {
189 let own_simple0 = class.name.rsplit(['/', '$']).next().unwrap_or("");
190 let can_obscure = shadow.iter().any(|f| pool.package_simples().contains_key(f))
191 || (!own_simple0.is_empty()
192 && pool.package_simples().contains_key(own_simple0));
193 if can_obscure && !blocked.is_empty() {
194 let used = sibling_ref_simples(pool, class);
195 blocked.retain(|b| used.contains(b));
196 }
197 }
198 // Blocked names must include the RENAMED displays of same-package
199 // classes too: refs and imports render through the rename registry,
200 // so an import matching a DISPLAY (not the raw simple) shadows that
201 // class's in-file uses exactly the same (weixin n0/y0 displays as
202 // y02 while h72 declares a raw class y0 — the raw-only blocked set
203 // let the y02 import through on one side and killed it on the
204 // other, stranding 622 bare `y02` refs).
205 // Renamed-display blocking: a ZERO-COPY view into the process-wide
206 // per-package cache (built once from the rename registry — iterating
207 // `blocked` through apply_class_rename per FILE was O(files ×
208 // package): WhatsApp's ~10k-class package X cost 1,269 cumulative
209 // worker-seconds, decompile 24s → 134s; per-file clones of the
210 // extension set still cost 184s).
211 static PKG_RENAMED_DISPLAYS: std::sync::OnceLock<
212 jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
213 > = std::sync::OnceLock::new();
214 let blocked_renamed: Option<&'static jdc_core::FxHashSet<String>> = {
215 let cache = PKG_RENAMED_DISPLAYS.get_or_init(|| {
216 let mut m: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
217 jdc_core::FxHashMap::default();
218 if let Some(ren) = jdc_core::rename::with_renames(|r| r.clone()) {
219 for (raw, disp) in ren.iter() {
220 if raw == disp {
221 continue;
222 }
223 let pkg_key = raw.rfind('/').map(|i| &raw[..i]).unwrap_or("");
224 let tail = disp
225 .rsplit(['/', '$'])
226 .next()
227 .unwrap_or(disp.as_str())
228 .to_string();
229 m.entry(pkg_key.to_string()).or_default().insert(tail);
230 }
231 }
232 m
233 });
234 cache.get(pkg_lookup)
235 };
236 // In-file member shadowing: the own class's FIELDS and its family's
237 // nested-class tails are in scope inside the body and shadow any
238 // single-type import of the same simple (JLS 6.4.1/6.4.2) — an
239 // obscured simple colliding with them cannot be imported.
240 {
241 let own_raw = class.name.rsplit('/').next().unwrap_or("");
242 let prefix = format!("{}$", own_raw);
243 let extras: Vec<String> = blocked
244 .iter()
245 .filter(|b| b.starts_with(&prefix))
246 .filter_map(|b| b.rsplit('$').next().map(|t| t.to_string()))
247 .collect();
248 blocked.extend(extras);
249 for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
250 blocked.insert(crate::classdec::java_ident(&f.name).into_owned());
251 }
252 }
253 obscured_map.retain(|_internal, simple| {
254 !blocked.contains(simple)
255 && !blocked_renamed.is_some_and(|e| e.contains(simple))
256 });
257 // Display-simple collisions cannot coexist as single-type imports:
258 // importing simple `n` while THIS file declares `n` is a JLS 7.5.1
259 // error, and two imports of the same simple make every use of it
260 // ambiguous (weibo `class a2 extends a2` with `import a.a.b.c.a2`
261 // + `import a.e.a.a.a2` — both shadow-renamed from package-`a`
262 // classes named `a`). Drop every colliding entry (own-display hits
263 // drop ALL claimants; multi-internal simples keep the least
264 // internal for determinism) — dropped refs fall back to the
265 // qualified display render, which resolves against the renamed
266 // output files.
267 let own_display_internal = crate::apply_class_rename(&class.name);
268 let own_disp_simple = own_display_internal
269 .rsplit(['/', '$'])
270 .next()
271 .unwrap_or("")
272 .to_string();
273 {
274 let mut by_simple: std::collections::BTreeMap<&str, Vec<&String>> =
275 std::collections::BTreeMap::new();
276 for (internal, simple) in obscured_map.iter() {
277 by_simple.entry(simple.as_str()).or_default().push(internal);
278 }
279 let mut drop: Vec<String> = Vec::new();
280 for (simple, mut internals) in by_simple {
281 if simple == own_disp_simple.as_str() {
282 drop.extend(internals.into_iter().cloned());
283 } else if internals.len() > 1 {
284 internals.sort();
285 drop.extend(internals.into_iter().skip(1).cloned());
286 }
287 }
288 for d in &drop {
289 obscured_map.remove(d);
290 }
291 }
292 set_obscured_state(
293 class.name.clone(),
294 obscured_map.clone(),
295 blocked.clone(),
296 blocked_renamed,
297 shadow,
298 );
299 let mut body_buf = String::with_capacity(out.capacity() / 2);
300 let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
301 let recorded = take_recorded_and_clear();
302 body_res?;
303 if !pkg.is_empty() {
304 out.push('\n');
305 out.push_str("package ");
306 let dotted = pkg.replace('/', ".");
307 out.push_str(&sanitize_fq(&dotted));
308 out.push_str(";\n");
309 // Imports: the metadata map plus the expression-level
310 // discoveries, pool-known only (an import of an unknown class
311 // is a hard error), display-renamed, sorted for determinism.
312 let mut import_set: jdc_core::FxHashSet<String> =
313 obscured_map.keys().cloned().collect();
314 for r in recorded {
315 if pool.get(&r).is_some() {
316 // The import renders under the RENAMED display — the
317 // blocked check must use the same name the refs render.
318 let display = crate::apply_class_rename(&r);
319 let simple = display.rsplit(['/', '$']).next().unwrap_or("");
320 if !simple.is_empty()
321 && !blocked.contains(simple)
322 && !blocked_renamed.is_some_and(|e| e.contains(simple))
323 {
324 import_set.insert(r);
325 }
326 }
327 }
328 let mut imports: Vec<String> = import_set
329 .iter()
330 .map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
331 .collect();
332 imports.sort();
333 // Body-discovered (recorded) refs joined after the pre-emission
334 // dedupe — re-check at render: one import per display simple,
335 // none against this file's own display (JLS 7.5.1). A dropped
336 // recorded ref's body render is ambiguous regardless; skipping
337 // its import at least avoids the duplicate-import error.
338 let own_display_dotted = crate::classdec::dotted(&own_display_internal);
339 let own_dot_simple = own_display_dotted.rsplit('.').next().unwrap_or("");
340 let mut seen_simples: jdc_core::FxHashSet<String> =
341 jdc_core::FxHashSet::default();
342 for display in imports {
343 let simple = display.rsplit('.').next().unwrap_or("");
344 if simple == own_dot_simple || !seen_simples.insert(simple.to_string()) {
345 continue;
346 }
347 out.push_str(&format!("import {};\n", display));
348 }
349 }
350 out.push('\n');
351 out.push_str(&body_buf);
352 Ok(out)
353}
354
355/// Render the class header, fields, methods and nested member classes into
356/// `out` (used at top level and recursively for nested members).
357// `opts` is only consumed by the nested-member recursion below — that
358// is its purpose (propagating emission options into inline children).
359/// One enum constant: the source identifier plus constructor
360/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
361use jdc_core::ir::expr::{ConstVal, Expr};
362use jdc_core::ir::stmt::Stmt;
363struct EnumConst {
364 #[allow(dead_code)]
365 field: String,
366 name: String,
367 extra_args: Vec<Expr>,
368}
369
370/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
371/// static field must be initialized in `<clinit>` by
372/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
373/// always emit. Returns None (caller falls back to the desugared
374/// `/* enum */ class` form) when anything is missing: R8 variance,
375/// constant-specific bodies (the field holds an anonymous subclass), or
376/// a <clinit> that failed to decompile.
377/// Cached DDC_DBG_ENUM gate (env reads must not sit in per-class paths).
378fn dbg_enum() -> bool {
379 static CACHED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
380 *CACHED.get_or_init(|| std::env::var_os("DDC_DBG_ENUM").is_some())
381}
382
383fn dbg_enum_none(ln: u32) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
384 if dbg_enum() {
385 eprintln!("[enum-none] line {}", ln);
386 }
387 None
388}
389
390fn collect_enum_constants(
391 pool: &DexPool,
392 class: &PoolClass,
393 ctx: &DexCtx<'_>,
394) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
395 let _ = ctx;
396 let const_fields: Vec<&PoolField> = class
397 .static_fields
398 .iter()
399 .filter(|f| f.access & crate::access::ACC_ENUM != 0)
400 .collect();
401 // No ACC_ENUM constant fields: R8 drops them ALL when nothing
402 // outside reads them (weixin sp2/ji — only the synthetic $VALUES
403 // array survives). An ACC_ENUM CLASS still promotes: the synthetic-
404 // constant scan below lifts the constants out of the array literal.
405 // Non-enum classes have no business here.
406 if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
407 return dbg_enum_none(line!());
408 }
409 // JLS 8.9.2: a restored true-enum ctor may not read the enum's own
410 // static fields (javac rejects even the QUALIFIED `E.field` form).
411 // Meituan Robust injects a hotpatch guard reading
412 // `changeQuickRedirect` into every instrumented ctor — legal in
413 // bytecode (injected after javac), and strip_robust_enum_ctor_guard
414 // removes it from the restored source. A self-static sget of any
415 // OTHER type makes restoration inexpressible → stay on the fallback.
416 for m in class.all_methods() {
417 if &*m.name != "<init>" {
418 continue;
419 }
420 let dex = pool.dex(m.dex_idx)?;
421 let ci = dex.code_at(m.code_off)?;
422 for ins in &ci.insns {
423 if let ddc_dex::insn::InsnKind::SGet { field_idx, .. } = ins.kind {
424 let fr = dex.field(field_idx);
425 if dex.class_name(fr.class_idx) == class.name.as_str()
426 && dex.type_name(fr.type_idx) != "Lcom/meituan/robust/ChangeQuickRedirect;"
427 {
428 return dbg_enum_none(line!());
429 }
430 }
431 }
432 }
433 let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
434 let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
435 // The structurer can leave the constant-build sequence inside a
436 // nested Stmt::Block (jd1.u: stmt4 wraps all 26 ctors beside a
437 // trailing loop) — the passes below index a FLAT statement list.
438 if let Stmt::Block(vs) = &mut body.body {
439 crate::passes::flatten_top_blocks(vs);
440 }
441 if dbg_enum() {
442 if let Stmt::Block(vs) = &body.body {
443 for (i, st) in vs.iter().enumerate() {
444 let d = format!("{:?}", st);
445 eprintln!("[enum-stmt {}] {}", i, d.chars().take(220).collect::<String>());
446 }
447 }
448 }
449 crate::passes::strip_clinit_hotfix_guard(&mut body.body);
450
451 // Meituan Robust hotpatch guard (weibo's lifecycle enums — a big
452 // slice of the 1,137 fallback population): the ENTIRE constant-build
453 // sequence lives in one branch of a top-level
454 // `if (PatchProxy.isSupportClinit(..)) { .. } else { .. }`, so the
455 // flat scans below see nothing and the enum falls back (its dex
456 // `Enum.valueOf(X.class,..)` then fails — X is no Enum subtype,
457 // weibo ×853). Hoist the constant-carrying branch to the top level,
458 // but only when the top level itself has no constant defs (an
459 // ordinary clinit must not be touched, and a mixed shape is beyond
460 // the flat-scan contract). Restoration semantics already normalize
461 // the clinit (constants move to the header, inits drop); the
462 // dispatch shim is inexpressible in a true enum regardless.
463 if let Stmt::Block(vs) = &mut body.body {
464 let self_cls: &str = class.name.as_str();
465 let top_def = |st: &Stmt| -> bool {
466 let v = match st {
467 Stmt::LocalDef { init: Some(e), .. } => e,
468 Stmt::ExprStmt(Expr::Assign { value, .. }) => value,
469 _ => return false,
470 };
471 matches!(v, Expr::New { cls, args, .. }
472 if cls.as_ref() == self_cls && args.len() >= 2)
473 };
474 if !vs.iter().any(top_def) {
475 let branch_news = |st: &Stmt| -> usize {
476 let mut n = 0usize;
477 crate::passes::visit_all_exprs(st, &mut |x| {
478 if let Expr::New { cls, args, .. } = x {
479 if cls.as_ref() == self_cls && args.len() >= 2 {
480 n += 1;
481 }
482 }
483 });
484 n
485 };
486 let mut pick: Option<(usize, bool)> = None;
487 for (i, st) in vs.iter().enumerate() {
488 if let Stmt::If { then_stmt, else_stmt, .. } = st {
489 let t = branch_news(then_stmt) > 0;
490 let e = else_stmt
491 .as_ref()
492 .map(|s2| branch_news(s2) > 0)
493 .unwrap_or(false);
494 if t != e {
495 pick = Some((i, t));
496 break;
497 }
498 }
499 }
500 if let Some((i, then_is_const)) = pick {
501 let st = std::mem::replace(&mut vs[i], Stmt::Block(Vec::new()));
502 if let Stmt::If { then_stmt, else_stmt, .. } = st {
503 let keep = if then_is_const {
504 Some(then_stmt)
505 } else {
506 else_stmt
507 };
508 if let Some(bx) = keep {
509 let inner = match *bx {
510 Stmt::Block(v) => v,
511 other => vec![other],
512 };
513 vs.splice(i..=i, inner);
514 // The branch can be double-wrapped (Block[Block[..]]);
515 // the passes below index a FLAT list.
516 crate::passes::flatten_top_blocks(vs);
517 }
518 }
519 }
520 }
521 }
522
523 // R8/d8 split the constant build across an intermediate local:
524 // Self v0 = new Self("NAME", i, ...);
525 // a = v0; // sput to the ACC_ENUM field
526 // Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
527 // Pass 1 registers each intermediate (or direct) new; pass 2 binds
528 // them to the ACC_ENUM fields; references to the locals are then
529 // rewritten into constant identifiers so the $VALUES build keeps
530 // compiling once the definitions drop.
531 use std::collections::HashMap;
532 let mut const_name: Vec<String> = Vec::new();
533 let mut const_field: Vec<String> = Vec::new();
534 let mut const_extra: Vec<Vec<Expr>> = Vec::new();
535 // Declared ordinal per constant (the ctor's int arg) — the merge at
536 // the end orders field-bound and synthetic constants by it and
537 // demands a dense 0..n run (Java derives ordinals from declaration
538 // position).
539 let mut const_ord: Vec<i64> = Vec::new();
540 let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
541 let mut drop_stmts: Vec<usize> = Vec::new();
542
543 // Collect (immutable borrows) first; the mutable passes come after.
544 if !matches!(&body.body, Stmt::Block(_)) { return dbg_enum_none(line!()); }
545
546 // Pass 1: definitions. (immutable borrow; rewrite comes later)
547 // Rolling reaching-defs of clinit locals for resolving enum-ctor
548 // extra args (resolve_enum_arg); `tainted` flips at the first
549 // non-straight-line statement — past it, linear-scan defs are no
550 // longer provable and Local args reject the enum mode.
551 let mut defs: HashMap<u32, &Expr> = HashMap::default();
552 let mut tainted = false;
553 let self_name: std::sync::Arc<str> = class.name.as_str().into();
554 let self_ty =
555 jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
556 // A REUSED intermediate local: `v = new Self(..); A = v; v = new
557 // Self(..); B = v;` — the first def is a LocalDef, the rest are
558 // Assigns to the same var. Register every def site; pass 2 resolves
559 // a local sput to the def site CURRENT at that statement (rolling),
560 // not to the var (Kotlin EnumEntries enums, RegexOption: 8→7 count
561 // mismatch used to abort the whole promotion).
562 let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
563 for (i, st) in match &body.body {
564 Stmt::Block(v) => v.iter().enumerate(),
565 _ => return dbg_enum_none(line!()),
566 } {
567 // (var, New) for both def shapes: LocalDef and Assign-to-local.
568 let def_shape: Option<(u32, &Expr)> = match st {
569 Stmt::LocalDef {
570 var,
571 init: Some(e),
572 ..
573 } => Some((*var, e)),
574 Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
575 match (&**target, &**value) {
576 (Expr::Local { var, .. }, e) => Some((*var, e)),
577 _ => None,
578 }
579 }
580 _ => None,
581 };
582 if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
583 if ncls.as_ref() == class.name && args.len() >= 2 {
584 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
585 (&args[0], &args[1])
586 {
587 if java_ident(n).as_ref() != &**n || n.is_empty() { return dbg_enum_none(line!()); }
588 var_of.insert(var, const_name.len());
589 def_site.insert(i, (var, const_name.len()));
590 const_ord.push(*ord0 as i64);
591 const_name.push(n.to_string());
592 const_field.push(String::new());
593 const_extra.push(resolve_enum_extras(
594 &args[2..],
595 &defs,
596 tainted,
597 &var_of,
598 &const_name,
599 &self_name,
600 &self_ty,
601 )?);
602 drop_stmts.push(i);
603 }
604 }
605 }
606 track_def(st, &mut defs, &mut tainted);
607 }
608
609 // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
610 // Local sputs resolve through the ROLLING def map (updated at each
611 // def-site statement) — a reused `v` must bind to the constant
612 // defined most recently, not to the var's first registration.
613 let mut defs2: HashMap<u32, &Expr> = HashMap::default();
614 let mut tainted2 = false;
615 let mut cur_def: HashMap<u32, usize> = HashMap::default();
616 for (i, st) in match &body.body {
617 Stmt::Block(v) => v.iter().enumerate(),
618 _ => return dbg_enum_none(line!()),
619 } {
620 if let Some((var, idx)) = def_site.get(&i) {
621 cur_def.insert(*var, *idx);
622 }
623 if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
624 if let Expr::Field {
625 cls,
626 name: fname,
627 is_static: true,
628 ..
629 } = &**target
630 {
631 if cls.as_ref() != class.name {
632 continue;
633 }
634 // The lifter emits field refs THROUGH field_display — an
635 // obscuring-renamed const field (te/b's field `b` shares
636 // the class name → displays `b7`) arrives here already
637 // renamed; the raw-name compare silently skipped the sput
638 // and the enum fell back to `/* enum */ class` with the
639 // synthetic ctor stripped — every `new b("STARS", 0)`
640 // then failed 「无法将构造器应用到给定类型」 (Telegram 603
641 // errors / 430 files). Match on the display name; the
642 // promoted constant declares under it, which is exactly
643 // what every pool reference resolves through.
644 let bound = const_fields.iter().any(|f| {
645 f.name.as_str() == &**fname
646 || jdc_core::rename::field_display(
647 class.name.as_str(),
648 &f.name,
649 &f.desc,
650 )
651 .is_some_and(|d| d == &**fname)
652 });
653 if !bound {
654 continue;
655 }
656 if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { return dbg_enum_none(line!()); // duplicate assignment
657 }
658 let idx = match &**value {
659 Expr::Local { var, .. } => cur_def.get(var).copied()?,
660 Expr::New { cls: ncls, args, .. }
661 if ncls.as_ref() == class.name && args.len() >= 2 =>
662 {
663 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
664 (&args[0], &args[1])
665 {
666 if java_ident(n).as_ref() != &**n || n.is_empty() { return dbg_enum_none(line!()); }
667 let idx = const_name.len();
668 const_ord.push(*ord0 as i64);
669 const_name.push(n.to_string());
670 const_field.push(String::new());
671 const_extra.push(resolve_enum_extras(
672 &args[2..],
673 &defs2,
674 tainted2,
675 &var_of,
676 &const_name,
677 &self_name,
678 &self_ty,
679 )?);
680 idx
681 } else {
682 return dbg_enum_none(line!());
683 }
684 }
685 _ => return dbg_enum_none(line!()),
686 };
687 const_field[idx] = fname.to_string();
688 drop_stmts.push(i);
689 }
690 }
691 track_def(st, &mut defs2, &mut tainted2);
692 }
693 // Own the reaching-def values: defs2 borrows body.body, and the
694 // synthetic-constant scan below mutates the $VALUES array in place.
695 let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
696 .iter()
697 .map(|(k, v)| (*k, (*v).clone()))
698 .collect();
699 drop(defs2);
700 let defs2: std::collections::HashMap<u32, &Expr> =
701 defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
702
703 // Every ACC_ENUM field bound, every intermediate matched.
704 // Every ACC_ENUM FIELD must be bound to a constant. The converse
705 // need not hold: R8 drops the FIELD of a constant nothing reads
706 // externally while the $VALUES array keeps it (weixin lite/api/n's
707 // ON_DESTROY — a pass-1 local with no pass-2 sput). Field-less
708 // constants keep their ctor-string source name.
709 if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
710 {
711 if dbg_enum() {
712 eprintln!("[enum-gate677] const_fields={:?} const_field={:?} const_name={:?} const_ord={:?}",
713 const_fields.iter().map(|f| f.name.to_string()).collect::<Vec<_>>(),
714 const_field, const_name, const_ord);
715 }
716 return dbg_enum_none(line!());
717 }
718 }
719 // Obfuscated enums rename the ACC_ENUM FIELD (d/e/f) while the ctor's
720 // name STRING keeps the source identifier — the promoted constant
721 // must be declared under the FIELD name: every reference in the
722 // pool resolves through it. Using the name string broke all
723 // cross-file references (weixin +2.9k when newly-promoted enums
724 // swapped d/e/f for TEXT_ENTER_EDITING/…).
725 for i in 0..const_name.len() {
726 let field = &const_field[i];
727 if !field.is_empty() && field != &const_name[i] {
728 let id = java_ident(field);
729 if id.is_empty() || id.as_ref() != field.as_str() {
730 return dbg_enum_none(line!());
731 }
732 const_name[i] = field.clone();
733 }
734 }
735 {
736 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
737 if !const_name.iter().all(|c| seen.insert(c.as_str())) { return dbg_enum_none(line!()); }
738 }
739
740 // Synthetic constants: entries built INLINE inside the $VALUES
741 // array (`new Self("NAME", ord, ..)` with no ACC_ENUM field — R8
742 // drops the field when nothing outside reads it; weixin u2/i's
743 // "CONSTANT"). Promote them so the header declares the constant:
744 // the raw `new` in the array is "无法实例化枚举类", and every
745 // constant after the gap carries a SHIFTED ordinal (e was 2 in the
746 // dex, renders as position 1).
747 let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
748 {
749 let self_cls: &str = class.name.as_str();
750 if let Stmt::Block(vs) = &mut body.body {
751 for st in vs.iter_mut() {
752 crate::passes::walk_stmt_exprs(st, &mut |e| {
753 crate::passes::deep_rewrite(e, &mut |x| {
754 let Expr::NewArray { elem, init: Some(list), .. } = x else {
755 return;
756 };
757 if elem.erased() != JavaType::Object(self_cls.into()) {
758 return;
759 }
760 for slot in list.iter_mut() {
761 let (n, ord, rest) = match slot {
762 Expr::New { cls, args, raw: false, .. }
763 if cls.as_ref() == self_cls && args.len() >= 2 =>
764 {
765 match (&args[0], &args[1]) {
766 (
767 Expr::Const(ConstVal::Str(sv)),
768 Expr::Const(ConstVal::Int(o)),
769 ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
770 _ => continue,
771 }
772 }
773 _ => continue,
774 };
775 if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
776 continue;
777 }
778 if const_name.contains(&n)
779 || synth.iter().any(|(_, s2, _)| *s2 == n)
780 {
781 continue;
782 }
783 let Some(extras) = resolve_enum_extras(
784 &rest,
785 &defs2,
786 tainted2,
787 &var_of,
788 &const_name,
789 &self_name,
790 &self_ty,
791 ) else {
792 continue;
793 };
794 *slot = Expr::Field {
795 owner: None,
796 cls: self_name.clone(),
797 name: std::sync::Arc::from(n.as_str()),
798 ty: self_ty.clone(),
799 is_static: true,
800 };
801 synth.push((ord, n, extras));
802 }
803 });
804 });
805 }
806 }
807 }
808
809 // Pass 3: rewrite references to the intermediate locals into the
810 // constant identifiers (static-field reads on Self). POSITION-AWARE:
811 // a REUSED local must read as the constant defined most recently at
812 // that statement — a var-level map made every `arr[k] = v` in the
813 // $VALUES build reference the LAST constant (weixin +2.9k when this
814 // regressed values() contents).
815 {
816 let mut cur: HashMap<u32, usize> = HashMap::default();
817 if let Stmt::Block(vs) = &mut body.body {
818 for (i, st) in vs.iter_mut().enumerate() {
819 if let Some((var, idx)) = def_site.get(&i) {
820 cur.insert(*var, *idx);
821 }
822 crate::passes::walk_stmt_exprs(st, &mut |e| {
823 crate::passes::deep_rewrite(e, &mut |x| {
824 if let Expr::Local { var, .. } = x {
825 if let Some(&idx) = cur.get(var) {
826 let name: std::sync::Arc<str> =
827 std::sync::Arc::from(const_name[idx].as_str());
828 *x = Expr::Field {
829 owner: None,
830 cls: self_name.clone(),
831 name,
832 ty: self_ty.clone(),
833 is_static: true,
834 };
835 }
836 }
837 });
838 });
839 }
840 }
841 }
842
843 // Pass 4: drop the definitions and their sputs.
844 if let Stmt::Block(vs) = &mut body.body {
845 let drop_set: std::collections::HashSet<usize> =
846 drop_stmts.iter().copied().collect();
847 vs.retain(|_| true);
848 let mut idx = 0usize;
849 vs.retain(|_| {
850 let keep = !drop_set.contains(&idx);
851 idx += 1;
852 keep
853 });
854 }
855 // The resolved args consumed the clinit locals' readers; prune the
856 // now-dead defs from the remnant (impure inits survive as bare
857 // expression statements — drop_dead_locals' standard contract).
858 crate::passes::drop_dead_locals(&mut body.body);
859
860 let mut merged: Vec<(i64, EnumConst)> = const_ord
861 .into_iter()
862 .zip(
863 const_field
864 .into_iter()
865 .zip(const_name)
866 .zip(const_extra)
867 .map(|((field, name), extra_args)| EnumConst {
868 field,
869 name,
870 extra_args,
871 }),
872 )
873 .collect();
874 for (ord, name, extra_args) in synth {
875 merged.push((
876 ord,
877 EnumConst {
878 field: name.clone(),
879 name,
880 extra_args,
881 },
882 ));
883 }
884 if merged.is_empty() {
885 return dbg_enum_none(line!()); // nothing extracted — keep the desugared form
886 }
887 merged.sort_by_key(|(o, _)| *o);
888 // Ordinals must be UNIQUE and ASCENDING; gaps are legal — R8 drops
889 // unused constants wholesale (weixin lite/api/n starts at ordinal
890 // 1). Java derives ordinal() from declaration position, so a gap
891 // shifts every later constant. Pad each gap with a synthetic
892 // constant (`_r<k>`) to keep positions faithful — only when no
893 // constant carries extra ctor args (a pad cannot invent them).
894 // The synthetic values()/valueOf() the true-enum render suppresses
895 // then see the pad (values() drift vs the dex $VALUES array — the
896 // compilable-and-ordinal-faithful side of the trade).
897 let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
898 let mut member_taken: jdc_core::FxHashSet<String> = merged
899 .iter()
900 .map(|(_, c)| c.name.clone())
901 .chain(
902 class
903 .static_fields
904 .iter()
905 .chain(class.instance_fields.iter())
906 .map(|f| f.name.to_string()),
907 )
908 .chain(class.all_methods().map(|m| m.name.to_string()))
909 .collect();
910 let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
911 let mut expect: i64 = 0;
912 let mut pad_k = 0u32;
913 for (ord, c) in merged {
914 if ord < expect {
915 return dbg_enum_none(line!()); // duplicate/colliding ordinals — unfaithful
916 }
917 if ord > expect {
918 // Extras-bearing ctors: a pad may borrow an adjacent
919 // constant's literal extras ONLY when every ctor of the
920 // class is benign (no branch, no throw, no non-<init>
921 // invoke — pure param stores), so the invented value
922 // cannot change runtime behavior beyond a dead constant's
923 // stored field (jd1.u: gap at ordinal 7 + int extra arg
924 // aborted the whole promotion — 92 files of broken
925 // Enum.valueOf). Non-benign ctors keep the abort.
926 let tmpl: Vec<Expr> = if has_extras {
927 if !enum_ctors_benign(pool, class) {
928 return dbg_enum_none(line!()); // cannot synthesize the missing ctor args
929 }
930 match padded.last() {
931 Some(prev) => prev.extra_args.clone(),
932 None => c.extra_args.clone(),
933 }
934 } else {
935 Vec::new()
936 };
937 while expect < ord {
938 let pname = loop {
939 let cand = format!("_r{pad_k}");
940 pad_k += 1;
941 if !member_taken.contains(&cand) {
942 member_taken.insert(cand.clone());
943 break cand;
944 }
945 };
946 padded.push(EnumConst {
947 field: pname.clone(),
948 name: pname,
949 extra_args: tmpl.clone(),
950 });
951 expect += 1;
952 }
953 }
954 expect = ord + 1;
955 padded.push(c);
956 }
957 let out = padded;
958 Some((out, body))
959}
960
961/// True when every `<init>` of the class is a pure param-store: no
962/// branch, no throw, no switch, no goto, and no invoke other than a
963/// constructor call (the super delegation). A benign ctor cannot
964/// validate or leak its arguments, so gap-pad constants may borrow a
965/// neighbor's literal extras without observable effect.
966fn enum_ctors_benign(pool: &DexPool, class: &PoolClass) -> bool {
967 let mut checked = 0usize;
968 for m in class.all_methods() {
969 if &*m.name != "<init>" {
970 continue;
971 }
972 checked += 1;
973 let Some(dex) = pool.dex(m.dex_idx) else {
974 return false;
975 };
976 let Some(ci) = dex.code_at(m.code_off) else {
977 return false;
978 };
979 for ins in &ci.insns {
980 use ddc_dex::insn::InsnKind;
981 match ins.kind {
982 InsnKind::If { .. }
983 | InsnKind::Throw { .. }
984 | InsnKind::PackedSwitch { .. }
985 | InsnKind::SparseSwitch { .. }
986 | InsnKind::Goto { .. } => return false,
987 InsnKind::Invoke { method_idx, .. } => {
988 let mr = dex.method(method_idx);
989 if dex.string(mr.name_idx) != "<init>" {
990 return false;
991 }
992 }
993 _ => {}
994 }
995 }
996 }
997 checked > 0
998}
999
1000/// Update the rolling reaching-def map for enum-arg resolution. Only
1001/// flat single-target definitions keep the tracking sound; any control
1002/// flow taints it (a linear scan can no longer prove WHICH definition
1003/// reaches later capture sites).
1004fn track_def<'e>(
1005 st: &'e Stmt,
1006 defs: &mut std::collections::HashMap<u32, &'e Expr>,
1007 tainted: &mut bool,
1008) {
1009 match st {
1010 Stmt::LocalDef { var, init, .. } => match init {
1011 Some(e) => {
1012 defs.insert(*var, e);
1013 }
1014 None => {
1015 defs.remove(var);
1016 }
1017 },
1018 Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
1019 if let Expr::Local { var, .. } = &**target {
1020 if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
1021 defs.insert(*var, value);
1022 } else {
1023 defs.remove(var);
1024 }
1025 }
1026 }
1027 // Plain expression statements (calls) don't define locals.
1028 Stmt::ExprStmt(_) => {}
1029 _ => *tainted = true,
1030 }
1031}
1032
1033/// Resolve every enum-ctor extra arg to a self-contained expression, or
1034/// reject the enum mode (None). R8 reuses ONE register across all
1035/// constant constructions — revenuecat's LogIntent builds 11 of 12
1036/// emoji lists through the same `list` local, reassigned between the
1037/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
1038/// clinit where no local is in scope: an unresolved `Local` used to
1039/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
1040/// cannot-find). Each Local is replaced by its reaching pure definition
1041/// (recursively) or, when it names an earlier constant's intermediate,
1042/// by a static-field reference to that constant. Any expression shape
1043/// WITHOUT locals passes through untouched (enum args may be arbitrary
1044/// expressions — BinOp/Cast/Method/New — each renders exactly once per
1045/// constant, so no duplication concern applies). Only an unresolvable
1046/// Local (missing/tainted def, depth > 4) rejects the whole enum
1047/// detection; the class then falls back to plain-field rendering, which
1048/// always compiles.
1049fn resolve_enum_extras(
1050 extras: &[Expr],
1051 defs: &std::collections::HashMap<u32, &Expr>,
1052 tainted: bool,
1053 var_of: &std::collections::HashMap<u32, usize>,
1054 const_name: &[String],
1055 self_name: &std::sync::Arc<str>,
1056 self_ty: &jdc_core::ir::expr::TypeRef,
1057) -> Option<Vec<Expr>> {
1058 extras
1059 .iter()
1060 .map(|e| {
1061 let mut out = e.clone();
1062 let mut fail = false;
1063 resolve_locals_in(
1064 &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
1065 &mut fail,
1066 );
1067 if fail {
1068 None
1069 } else {
1070 Some(out)
1071 }
1072 })
1073 .collect()
1074}
1075
1076#[allow(clippy::too_many_arguments)]
1077fn resolve_locals_in(
1078 e: &mut Expr,
1079 defs: &std::collections::HashMap<u32, &Expr>,
1080 tainted: bool,
1081 var_of: &std::collections::HashMap<u32, usize>,
1082 const_name: &[String],
1083 self_name: &std::sync::Arc<str>,
1084 self_ty: &jdc_core::ir::expr::TypeRef,
1085 depth: u32,
1086 fail: &mut bool,
1087) {
1088 crate::passes::deep_rewrite(e, &mut |x| {
1089 if let Expr::Local { var, .. } = x {
1090 // An earlier constant's intermediate: a static-field read of
1091 // that constant (declared above — backward reference, legal
1092 // in enum ctor args).
1093 if let Some(&ci) = var_of.get(var) {
1094 if let Some(nm) = const_name.get(ci) {
1095 *x = Expr::Field {
1096 owner: None,
1097 cls: self_name.clone(),
1098 name: std::sync::Arc::from(nm.as_str()),
1099 ty: self_ty.clone(),
1100 is_static: true,
1101 };
1102 return;
1103 }
1104 }
1105 if tainted || depth > 4 {
1106 *fail = true;
1107 return;
1108 }
1109 let Some(d) = defs.get(var).copied() else {
1110 *fail = true;
1111 return;
1112 };
1113 let mut sub = d.clone();
1114 resolve_locals_in(
1115 &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
1116 depth + 1, fail,
1117 );
1118 if *fail {
1119 return;
1120 }
1121 *x = sub;
1122 }
1123 });
1124}
1125
1126/// Render enum-constant constructor arguments via the shared expression
1127/// emitter (the VarTable is irrelevant for argument printing — no local
1128/// names appear — but the API requires one).
1129fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
1130 let dummy_vt = jdc_core::var::VarTable::default();
1131 let mut p = Printer::new(ctx, &dummy_vt);
1132 for (i, a) in args.iter().enumerate() {
1133 if i > 0 {
1134 out.push_str(", ");
1135 }
1136 p.expr(a, 0, out);
1137 }
1138 let _ = pool;
1139}
1140
1141/// Synthesize `throw new AbstractMethodError()` stubs for abstract
1142/// interface methods a CONCRETE class fails to implement. R8 tree-shakes
1143/// an interface method whose call sites it proved unreachable, leaving a
1144/// concrete class ART accepts but javac rejects ("X不是抽象的, 并且未覆盖
1145/// 抽象方法…"). jadx reproduces the same incomplete class; the stub
1146/// restores compilability AND matches ART semantics exactly (invoking a
1147/// stripped abstract method throws AbstractMethodError). `provided` is
1148/// computed from the DEX method table (not the render), so a method ddc
1149/// merely failed to render is never double-declared, and a same-erasure
1150/// return-type-specialized sibling (Family B: void `invoke` beside the
1151/// Object bridge) counts as provided → no colliding stub.
1152struct ParsedStub {
1153 ret: JavaType,
1154 args: Vec<JavaType>,
1155}
1156
1157/// Ctor argument signatures to mirror on an empty-shell class whose
1158/// ancestor chain ends at a Throwable-family FRAMEWORK class — read
1159/// from the embedded API database (the hand-written 26-name table it
1160/// replaced could not cover app-specific chains and hard-coded the
1161/// sig set instead of the real one). Private ctors are skipped: the
1162/// bridge `super(..)` would not compile against them (Throwable's
1163/// 4-arg suppression ctor is private).
1164fn throwable_ctor_sigs(fw: &str) -> Option<Vec<String>> {
1165 if !crate::fwdb::is_subtype(fw, "java/lang/Throwable") {
1166 return None;
1167 }
1168 let mut out: Vec<String> = Vec::new();
1169 crate::fwdb::for_each_method(fw, |d, f| {
1170 if d.starts_with("<init>(")
1171 && f & crate::fwdb::MF_STATIC == 0
1172 && f & crate::fwdb::MF_PRIVATE == 0
1173 && f & crate::fwdb::MF_UNKNOWN == 0
1174 {
1175 let hi = d.find(')').unwrap_or(d.len());
1176 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1177 out.push(d[lo..hi].to_string());
1178 }
1179 });
1180 out.sort();
1181 out.dedup();
1182 Some(out)
1183}
1184
1185fn synth_missing_interface_stubs(
1186 pool: &DexPool,
1187 class: &PoolClass,
1188 out: &mut String,
1189 depth: usize,
1190 emitted_any: &mut bool,
1191) {
1192 use crate::access::*;
1193 // Concrete classes only: interfaces / abstract classes / annotations
1194 // legitimately leave methods unimplemented.
1195 if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
1196 return;
1197 }
1198 fn argsig(d: &str) -> &str {
1199 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1200 let hi = d.find(')').unwrap_or(d.len());
1201 &d[lo..hi]
1202 }
1203 // Framework interface method lists come from the embedded API
1204 // database (fwdb) — see the lookup below.
1205
1206 // Transitive interface closure — seeded from the WHOLE superclass
1207 // chain, not just the class's own `implements`: weixin rd extends
1208 // abstract k0 implements ey2.a, R8 stripped rd's onAttach, and the
1209 // old own-interfaces-only seed saw an empty requirement (×51
1210 // weixin a.onAttach family). Pool interfaces contribute their
1211 // method lists; framework interfaces consult the table above.
1212 // (name, argsig) -> the unique declaring desc, or None when two
1213 // ancestors declare the same erasure with DIFFERENT descriptors —
1214 // a Java-inexpressible diamond where any synthesized stub would
1215 // itself fail "无法覆盖/无法实现" (weibo +1016 regression when the
1216 // first-wins desc was stubbed against a conflicting sibling).
1217 let mut required: jdc_core::FxHashMap<(String, String), Option<(String, String)>> =
1218 jdc_core::FxHashMap::default();
1219 let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1220 let mut stack: Vec<String> = Vec::new();
1221 {
1222 let mut cur: Option<&PoolClass> = Some(class);
1223 let mut hops = 0;
1224 while let Some(c) = cur {
1225 hops += 1;
1226 if hops > 64 {
1227 break;
1228 }
1229 stack.extend(c.interfaces.iter().cloned());
1230 let mut hit_fw = false;
1231 cur = c.super_name.as_ref().and_then(|s| {
1232 if s == "java/lang/Object" {
1233 None
1234 } else {
1235 let pc = pool.get(s);
1236 if pc.is_none() {
1237 hit_fw = true;
1238 }
1239 pc
1240 }
1241 });
1242 if hit_fw {
1243 // A FRAMEWORK superclass contributes methods the pool
1244 // walk cannot see: the class may already provide (or be
1245 // barred from overriding — Context.getString is final)
1246 // any requirement, and a stub here collided ×669 weibo.
1247 // No stubs past the framework boundary.
1248 return;
1249 }
1250 }
1251 }
1252 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1253 while let Some(iname) = stack.pop() {
1254 if !seen.insert(iname.clone()) {
1255 continue;
1256 }
1257 let Some(ic) = pool.get(&iname) else {
1258 // Framework interface: the embedded API database provides
1259 // its abstract instance methods with real access flags
1260 // (abstract, not static, not default) — the hand-written
1261 // 13-interface table this replaced covered only classics.
1262 if crate::fwdb::class_flags(&iname) & crate::fwdb::CF_INTERFACE != 0 {
1263 crate::fwdb::for_each_method(&iname, |d, f| {
1264 if f & crate::fwdb::MF_ABSTRACT == 0
1265 || f & crate::fwdb::MF_STATIC != 0
1266 || f & crate::fwdb::MF_UNKNOWN != 0
1267 || d.starts_with("<init>")
1268 {
1269 return;
1270 }
1271 let hi = d.find('(').unwrap_or(d.len());
1272 let mn = d[..hi].to_string();
1273 let md = d[hi..].to_string();
1274 let key = (mn.clone(), argsig(&md).to_string());
1275 match required.entry(key) {
1276 std::collections::hash_map::Entry::Vacant(v) => {
1277 v.insert(Some((mn, md)));
1278 }
1279 std::collections::hash_map::Entry::Occupied(mut o) => {
1280 if o.get().as_ref().is_some_and(|(_, d0)| *d0 != md) {
1281 o.insert(None);
1282 }
1283 }
1284 }
1285 });
1286 }
1287 continue;
1288 };
1289 for m in ic.all_methods() {
1290 if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
1291 continue;
1292 }
1293 let key = (m.name.to_string(), argsig(&m.desc).to_string());
1294 if m.code_off == 0 {
1295 let mdesc = m.desc.to_string();
1296 match required.entry(key) {
1297 std::collections::hash_map::Entry::Vacant(v) => {
1298 v.insert(Some((m.name.to_string(), mdesc)));
1299 }
1300 std::collections::hash_map::Entry::Occupied(mut o) => {
1301 if o.get().as_ref().is_some_and(|(_, d)| *d != mdesc) {
1302 o.insert(None);
1303 }
1304 }
1305 }
1306 } else {
1307 defaults.insert(key);
1308 }
1309 }
1310 stack.extend(ic.interfaces.iter().cloned());
1311 }
1312 if required.is_empty() {
1313 return;
1314 }
1315 // Methods provided by the class + its concrete superclass chain, plus
1316 // the java/lang/Object publics that satisfy common requirements.
1317 let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1318 provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
1319 provided.insert(("hashCode".into(), String::new()));
1320 provided.insert(("toString".into(), String::new()));
1321 // (name, desc) pairs whose erasure twin exists elsewhere in the
1322 // chain with a DIFFERENT desc — stub-hostile shapes (see below).
1323 let mut chain_sigs: jdc_core::FxHashMap<(String, String), jdc_core::FxHashSet<String>> =
1324 jdc_core::FxHashMap::default();
1325 let mut cur: Option<&PoolClass> = Some(class);
1326 while let Some(c) = cur {
1327 for m in c.all_methods() {
1328 if !m.is_static() && &*m.name != "<init>" && &*m.name != "<clinit>" {
1329 chain_sigs
1330 .entry((m.name.to_string(), argsig(&m.desc).to_string()))
1331 .or_default()
1332 .insert(m.desc.to_string());
1333 }
1334 }
1335 for m in c.all_methods() {
1336 // A method PROVIDES the implementation if it is not abstract:
1337 // bytecode (code_off != 0) OR native (JNI body, code_off == 0
1338 // but ACC_NATIVE — `…ToNative` methods satisfy their interface
1339 // and must not be stubbed twice).
1340 if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
1341 // A RENAMED method no longer provides its raw (name,
1342 // argsig): impostor overrides (return type satisfying no
1343 // ancestor declaration) and erasure-clash losers render
1344 // under their registry display, and the ancestor
1345 // requirement falls to the stub below — matching ART,
1346 // where the interface proto never resolved either.
1347 let renamed = jdc_core::rename::field_display(&c.name, &m.name, &m.desc)
1348 .is_some();
1349 if !renamed {
1350 provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
1351 }
1352 }
1353 }
1354 cur = c.super_name.as_ref().and_then(|s| {
1355 if s == "java/lang/Object" {
1356 None
1357 } else {
1358 pool.get(s)
1359 }
1360 });
1361 }
1362 let mut chain_conflict: jdc_core::FxHashSet<(String, String)> =
1363 jdc_core::FxHashSet::default();
1364 for ((name, _argsig), descs) in chain_sigs.iter() {
1365 if descs.len() > 1 {
1366 for d in descs {
1367 chain_conflict.insert((name.clone(), d.clone()));
1368 }
1369 }
1370 }
1371 let mut missing: Vec<(String, String)> = Vec::new();
1372 for (key, m) in &required {
1373 let Some(m) = m else { continue }; // conflicting ancestor descs
1374 if defaults.contains(key) || provided.contains(key) {
1375 continue;
1376 }
1377 // Kotlin bridge variant (return kotlin/Unit beside the real
1378 // boolean/object twin a framework interface declares): stubbing
1379 // it renders `java.lang.Void addAll(Collection)` that neither
1380 // implements the twin nor serves its callers — the poisoned
1381 // return type cascades through every call site (weibo +703
1382 // regression). The honest 未覆盖 error for the twin stays.
1383 if m.1.ends_with(")Lkotlin/Unit;") {
1384 continue;
1385 }
1386 // A chain method sharing the erasure but NOT the descriptor
1387 // (abstract super with a different return, an impostor the
1388 // rename pass left because it satisfies another ancestor):
1389 // the stub would clash with it — skip; the class keeps the
1390 // single honest 未覆盖 error instead of gaining a 无法覆盖 one.
1391 if chain_conflict.contains(&(m.0.clone(), m.1.clone())) {
1392 continue;
1393 }
1394 missing.push(m.clone());
1395 }
1396 missing.sort();
1397 let ind = " ".repeat(depth + 1);
1398 for (mname, mdesc) in missing {
1399 let lo = mdesc.find('(').map(|i| i + 1).unwrap_or(0);
1400 let hi = mdesc.find(')').unwrap_or(mdesc.len());
1401 let ret_s = &mdesc[hi + 1..];
1402 let args_v = split_arg_descs(&mdesc[lo..hi]);
1403 let d = ParsedStub { ret: crate::desc_type(ret_s), args: args_v };
1404 if *emitted_any {
1405 out.push('\n');
1406 }
1407 out.push_str(&ind);
1408 out.push_str("public ");
1409 out.push_str(&type_name(pool, &d.ret));
1410 out.push(' ');
1411 out.push_str(&java_ident(&mname));
1412 out.push('(');
1413 for (i, a) in d.args.iter().enumerate() {
1414 if i > 0 {
1415 out.push_str(", ");
1416 }
1417 out.push_str(&type_name(pool, a));
1418 out.push_str(&format!(" p{}", i + 1));
1419 }
1420 out.push_str(") {\n");
1421 out.push_str(&ind);
1422 out.push_str(" throw new AbstractMethodError();\n");
1423 out.push_str(&ind);
1424 out.push_str("}\n");
1425 *emitted_any = true;
1426 }
1427}
1428
1429/// Synthetic storage fields backing name()/ordinal() on a FALLBACK enum
1430/// (a plain class cannot inherit java.lang.Enum's). The enum ctor's
1431/// (String, int) trace params feed them (injected in emit_method).
1432const ENUM_NAME_FIELD: &str = "$ddcName";
1433const ENUM_ORD_FIELD: &str = "$ddcOrdinal";
1434
1435/// True when the class renders as a fallback enum BASE: ACC_ENUM,
1436/// restoration failed (enum_consts None — caller-gated), super is the
1437/// Enum root. Constant subclasses inherit the synthesized members.
1438fn fallback_enum_base(class: &PoolClass) -> bool {
1439 class.is_enum()
1440 && class
1441 .super_name
1442 .as_ref()
1443 .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
1444 .unwrap_or(true)
1445}
1446
1447/// Full gate for the synthetic Enum surface: fallback base WITH the
1448/// trace ctor and no `$ddc*` field-name clash. The ctor injection in
1449/// emit_method, the member synthesis and the dex-valueOf suppression
1450/// must all agree, or fields/ctor/renders desynchronize.
1451fn enum_synth_active(class: &PoolClass) -> bool {
1452 fallback_enum_base(class)
1453 && enum_trace_ctor(class)
1454 && !class
1455 .static_fields
1456 .iter()
1457 .chain(class.instance_fields.iter())
1458 .any(|f| f.name == ENUM_NAME_FIELD || f.name == ENUM_ORD_FIELD)
1459}
1460
1461/// Does the class have the compiler-generated `(String, int, ..)` enum
1462/// ctor whose leading params feed the synthetic name/ordinal fields?
1463fn enum_trace_ctor(class: &PoolClass) -> bool {
1464 class.all_methods().any(|m| {
1465 &*m.name == "<init>"
1466 && m.parsed_desc()
1467 .map(|d| {
1468 matches!(d.args.first(), Some(JavaType::Object(s))
1469 if s.as_ref() == "java/lang/String")
1470 && matches!(d.args.get(1), Some(JavaType::Int))
1471 })
1472 .unwrap_or(false)
1473 })
1474}
1475
1476/// Synthesize the java.lang.Enum member surface a FALLBACK enum base
1477/// lacks (it renders as a plain class — `extends Enum` is illegal in
1478/// source): storage fields `$ddcName`/`$ddcOrdinal` (fed by the ctor
1479/// trace-param injection in emit_method), `name()`, `ordinal()`,
1480/// `valueOf(String)` and `compareTo`, plus `values()` when R8 stripped
1481/// the dex copy. Kills the illegal dex `Enum.valueOf(X.class, ..)`
1482/// ("无法将类 Enum<E>中的方法 valueOf应用到给定类型" — weixin ×1,307,
1483/// weibo ×477) and makes ordinal() EXACT (the interim values()-scan
1484/// misread enums whose $VALUES carries register-reuse duplicates,
1485/// protobuf ub). Purely additive: the dex never declares these members
1486/// on the enum class (they are Enum inheritances), so no collision —
1487/// except the dex valueOf, suppressed at the method loop.
1488fn synth_enum_members(
1489 pool: &DexPool,
1490 class: &PoolClass,
1491 out: &mut String,
1492 depth: usize,
1493 emitted_any: &mut bool,
1494) {
1495 use crate::access::*;
1496 if !fallback_enum_base(class) {
1497 return;
1498 }
1499 let self_ty = JavaType::Object(class.name.as_str().into());
1500 let self_name = type_name(pool, &self_ty);
1501 let self_arr = JavaType::Array(Box::new(self_ty));
1502 let arr_name = type_name(pool, &self_arr);
1503 let traced = enum_synth_active(class);
1504 let has_values = class.all_methods().any(|m| {
1505 &*m.name == "values"
1506 && m.access & ACC_STATIC != 0
1507 && m.parsed_desc()
1508 .map(|d| d.ret == self_arr && d.args.is_empty())
1509 .unwrap_or(false)
1510 });
1511 let ind = " ".repeat(depth + 1);
1512 let push_line = |out: &mut String, l: &str| {
1513 out.push_str(&ind);
1514 out.push_str(l);
1515 out.push('\n');
1516 };
1517 // values(): from the $VALUES array field when the dex copy is gone.
1518 if !has_values {
1519 let want = format!("[L{};", class.name);
1520 if let Some(vf) = class.static_fields.iter().find(|f| f.desc == want) {
1521 if *emitted_any {
1522 out.push('\n');
1523 }
1524 *emitted_any = true;
1525 push_line(out, &format!("public static {} values() {{", arr_name));
1526 push_line(
1527 out,
1528 &format!(" return ({}) {}.clone();", arr_name, java_ident(&vf.name)),
1529 );
1530 push_line(out, "}");
1531 }
1532 }
1533 if !traced {
1534 return; // no (String,int) ctor: fields would stay default — keep
1535 // the scan-based ordinal only (below) and the dex valueOf.
1536 }
1537 if *emitted_any {
1538 out.push('\n');
1539 }
1540 *emitted_any = true;
1541 push_line(out, &format!("private java.lang.String {};", ENUM_NAME_FIELD));
1542 push_line(out, &format!("private int {};", ENUM_ORD_FIELD));
1543 out.push('\n');
1544 push_line(out, &format!(
1545 "public java.lang.String name() {{\n{} return this.{};\n{}}}",
1546 ind, ENUM_NAME_FIELD, ind
1547 ));
1548 out.push('\n');
1549 push_line(out, &format!(
1550 "public int ordinal() {{\n{} return this.{};\n{}}}",
1551 ind, ENUM_ORD_FIELD, ind
1552 ));
1553 out.push('\n');
1554 push_line(out, &format!(
1555 "public int compareTo({0} o) {{\n{1} return this.{2} - o.{2};\n{1}}}",
1556 self_name, ind, ENUM_ORD_FIELD
1557 ));
1558 out.push('\n');
1559 push_line(out, &format!(
1560 "public static {0} valueOf(java.lang.String s) {{\n{1} for ({0} v : values()) {{\n{1} if (v.name().equals(s)) {{\n{1} return v;\n{1} }}\n{1} }}\n{1} throw new java.lang.IllegalArgumentException(s);\n{1}}}",
1561 self_name, ind
1562 ));
1563}
1564
1565/// Scan-based ordinal() fallback for fallback enums WITHOUT the trace
1566/// ctor (rare): position in values(). Kept separate so synth_enum_members
1567/// can bail after values() synthesis.
1568fn synth_enum_ordinal_scan(
1569 pool: &DexPool,
1570 class: &PoolClass,
1571 out: &mut String,
1572 depth: usize,
1573 emitted_any: &mut bool,
1574) {
1575 if !fallback_enum_base(class) || enum_trace_ctor(class) {
1576 return;
1577 }
1578 if class
1579 .all_methods()
1580 .any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
1581 {
1582 return;
1583 }
1584 let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
1585 let arr_name = type_name(pool, &self_arr);
1586 let ind = " ".repeat(depth + 1);
1587 if *emitted_any {
1588 out.push('\n');
1589 }
1590 *emitted_any = true;
1591 out.push_str(&ind);
1592 out.push_str("public int ordinal() {\n");
1593 out.push_str(&ind);
1594 out.push_str(&format!(" {} vs = values();\n", arr_name));
1595 out.push_str(&ind);
1596 out.push_str(" for (int i = 0; i < vs.length; i++) {\n");
1597 out.push_str(&ind);
1598 out.push_str(" if (vs[i] == this) {\n");
1599 out.push_str(&ind);
1600 out.push_str(" return i;\n");
1601 out.push_str(&ind);
1602 out.push_str(" }\n");
1603 out.push_str(&ind);
1604 out.push_str(" }\n");
1605 out.push_str(&ind);
1606 out.push_str(" return 0;\n");
1607 out.push_str(&ind);
1608 out.push_str("}\n");
1609}
1610
1611/// Strip the Meituan Robust hotpatch guard from a RESTORED enum ctor:
1612/// `Object[] v3 = {name, ordinal}; Class[] v4 = ..; boolean s =
1613/// PatchProxy.isSupport(v3, this, changeQuickRedirect, ..); if (s) {
1614/// PatchProxy.accessDispatch(..); return; }`. The guard reads the enum's
1615/// own static field — bytecode-legal (Robust instruments after javac),
1616/// source-illegal (JLS 8.9.2, both simple AND qualified forms). It is
1617/// dead code for decompiled output (no patch is ever loaded). Removal is
1618/// surgical: statements containing a PatchProxy call, the locals feeding
1619/// its arguments, their defs and element writes — a fixpoint over the
1620/// top-level list. collect_enum_constants' dex gate already refused
1621/// restoration when self-static reads go beyond the redirect field.
1622fn strip_robust_enum_ctor_guard(body: &mut Stmt) {
1623 let is_patchy = |e: &Expr| {
1624 matches!(e, Expr::Method { cls, name, .. }
1625 if cls.contains("PatchProxy")
1626 && (&**name == "isSupport"
1627 || &**name == "accessDispatch"
1628 || &**name == "proxy"
1629 || &**name == "isSupportClinit"
1630 || &**name == "accessDispatchClinit"))
1631 };
1632 let Stmt::Block(vs) = body else { return };
1633 if !vs.iter().any(|st| {
1634 let mut h = false;
1635 crate::passes::visit_all_exprs(st, &mut |x| {
1636 if is_patchy(x) {
1637 h = true;
1638 }
1639 });
1640 h
1641 }) {
1642 return;
1643 }
1644 let mut dead: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
1645 // Locals read inside the PatchProxy call args of a statement (the
1646 // pack arrays) — dead once the statement goes.
1647 let patchy_arg_locals = |st: &Stmt, out: &mut jdc_core::FxHashSet<u32>| {
1648 crate::passes::visit_all_exprs(st, &mut |x| {
1649 if let Expr::Method { args, .. } = x {
1650 if is_patchy(x) {
1651 for a in args {
1652 let mut aa = a.clone();
1653 crate::passes::deep_rewrite(&mut aa, &mut |y| {
1654 if let Expr::Local { var, .. } = y {
1655 out.insert(*var);
1656 }
1657 });
1658 }
1659 }
1660 }
1661 });
1662 };
1663 for _ in 0..4 {
1664 let before = vs.len();
1665 let mut i = 0usize;
1666 while i < vs.len() {
1667 let action = {
1668 let st = &vs[i];
1669 match st {
1670 Stmt::LocalDef { var, .. } if dead.contains(var) => Some((*var, false)),
1671 Stmt::LocalDef { var, init: Some(e), .. } => {
1672 let mut has_patchy = false;
1673 let mut ec = e.clone();
1674 crate::passes::deep_rewrite(&mut ec, &mut |x| {
1675 if is_patchy(x) {
1676 has_patchy = true;
1677 }
1678 });
1679 if has_patchy {
1680 Some((*var, true))
1681 } else {
1682 None
1683 }
1684 }
1685 Stmt::If { .. } => {
1686 let mut h = false;
1687 crate::passes::visit_all_exprs(st, &mut |x| {
1688 if is_patchy(x) {
1689 h = true;
1690 }
1691 });
1692 if h {
1693 Some((u32::MAX, true))
1694 } else {
1695 None
1696 }
1697 }
1698 // `v3[0] = str;` / `v3 = ..;` writes to a dead pack.
1699 Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1700 let mut tgt_dead = false;
1701 let mut tc = target.clone();
1702 crate::passes::deep_rewrite(&mut tc, &mut |x| {
1703 if let Expr::Local { var, .. } = x {
1704 if dead.contains(var) {
1705 tgt_dead = true;
1706 }
1707 }
1708 });
1709 if tgt_dead {
1710 Some((u32::MAX, false))
1711 } else {
1712 None
1713 }
1714 }
1715 _ => None,
1716 }
1717 };
1718 if let Some((var, collect_args)) = action {
1719 if collect_args {
1720 patchy_arg_locals(&vs[i], &mut dead);
1721 }
1722 if var != u32::MAX {
1723 dead.insert(var);
1724 }
1725 vs.remove(i);
1726 } else {
1727 i += 1;
1728 }
1729 }
1730 if vs.len() == before {
1731 break;
1732 }
1733 }
1734}
1735
1736#[allow(clippy::only_used_in_recursion)]
1737fn emit_class_body(
1738 pool: &DexPool,
1739 class: &PoolClass,
1740 ctx: &DexCtx<'_>,
1741 opts: &ClassOptions,
1742 out: &mut String,
1743 depth: usize,
1744) -> anyhow::Result<()> {
1745 let ind = indent(depth);
1746 // The class's own header must use the RENAMED display name — the
1747 // ctor path (is_init) already renames; a header declaring `class a`
1748 // while the file/ctor say `a_2` leaves the ctor looking like a
1749 // method with no return type (MinisApp's Y2.a vs y2.a package
1750 // case-collision: 2,518 javac parse errors).
1751 let cname = crate::apply_class_rename(&class.name);
1752 let (_, simple) = split_name(&cname);
1753 // A class emitted as its OWN top-level file (depth 0) must declare a
1754 // flat `$` name — `class Outer.Inner` is not declarable at file
1755 // scope. An INLINE nested member (depth > 0) declares its own
1756 // segment inside the parent's body.
1757 let simple = if depth == 0 {
1758 simple.to_string()
1759 } else {
1760 // R8 names can END in `$`: an empty last `$`-segment would
1761 // render `class {` — keep the whole simple name.
1762 let seg = simple.rsplit('$').next().unwrap_or(&simple);
1763 if seg.is_empty() {
1764 simple.to_string()
1765 } else {
1766 seg.to_string()
1767 }
1768 };
1769 // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
1770 // declared — restricted contextual type names escape here and in
1771 // the ctor, file name, and every type reference.
1772 let simple = if is_restricted_type_name(&simple) {
1773 format!("_{simple}")
1774 } else {
1775 simple
1776 };
1777 let is_iface = class.is_interface();
1778 let is_enum = class.is_enum();
1779
1780 let mut head = String::new();
1781 let a = class.access;
1782 if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
1783 head.push_str("public ");
1784 }
1785 // Inline nested members need their `static` (interfaces/annotations
1786 // are implicitly static; a missing `static` on a member class makes
1787 // every `new Report(...)` site an "outer instance required" error).
1788 if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
1789 head.push_str("static ");
1790 }
1791 if a & ACC_FINAL != 0 && !is_enum {
1792 head.push_str("final ");
1793 }
1794 if a & ACC_ABSTRACT != 0 && !is_iface {
1795 head.push_str("abstract ");
1796 }
1797 if a & ACC_ANNOTATION != 0 {
1798 head.push('@');
1799 }
1800 let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
1801 collect_enum_constants(pool, class, ctx)
1802 } else {
1803 None
1804 };
1805 if let Some(ecs) = &enum_consts {
1806 // True `enum` declaration: constants render in the header, the
1807 // desugared boilerplate (const fields, their <clinit> inits —
1808 // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
1809 // disappears. R8-renamed values()/valueOf() stay (they do not
1810 // collide with the compiler-generated ones); javac-named ones
1811 // are skipped at the method loop below.
1812 head.push_str("enum ");
1813 head.push_str(&sanitize_ref(&simple));
1814 let _ = ecs;
1815 } else if is_enum {
1816 // An enum with constant-specific bodies carries ACC_ABSTRACT —
1817 // `abstract final` is an illegal modifier combination; abstract
1818 // (already pushed above) suppresses the hardcoded final.
1819 let abstract_ = a & ACC_ABSTRACT != 0;
1820 head.push_str(if abstract_ {
1821 "/* enum */ class "
1822 } else {
1823 "/* enum */ final class "
1824 });
1825 head.push_str(&sanitize_ref(&simple));
1826 } else if is_iface {
1827 head.push_str("interface ");
1828 head.push_str(&sanitize_ref(&simple));
1829 } else {
1830 head.push_str("class ");
1831 head.push_str(&sanitize_ref(&simple));
1832 }
1833 // The obscured-super import was emitted at the package line; the
1834 // clause must render the SIMPLE name (the qualified form binds to
1835 // the class itself even with the import present).
1836 let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
1837 if is_iface {
1838 // A @interface cannot declare extends at all (JLS 9.6): the dex
1839 // interface table lists java/lang/annotation/Annotation for
1840 // every annotation type — rendering it produced "对于
1841 // @interfaces, 不允许 'extends'" (weibo 1100).
1842 let is_annot = a & ACC_ANNOTATION != 0;
1843 if !class.interfaces.is_empty() && !is_annot {
1844 head.push_str(" extends ");
1845 head.push_str(&join_dotted(pool, &class.interfaces));
1846 }
1847 } else {
1848 if let Some(sup) = &class.super_name {
1849 // Suppress `extends` for TRUE `enum` declarations (they
1850 // implicitly extend java.lang.Enum) and for the enum BASE in
1851 // fallback form (super IS java/lang/Enum — `class X extends
1852 // Enum` is illegal). But an ENUM CONSTANT SUBCLASS rendered
1853 // in fallback (`/* enum */ class j$1`, ACC_ENUM, super = the
1854 // enum base) MUST keep `extends j`: without it the constant
1855 // loses its subtype relation and every `field = new j$1(..)`
1856 // fails ("j$1无法转换为j", weibo jsoup TokeniserState ×172).
1857 let suppress_extends =
1858 is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
1859 if sup != "java/lang/Object" && !suppress_extends {
1860 head.push_str(" extends ");
1861 head.push_str(&render_super(sup));
1862 }
1863 }
1864 if !class.interfaces.is_empty() {
1865 head.push_str(if is_iface {
1866 " extends "
1867 } else {
1868 " implements "
1869 });
1870 head.push_str(&join_dotted(pool, &class.interfaces));
1871 }
1872 }
1873
1874 out.push_str(&ind);
1875 out.push_str(&head);
1876 out.push_str(" {\n");
1877
1878 // True-enum constant list: the constants lead the body, ahead of
1879 // any remaining fields.
1880 if let Some((ecs, _)) = &enum_consts {
1881 for (i, ec) in ecs.iter().enumerate() {
1882 out.push_str(&indent(depth + 1));
1883 out.push_str(&java_ident(&ec.name));
1884 if !ec.extra_args.is_empty() {
1885 let mut line = String::from("(");
1886 render_enum_args(ctx, pool, &ec.extra_args, &mut line);
1887 line.push(')');
1888 out.push_str(&line);
1889 }
1890 if i + 1 < ecs.len() {
1891 out.push_str(",\n");
1892 } else {
1893 out.push_str(";\n");
1894 }
1895 }
1896 out.push('\n');
1897 }
1898
1899 // Fields.
1900 // A static final whose dex-encoded static value is OVERWRITTEN by
1901 // the clinit must render as a blank final: the dex encoded array
1902 // value is just the class-load default (ART lets <clinit> reassign
1903 // static finals), but Java forbids touching an initialized final
1904 // ("无法为 static final 变量 $stable 分配值" — Compose `$stable = 0`
1905 // + clinit `= 8`; WalletBaseUI HARDCODE_* = 0 + clinit computed
1906 // value; the original null-valued shape: Kotlin `object` INSTANCE,
1907 // okio SegmentPool). Scan the clinit's raw SPuts once.
1908 let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
1909 // Gate: only classes with a VALUED final static can hit the
1910 // conflict — skip the clinit decode otherwise (decoding it for
1911 // every class cost seconds on 98k-class corpora).
1912 (|| {
1913 let has_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
1914 f.access & crate::access::ACC_FINAL != 0
1915 && class.static_values.get(i).is_some()
1916 });
1917 if !has_valued_final {
1918 return None;
1919 }
1920 let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
1921 let dex = pool.dex(m.dex_idx)?;
1922 let ci = dex.code_at(m.code_off)?;
1923 let mut set: jdc_core::FxHashSet<(
1924 std::sync::Arc<str>,
1925 std::sync::Arc<str>,
1926 )> = jdc_core::FxHashSet::default();
1927 for ins in &ci.insns {
1928 if let InsnKind::SPut { field_idx, .. } = ins.kind {
1929 let fid = dex.field(field_idx);
1930 // clinit only writes own-class fields here; match
1931 // (name, type) against the field list below.
1932 set.insert((
1933 std::sync::Arc::from(dex.string(fid.name_idx)),
1934 std::sync::Arc::from(dex.type_name(fid.type_idx)),
1935 ));
1936 }
1937 }
1938 Some(set)
1939 })();
1940 let mut field_emitted = false;
1941 for (i, f) in class.static_fields.iter().enumerate() {
1942 // Enum constant fields became the header list above.
1943 if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
1944 continue;
1945 }
1946 if field_emitted || !class.instance_fields.is_empty() {
1947 out.push('\n');
1948 }
1949 field_emitted = true;
1950 // A blank-final: static final, clinit-assigned. Interfaces are
1951 // excluded — their fields are implicitly `public static final`
1952 // and MUST carry an initializer.
1953 let blank_final = f.access & crate::access::ACC_FINAL != 0
1954 && !class.is_interface()
1955 && clinit_sputs
1956 .as_ref()
1957 .is_some_and(|s| {
1958 s.contains(&(
1959 std::sync::Arc::from(f.name.as_str()),
1960 std::sync::Arc::from(f.desc.as_str()),
1961 ))
1962 });
1963 emit_field(
1964 pool,
1965 f,
1966 if blank_final {
1967 None
1968 } else {
1969 class.static_values.get(i)
1970 },
1971 &class.name,
1972 out,
1973 depth + 1,
1974 true,
1975 class.is_interface(),
1976 );
1977 }
1978 if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1979 out.push('\n');
1980 }
1981 for (i, f) in class.instance_fields.iter().enumerate() {
1982 if i > 0 {
1983 out.push('\n');
1984 }
1985 emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1986 }
1987
1988 // Methods.
1989 // Duplicated (name, parameter-types) pairs in one class are javac
1990 // "method is already defined" errors (weibo: 11.5k hits). Source
1991 // cannot declare them, so exactly one survives: covariant bridges
1992 // (`Object get(int)` beside `ByteString get(int)` — the compiler
1993 // GENERATES bridges), plus R8 output that lost the bridge flag
1994 // (okio Buffer.clone, interface getView re-declarations). A
1995 // non-bridge method outranks a bridge for the same signature
1996 // (bridge bodies are delegation stubs); first occurrence otherwise.
1997 // Never drop a signature outright — the last method standing for a
1998 // key is always rendered.
1999 // Keyed on the DISPLAY name: a member-renamed method (return-type-
2000 // only collisions on isolated classes) no longer collides with its
2001 // sibling — both render, each caller resolving through the registry.
2002 fn sig_key<'m>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
2003 let d: &'m str = &m.desc;
2004 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2005 let hi = d.find(')').unwrap_or(d.len());
2006 let n = if renames_on {
2007 jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
2008 } else {
2009 &*m.name
2010 };
2011 (n, &d[lo..hi])
2012 }
2013 let renames_on = jdc_core::rename::member_rename_active();
2014 let suppress_dex_valueof = class.is_enum()
2015 && enum_consts.is_none()
2016 && enum_synth_active(class);
2017 let methods: Vec<&PoolMethod> = class.all_methods().collect();
2018 let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
2019 for (i, m) in methods.iter().enumerate() {
2020 let key = sig_key(&class.name, renames_on, m);
2021 match claim.get(&key) {
2022 Some(&j)
2023 if m.access & crate::access::ACC_BRIDGE == 0
2024 && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
2025 {
2026 claim.insert(key, i);
2027 }
2028 None => {
2029 claim.insert(key, i);
2030 }
2031 _ => {}
2032 }
2033 }
2034 // NOTE: a same-signature bridge is already retired by the claim map
2035 // above. An ERASURE-shaped bridge (SAM/variance: params are the
2036 // Object-erased types of a sibling's) must NOT be skipped: with raw
2037 // (non-generic) interface rendering the bridge is exactly what
2038 // satisfies the interface — skipping it turned every Kotlin lambda
2039 // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
2040 // +17), while the "对invoke的引用不明确" it appeared to fix was a
2041 // missing-kotlin-classpath cascade, not a ddc bug.
2042 let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
2043 for (i, m) in methods.iter().enumerate() {
2044 if &*m.name == "<clinit>" {
2045 continue; // rendered after the fields
2046 }
2047 if claim.get(&sig_key(&class.name, renames_on, m)) != Some(&i) {
2048 continue;
2049 }
2050 if bridge_shadowed_by_ancestor(pool, class, m) {
2051 continue;
2052 }
2053 // Fallback enum base: the dex valueOf body is
2054 // `Enum.valueOf(X.class, str)` — illegal once X is a plain class
2055 // (T is not bounded by Enum). Suppress it; synth_enum_members
2056 // renders the values()-scan replacement.
2057 if suppress_dex_valueof
2058 && m.is_static()
2059 && &*m.name == "valueOf"
2060 && m.parsed_desc().map(|d| {
2061 d.args.len() == 1
2062 && matches!(d.args[0], JavaType::Object(ref sn)
2063 if sn.as_ref() == "java/lang/String")
2064 && d.ret == JavaType::Object(class.name.as_str().into())
2065 }).unwrap_or(false)
2066 {
2067 continue;
2068 }
2069 // True-enum rendering: javac auto-generates values()/valueOf()
2070 // — the original-named ones must not re-declare (R8-renamed
2071 // copies stay, they do not collide).
2072 if let Some((ecs, _)) = &enum_consts {
2073 let d = m.parsed_desc();
2074 let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
2075 let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
2076 let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
2077 let one_str = d
2078 .as_ref()
2079 .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
2080 .unwrap_or(false);
2081 let static_ = m.is_static();
2082 if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
2083 continue;
2084 }
2085 if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
2086 continue;
2087 }
2088 // Enum constructors must be private in source form.
2089 let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
2090 let mut c = (*m).clone();
2091 c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
2092 Some(c)
2093 } else {
2094 None
2095 };
2096 let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
2097 let mark = out.len();
2098 if emitted_any {
2099 out.push('\n');
2100 }
2101 if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
2102 emitted_any = true;
2103 } else {
2104 out.truncate(mark);
2105 }
2106 let _ = ecs;
2107 continue;
2108 }
2109 let mark = out.len();
2110 if emitted_any {
2111 out.push('\n');
2112 }
2113 if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
2114 emitted_any = true;
2115 } else {
2116 out.truncate(mark);
2117 }
2118 }
2119 // Inherited-ctor bridges: dex method refs resolve through the
2120 // hierarchy, so `new C(args)` against a class C with NO declared
2121 // <init> legally targets the SUPERCLASS ctor (weixin tenpay
2122 // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
2123 // has no inherited constructors: without a bridge, C's implicit
2124 // default ctor is the only one and every arg-carrying construction
2125 // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
2126 // Mirror the nearest ctor-declaring ancestor's public/protected
2127 // ctors as thin `super(..)` delegations.
2128 if !class.is_interface()
2129 && enum_consts.is_none()
2130 && !class.all_methods().any(|m| &*m.name == "<init>")
2131 {
2132 if let Some(mut sup) = class.super_name.clone() {
2133 loop {
2134 if sup == "java/lang/Object" {
2135 break;
2136 }
2137 let Some(sc) = pool.get(&sup) else { break };
2138 let ctors: Vec<&PoolMethod> = sc
2139 .all_methods()
2140 .filter(|m| &*m.name == "<init>")
2141 .collect();
2142 if !ctors.is_empty() {
2143 for sm in ctors {
2144 if sm.access & crate::access::ACC_PRIVATE != 0
2145 || (sm.access
2146 & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2147 == 0)
2148 {
2149 continue; // private/package-private: no legal bridge
2150 }
2151 let Some(d) = sm.parsed_desc() else { continue };
2152 let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2153 "public "
2154 } else {
2155 "protected "
2156 };
2157 if emitted_any {
2158 out.push('\n');
2159 }
2160 out.push_str(&format!(" {}", " ".repeat(depth)));
2161 out.push_str(mods);
2162 out.push_str(&sanitize_ref(&simple));
2163 out.push('(');
2164 let mut names = Vec::with_capacity(d.args.len());
2165 for (i, a) in d.args.iter().enumerate() {
2166 if i > 0 {
2167 out.push_str(", ");
2168 }
2169 out.push_str(&type_name(pool, a));
2170 let nm = format!("p{}", i + 1);
2171 out.push(' ');
2172 out.push_str(&nm);
2173 names.push(nm);
2174 }
2175 out.push_str(") {\n");
2176 out.push_str(&format!(" {} super({});\n", " ".repeat(depth), names.join(", ")));
2177 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2178 emitted_any = true;
2179 }
2180 break;
2181 }
2182 match &sc.super_name {
2183 Some(n) => sup = n.clone(),
2184 None => break,
2185 }
2186 }
2187 }
2188 }
2189
2190 // Referenced-but-undeclared inherited-ctor bridges. The block above
2191 // covers a class with NO declared ctor; this covers a class that
2192 // DECLARES ctors but is missing one that dex callers reach THROUGH
2193 // it. Dex resolves a `<init>` method-ref up the superclass chain, so
2194 // a subclass `super(2, recv, owner, name, sig, flags)` may target
2195 // `m.<init>(int,Object,Class,String,String,int)` that `m`
2196 // (FunctionReferenceImpl) does NOT declare — its super `l` does.
2197 // Java has no inherited ctors, so without a bridge on `m` every such
2198 // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
2199 // referenced sig C lacks from the nearest ancestor declaring it, as a
2200 // thin `super(..)` delegation. Ref-gated (only sigs actually called
2201 // through C) so ordinary classes gain nothing.
2202 if !class.is_interface()
2203 && enum_consts.is_none()
2204 && class.all_methods().any(|m| &*m.name == "<init>")
2205 {
2206 if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2207 fn arg_sig(d: &str) -> &str {
2208 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2209 let hi = d.find(')').unwrap_or(d.len());
2210 &d[lo..hi]
2211 }
2212 let declared: jdc_core::FxHashSet<&str> = class
2213 .all_methods()
2214 .filter(|m| &*m.name == "<init>")
2215 .map(|m| arg_sig(&m.desc))
2216 .collect();
2217 let mut missing: Vec<&String> = refs
2218 .iter()
2219 .filter(|s| !declared.contains(s.as_str()))
2220 .collect();
2221 missing.sort();
2222 for sig in missing {
2223 // Nearest ancestor declaring a public/protected <init>
2224 // with this exact arg signature.
2225 let mut sup = class.super_name.clone();
2226 let mut found: Option<&PoolMethod> = None;
2227 while let Some(s) = sup {
2228 if s == "java/lang/Object" {
2229 break;
2230 }
2231 let Some(sc) = pool.get(&s) else { break };
2232 if let Some(sm) = sc.all_methods().find(|m| {
2233 &*m.name == "<init>"
2234 && arg_sig(&m.desc) == sig.as_str()
2235 && m.access & crate::access::ACC_PRIVATE == 0
2236 && m.access
2237 & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2238 != 0
2239 }) {
2240 found = Some(sm);
2241 break;
2242 }
2243 sup = sc.super_name.clone();
2244 }
2245 let Some(sm) = found else {
2246 // The walk terminated at java/lang/Object — which
2247 // always declares <init>()V, so an empty-sig ref
2248 // through this class resolved there dex-wise. The
2249 // Java implicit default ctor is gone (the class
2250 // declares other ctors); mirror it as an explicit
2251 // public bridge (AgentMenuTask$Request →
2252 // AppBrandProxyUIProcessTask$ProcessRequest:
2253 // "需要: Parcel 找到: 没有参数", weixin 205).
2254 if sig.is_empty() {
2255 if emitted_any {
2256 out.push('\n');
2257 }
2258 out.push_str(&format!(" {}", " ".repeat(depth)));
2259 out.push_str("public ");
2260 out.push_str(&sanitize_ref(&simple));
2261 out.push_str("() {\n");
2262 out.push_str(&format!(
2263 " {} super();\n",
2264 " ".repeat(depth)
2265 ));
2266 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2267 emitted_any = true;
2268 }
2269 continue;
2270 };
2271 let Some(d) = sm.parsed_desc() else { continue };
2272 let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2273 "public "
2274 } else {
2275 "protected "
2276 };
2277 if emitted_any {
2278 out.push('\n');
2279 }
2280 out.push_str(&format!(" {}", " ".repeat(depth)));
2281 out.push_str(mods);
2282 out.push_str(&sanitize_ref(&simple));
2283 out.push('(');
2284 let mut names = Vec::with_capacity(d.args.len());
2285 for (i, a) in d.args.iter().enumerate() {
2286 if i > 0 {
2287 out.push_str(", ");
2288 }
2289 out.push_str(&type_name(pool, a));
2290 let nm = format!("p{}", i + 1);
2291 out.push(' ');
2292 out.push_str(&nm);
2293 names.push(nm);
2294 }
2295 out.push_str(") {\n");
2296 out.push_str(&format!(
2297 " {} super({});\n",
2298 " ".repeat(depth),
2299 names.join(", ")
2300 ));
2301 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2302 emitted_any = true;
2303 }
2304 }
2305 }
2306
2307 // Framework-ancestor ctor bridge. The two blocks above mirror POOL
2308 // ancestors; both stop at a framework (off-pool) superclass. A class
2309 // whose DIRECT superclass is a framework type (in android.jar) that
2310 // declares a ctor the class lacks also needs a bridge — `super(args)`
2311 // resolves against the jar. Only the DIRECT-super-is-framework case is
2312 // safe: an intermediate pool ancestor with no matching ctor would
2313 // break the `super()` chain (that ancestor needs its own bridge, which
2314 // its own refs may not trigger). Covers weixin g3 extends
2315 // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
2316 // the custom-exception / Parcelable family the pool walk can't reach.
2317 if !class.is_interface() && enum_consts.is_none() {
2318 let direct_fw = class
2319 .super_name
2320 .as_ref()
2321 .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
2322 .unwrap_or(false);
2323 if direct_fw {
2324 if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2325 fn arg_sig(d: &str) -> &str {
2326 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2327 let hi = d.find(')').unwrap_or(d.len());
2328 &d[lo..hi]
2329 }
2330 let declared: jdc_core::FxHashSet<&str> = class
2331 .all_methods()
2332 .filter(|m| &*m.name == "<init>")
2333 .map(|m| arg_sig(&m.desc))
2334 .collect();
2335 let mut missing: Vec<&String> = refs
2336 .iter()
2337 .filter(|s| !declared.contains(s.as_str()))
2338 .collect();
2339 missing.sort();
2340 for sig in missing {
2341 // Never bridge the empty sig here: a no-ctor class
2342 // gets Java's implicit default, and a class WITH
2343 // ctors is covered by the referenced-but-undeclared
2344 // block's Object-walk tail (emitting in both was
2345 // `已在类 b0中定义了构造器 b0()` ×6 weixin).
2346 if sig.is_empty() {
2347 continue;
2348 }
2349 let args = split_arg_descs(sig);
2350 if emitted_any {
2351 out.push('\n');
2352 }
2353 out.push_str(&format!(" {}", " ".repeat(depth)));
2354 out.push_str("public ");
2355 out.push_str(&sanitize_ref(&simple));
2356 out.push('(');
2357 let mut names = Vec::with_capacity(args.len());
2358 for (i, a) in args.iter().enumerate() {
2359 if i > 0 {
2360 out.push_str(", ");
2361 }
2362 out.push_str(&type_name(pool, a));
2363 let nm = format!("p{}", i + 1);
2364 out.push(' ');
2365 out.push_str(&nm);
2366 names.push(nm);
2367 }
2368 out.push_str(") {\n");
2369 out.push_str(&format!(
2370 " {} super({});\n",
2371 " ".repeat(depth),
2372 names.join(", ")
2373 ));
2374 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2375 emitted_any = true;
2376 }
2377 }
2378 }
2379 }
2380
2381 // Throwable-family ctor bridge for empty-shell exception classes
2382 // (see throwable_ctor_sigs). Walks the pool chain to the nearest
2383 // FRAMEWORK ancestor, stopping at any pool ancestor that DECLARES
2384 // ctors (the mirror block above covers that shape) — so chains of
2385 // empty shells (C extends P extends Error) each get the bridges and
2386 // every super(..) resolves against its parent's bridge.
2387 if !class.is_interface()
2388 && enum_consts.is_none()
2389 && !class.all_methods().any(|m| &*m.name == "<init>")
2390 {
2391 let mut fw_anc: Option<&str> = None;
2392 if let Some(sup) = class.super_name.as_deref() {
2393 let mut cur = sup;
2394 let mut hops = 0;
2395 loop {
2396 hops += 1;
2397 if hops > 32 || cur == "java/lang/Object" {
2398 break;
2399 }
2400 match pool.get(cur) {
2401 Some(sc) => {
2402 if sc.all_methods().any(|m| &*m.name == "<init>") {
2403 break;
2404 }
2405 match sc.super_name.as_deref() {
2406 Some(n) => cur = n,
2407 None => break,
2408 }
2409 }
2410 None => {
2411 fw_anc = Some(cur);
2412 break;
2413 }
2414 }
2415 }
2416 }
2417 if let Some(sigs) = fw_anc.and_then(throwable_ctor_sigs) {
2418 // The framework-ancestor bridge above already emitted every
2419 // sig the dex method-ref table attributes to this class —
2420 // emitting the table copy too was `已在类 z中定义了构造器
2421 // z(String)` (+60 weixin dup-def). The empty sig is exempt
2422 // there, so the table keeps it.
2423 let ref_sigs = pool.ctor_ref_sigs(&class.name);
2424 for sig in &sigs {
2425 if !sig.is_empty()
2426 && ref_sigs.is_some_and(|rs| rs.contains(sig))
2427 {
2428 continue;
2429 }
2430 let args = split_arg_descs(sig);
2431 if emitted_any {
2432 out.push('\n');
2433 }
2434 out.push_str(&format!(" {}", " ".repeat(depth)));
2435 out.push_str("public ");
2436 out.push_str(&sanitize_ref(&simple));
2437 out.push('(');
2438 let mut names = Vec::with_capacity(args.len());
2439 for (i, a) in args.iter().enumerate() {
2440 if i > 0 {
2441 out.push_str(", ");
2442 }
2443 out.push_str(&type_name(pool, a));
2444 let nm = format!("p{}", i + 1);
2445 out.push(' ');
2446 out.push_str(&nm);
2447 names.push(nm);
2448 }
2449 out.push_str(") {\n");
2450 out.push_str(&format!(
2451 " {} super({});\n",
2452 " ".repeat(depth),
2453 names.join(", ")
2454 ));
2455 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2456 emitted_any = true;
2457 }
2458 }
2459 }
2460
2461 // Static resolver helpers extracted from branched-delegation ctors
2462 // (passes::extract_branched_delegation_helper) — the computation the
2463 // Java first-statement rule cannot host inline.
2464 for h in crate::passes::take_ctor_helpers(&class.name) {
2465 if emitted_any {
2466 out.push('\n');
2467 }
2468 out.push_str(&indent(depth + 1));
2469 out.push_str("private static ");
2470 out.push_str(&type_name(pool, &h.ret));
2471 out.push(' ');
2472 out.push_str(&h.name);
2473 out.push('(');
2474 for (i, (ty, nm)) in h.params.iter().enumerate() {
2475 if i > 0 {
2476 out.push_str(", ");
2477 }
2478 out.push_str(&type_name(pool, ty));
2479 out.push(' ');
2480 out.push_str(&java_ident(nm));
2481 }
2482 out.push_str(") {\n");
2483 let p = Printer::new(ctx, &h.vt);
2484 let text = p.with_indent(depth + 2).into_string(&h.body);
2485 for line in text.lines() {
2486 if line.trim().is_empty() {
2487 continue;
2488 }
2489 out.push_str(line);
2490 out.push('\n');
2491 }
2492 out.push_str(&indent(depth + 1));
2493 out.push_str("}\n");
2494 emitted_any = true;
2495 }
2496
2497 // Synthetic relay ctors for FULL-mode branched delegations
2498 // (passes::extract_branched_delegation_helper): the public ctor
2499 // renders `this(resolve$X(params))`; this unpacks the carrier
2500 // array into the one legal first-statement delegation. Primitives
2501 // unbox through their wrappers (the array boxes them on pack).
2502 // ONE relay per class: the (Object[]) erasure is class-unique. Two
2503 // Full-mode ctors in one class (rare; each worker rewrites its own
2504 // body independently) resolve deterministically — sorted by origin
2505 // desc, the least emits and the others keep the honest
2506 // ctor-missing error. No duplicate-declaration risk.
2507 for (ri, (_desc, r)) in crate::passes::take_ctor_relays(&class.name)
2508 .into_iter()
2509 .enumerate()
2510 {
2511 if ri > 0 {
2512 continue;
2513 }
2514 if emitted_any {
2515 out.push('\n');
2516 }
2517 // Same depth rule as emit_class_body's header: a depth-0 class
2518 // declares its FLAT `$` name (the ctor must match it — weixin
2519 // ModularizingPkgRetrieverContract$SharedIPCCallArgs rendered a
2520 // tail-named relay ctor: "方法声明无效" parse-abort); an INLINE
2521 // nested member declares its last segment.
2522 let disp = crate::apply_class_rename(&class.name);
2523 let (_, simple) = split_name(&disp);
2524 let ctor_name = if depth == 0 {
2525 simple.clone()
2526 } else {
2527 let seg = simple.rsplit('$').next().unwrap_or("");
2528 if seg.is_empty() {
2529 simple.clone()
2530 } else {
2531 seg.to_string()
2532 }
2533 };
2534 out.push_str(&indent(depth + 1));
2535 out.push_str("private ");
2536 out.push_str(&java_ident(&ctor_name));
2537 out.push_str("(Object[] h$relay) {\n");
2538 out.push_str(&indent(depth + 2));
2539 out.push_str(if r.is_super { "super(" } else { "this(" });
2540 for (i, t) in r.formals.iter().enumerate() {
2541 if i > 0 {
2542 out.push_str(", ");
2543 }
2544 let slot = format!("h$relay[{}]", i);
2545 match t {
2546 JavaType::Boolean => out.push_str(&format!("((Boolean) {}).booleanValue()", slot)),
2547 JavaType::Byte => out.push_str(&format!("((Byte) {}).byteValue()", slot)),
2548 JavaType::Char => out.push_str(&format!("((Character) {}).charValue()", slot)),
2549 JavaType::Short => out.push_str(&format!("((Short) {}).shortValue()", slot)),
2550 JavaType::Int => out.push_str(&format!("((Integer) {}).intValue()", slot)),
2551 JavaType::Float => out.push_str(&format!("((Float) {}).floatValue()", slot)),
2552 JavaType::Long => out.push_str(&format!("((Long) {}).longValue()", slot)),
2553 JavaType::Double => out.push_str(&format!("((Double) {}).doubleValue()", slot)),
2554 other => {
2555 out.push('(');
2556 out.push_str(&type_name(pool, other));
2557 out.push_str(") ");
2558 out.push_str(&slot);
2559 }
2560 }
2561 }
2562 out.push_str(");\n");
2563 out.push_str(&indent(depth + 1));
2564 out.push_str("}\n");
2565 emitted_any = true;
2566 }
2567
2568 // Missing-abstract-method stubs (R8 tree-shook an interface method a
2569 // concrete class no longer implements; javac rejects the incomplete
2570 // class). Synthesized last so they sit after the real members.
2571 synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
2572
2573 // Fallback enum base: recover the Enum member surface the
2574 // plain-class rendering lacks (name/ordinal/valueOf/compareTo +
2575 // synthetic storage fields fed by the ctor injection).
2576 if class.is_enum() && enum_consts.is_none() {
2577 synth_enum_members(pool, class, out, depth, &mut emitted_any);
2578 synth_enum_ordinal_scan(pool, class, out, depth, &mut emitted_any);
2579 }
2580
2581 // Static initializer. INTERFACES cannot carry a `static { }` block in
2582 // Java — their clinit only assigns constants, which static_values (or
2583 // the `= null` default) already render as field initializers; skip
2584 // the block entirely.
2585 let skip_clinit = class.is_interface();
2586 if let Some((_, clinit_body)) = enum_consts.take() {
2587 // True-enum <clinit>: the constant assignments are already gone;
2588 // render the remainder (the $VALUES array build) directly.
2589 // Skip an empty remainder (all statements were constant inits).
2590 let empty = match &clinit_body.body {
2591 Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
2592 _ => false,
2593 };
2594 if !empty {
2595 if emitted_any {
2596 out.push('\n');
2597 }
2598 out.push_str(&indent(depth + 1));
2599 out.push_str("static {\n");
2600 let p = Printer::new(ctx, &clinit_body.vt);
2601 let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
2602 for line in text.lines() {
2603 if line.trim().is_empty() {
2604 continue;
2605 }
2606 out.push_str(line);
2607 out.push('\n');
2608 }
2609 out.push_str(&indent(depth + 1));
2610 out.push_str("}\n");
2611 emitted_any = true;
2612 }
2613 } else if let Some(clinit) = (!skip_clinit)
2614 .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
2615 .flatten()
2616 {
2617 let mark = out.len();
2618 if emitted_any {
2619 out.push('\n');
2620 }
2621 if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
2622 emitted_any = true;
2623 } else {
2624 out.truncate(mark);
2625 }
2626 }
2627
2628 // Nested member classes (clean `$` tails only; anonymous/local/lambda
2629 // classes are emitted as their own top-level files by the driver).
2630 for nested in nested_members(pool, class, ctx) {
2631 if emitted_any {
2632 out.push('\n');
2633 }
2634 let nested_ctx = DexCtx::new(pool, nested);
2635 out.push('\n');
2636 emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
2637 emitted_any = true;
2638 }
2639
2640 out.push_str(&ind);
2641 out.push_str("}\n");
2642 Ok(())
2643}
2644
2645/// Member classes of `class` (direct children by the `$` chain / member
2646/// annotations), excluding anonymous / local / lambda shapes.
2647fn nested_members<'a>(
2648 pool: &'a DexPool,
2649 class: &'a PoolClass,
2650 ctx: &DexCtx<'_>,
2651) -> Vec<&'a PoolClass> {
2652 let mut out = Vec::new();
2653 for name in pool.children_of(&class.name) {
2654 let Some(pc) = pool.get(name) else { continue };
2655 // Only a REAL `outer$tail` name is an inline member: the
2656 // children index also carries annotation-derived outers
2657 // (EnclosingClass) with no naming relationship to the child
2658 // (obfuscated apps pair a 1-char name with a long enclosing
2659 // descriptor) — the blind slice panicked there (alipay
2660 // `a.a.a.a.c`, exposed once children_of actually returned data).
2661 let Some(rest) = name
2662 .strip_prefix(class.name.as_str())
2663 .and_then(|t| t.strip_prefix('$'))
2664 else {
2665 continue;
2666 };
2667 // The SAME predicate top_level_classes uses for the standalone
2668 // decision: a non-clean member tail ($ExternalSynthetic flat
2669 // units, hyphen markers, digit-led anonymous/local shapes) is
2670 // emitted as its own file — inlining it here too DOUBLE-EMITS
2671 // the class, and the nested copy renders corrupt (weibo:
2672 // 12,152 nested `ExternalSyntheticLambdaN` chimeras across
2673 // 4,316 files; Recorder's nested copy mixed a sibling's
2674 // captures — `int f$0` + Builder.setSource — 无法取消引用int).
2675 if !crate::clean_member_tail(rest) {
2676 continue;
2677 }
2678 if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
2679 continue;
2680 }
2681 out.push(pc);
2682 }
2683 out
2684}
2685
2686// Eight parameters are all load-bearing (pool, field, static value,
2687// owner class, output, depth, staticness, interface-init requirement);
2688// bundling them into a struct would obscure each call site.
2689#[allow(clippy::too_many_arguments)]
2690fn emit_field(
2691 pool: &DexPool,
2692 f: &crate::PoolField,
2693 init: Option<&StaticValue>,
2694 f_class: &str,
2695 out: &mut String,
2696 depth: usize,
2697 is_static: bool,
2698 require_init: bool,
2699) {
2700 let ind = indent(depth);
2701 let mut line = String::new();
2702 let a = f.access;
2703 if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
2704 line.push_str("public ");
2705 } else if a & ACC_PRIVATE != 0 {
2706 line.push_str("private ");
2707 } else if a & ACC_PROTECTED != 0 {
2708 line.push_str("protected ");
2709 }
2710 if is_static {
2711 line.push_str("static ");
2712 }
2713 if a & ACC_FINAL != 0 && !unfinal_field(f_class, &f.name) {
2714 line.push_str("final ");
2715 }
2716 if a & ACC_SYNTHETIC != 0 {
2717 line.push_str("/* synthetic */ ");
2718 }
2719 if a & ACC_TRANSIENT_HINT != 0 {
2720 line.push_str("transient ");
2721 }
2722 if a & ACC_VOLATILE_HINT != 0 {
2723 line.push_str("volatile ");
2724 }
2725 let ty = type_name(pool, &desc_type(&f.desc));
2726 line.push_str(&ty);
2727 line.push(' ');
2728 let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
2729 line.push_str(&java_ident(fname));
2730 let mut rendered = None;
2731 if let Some(v) = init {
2732 rendered = render_static_value(pool, v, f_class);
2733 }
2734 if rendered.is_none() && require_init {
2735 // Interface fields MUST have an initializer in Java; the dex may
2736 // not carry a static_values entry for a compile-time-constant the
2737 // compiler folded away. Keep it compilable.
2738 let default = match desc_type(&f.desc) {
2739 JavaType::Boolean => "false",
2740 JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
2741 JavaType::Long => "0L",
2742 JavaType::Float => "0.0F",
2743 JavaType::Double => "0.0",
2744 _ => "null",
2745 };
2746 rendered = Some(default.to_string());
2747 }
2748 if let Some(text) = rendered {
2749 // A long field holding an `Int(i64)` static value needs the `L`
2750 // suffix: without it the literal is an int and overflows
2751 // (`long d = -6343169151696340687` failed javac).
2752 let text = if matches!(desc_type(&f.desc), JavaType::Long)
2753 && text.chars().all(|c| c.is_ascii_digit() || c == '-')
2754 {
2755 format!("{text}L")
2756 } else {
2757 text
2758 };
2759 line.push_str(" = ");
2760 line.push_str(&text);
2761 }
2762 line.push(';');
2763 out.push_str(&ind);
2764 out.push_str(&line);
2765 out.push('\n');
2766}
2767
2768// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
2769// volatile(0x40)/transient(0x80) for fields.
2770pub const ACC_VOLATILE_HINT: u32 = 0x40;
2771pub const ACC_TRANSIENT_HINT: u32 = 0x80;
2772
2773fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
2774 Some(match v {
2775 StaticValue::Int(i) => i.to_string(),
2776 StaticValue::Float(f) => format_float(*f as f64, true),
2777 StaticValue::Double(d) => format_float(*d, false),
2778 StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
2779 StaticValue::Type(t) => format!("{}.class", dotted(t)),
2780 StaticValue::Boolean(b) => b.to_string(),
2781 StaticValue::Null => "null".into(),
2782 StaticValue::Field(cls, name, desc) => {
2783 // The rename registry owns the display name (obscuring-field
2784 // renames key on (owner, name, desc)) — a const field ref must
2785 // render the SAME name as the declaration.
2786 let n: std::borrow::Cow<str> = match jdc_core::rename::field_display(cls, name, desc) {
2787 Some(d) => std::borrow::Cow::Borrowed(d),
2788 None => java_ident(name),
2789 };
2790 if cls == owner {
2791 n.into_owned()
2792 } else {
2793 format!("{}.{}", print_class_name(pool, cls), n)
2794 }
2795 }
2796 StaticValue::Other => return None,
2797 })
2798}
2799
2800/// Render one method straight into the class buffer `out`. Returns
2801/// whether anything was written (skips: no descriptor, deferred to a
2802/// monitored thread). The old shape returned a per-method String that
2803/// the caller copied in — one extra full copy of every method body.
2804/// `enum_promoted`: the class rendered as a true `enum` declaration
2805/// (constants in the header), which changes what a ctor signature may
2806/// declare.
2807/// d8's covariant-return bridges: `Context getContext() { return
2808/// super.getContext(); }` shadowing MMActivity's covariant
2809/// `AppCompatActivity getContext()`. Source cannot declare the
2810/// wider-return override, and the rendered bridge HIJACKS every
2811/// `this.getContext()` in the file to the wide type (weixin
2812/// FTSBaseVoiceSearchUI's `AppCompatActivity context =
2813/// this.getContext();` — Context无法转换为AppCompatActivity ×74). The
2814/// inherited covariant method satisfies source callers natively and
2815/// javac regenerates binary bridges on demand — skip the render.
2816///
2817/// TWO gates, both learned the hard way (the ungated ACC_BRIDGE +
2818/// ancestor-name version skipped erasure/interface-satisfier bridges
2819/// and cost lark +29k — the claim-map's raw-interface lesson at whole-
2820/// hierarchy scale):
2821/// 1. INSTRUCTION SHAPE: the body must be exactly invoke-super to a
2822/// same-named method + optional move-result + return (≤3 insns).
2823/// Erasure bridges call THIS with casts; interface satisfiers are
2824/// real bodies — both keep rendering.
2825/// 2. POOL ancestor (super chain, exact name+args, non-private)
2826/// declares the covariant source. Framework ancestors are not
2827/// consulted (fwdb carries no arg descs — a name-only match could
2828/// skip a real overload); such bridges keep rendering.
2829fn bridge_shadowed_by_ancestor(pool: &DexPool, class: &PoolClass, m: &PoolMethod) -> bool {
2830 if m.access & crate::access::ACC_BRIDGE == 0 || &*m.name == "<init>" || m.code_off == 0 {
2831 return false;
2832 }
2833 let Some(d) = m.parsed_desc() else {
2834 return false;
2835 };
2836 // Gate 1: invoke-super-same-name + move-result*/return* tail only.
2837 let Some(dex) = pool.dex(m.dex_idx) else {
2838 return false;
2839 };
2840 let dex = &*dex;
2841 let Some(code) = dex.code_insns_bytes_at(m.code_off) else {
2842 return false;
2843 };
2844 let mut head_ok = false;
2845 let mut bad_tail = false;
2846 let mut cnt = 0usize;
2847 let mut first = true;
2848 ddc_dex::insn::scan_instructions(code, &mut |op, pc, bytes| {
2849 cnt += 1;
2850 if first {
2851 first = false;
2852 if op == 0x6f || op == 0x75 {
2853 let unit = bytes
2854 .get(2 * (pc + 1)..2 * (pc + 1) + 2)
2855 .map(|b| u16::from_le_bytes([b[0], b[1]]) as u32)
2856 .unwrap_or(u32::MAX);
2857 if unit != u32::MAX {
2858 let mr = dex.method(unit);
2859 head_ok = dex.string(mr.name_idx) == &*m.name;
2860 }
2861 }
2862 } else if !matches!(op, 0x0a | 0x0b | 0x0c | 0x0e | 0x0f | 0x10 | 0x11) {
2863 bad_tail = true;
2864 }
2865 });
2866 if !head_ok || bad_tail || !(2..=3).contains(&cnt) {
2867 return false;
2868 }
2869 // Gate 2: pool super chain declares name+args.
2870 let mut cur: Option<String> = class.super_name.clone();
2871 let mut hops = 0u32;
2872 while let Some(s) = cur {
2873 hops += 1;
2874 if hops > 64 {
2875 return false;
2876 }
2877 let Some(pc) = pool.get_if_materialized(&s) else {
2878 return false; // unresolvable ancestor — keep the bridge
2879 };
2880 if pc.all_methods().any(|am| {
2881 am.name == m.name
2882 && am.access & crate::access::ACC_PRIVATE == 0
2883 && am.parsed_desc().is_some_and(|ad| ad.args == d.args)
2884 }) {
2885 return true;
2886 }
2887 cur = pc.super_name.clone();
2888 }
2889 false
2890}
2891
2892fn emit_method(
2893 pool: &DexPool,
2894 class: &PoolClass,
2895 ctx: &DexCtx<'_>,
2896 m: &PoolMethod,
2897 depth: usize,
2898 enum_promoted: bool,
2899 out: &mut String,
2900) -> anyhow::Result<bool> {
2901 let ind = indent(depth);
2902 let desc = m.parsed_desc();
2903
2904 // Signature.
2905 let mut sig = String::with_capacity(192);
2906 let a = m.access;
2907 if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
2908 sig.push_str("public ");
2909 } else if a & ACC_PRIVATE != 0 {
2910 // A fallback enum BASE (`/* enum */ class`, not a promoted
2911 // `enum`) renders its constant-specific-body subclasses as
2912 // SEPARATE same-package classes (`j$6 extends j`); their ctors
2913 // call `super(name, ordinal, ..)`, which a PRIVATE base ctor
2914 // forbids ("j(String,int) has private access"). Widen the base
2915 // ctor to package-private so the subclass links. The base is
2916 // abstract → no external instantiation risk. Only the BASE
2917 // (super is Enum/Object), not the constant subclass itself.
2918 let fallback_enum_base_ctor = class.is_enum()
2919 && !enum_promoted
2920 && &*m.name == "<init>"
2921 && class
2922 .super_name
2923 .as_ref()
2924 .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
2925 .unwrap_or(true);
2926 if !fallback_enum_base_ctor {
2927 sig.push_str("private ");
2928 }
2929 } else if a & ACC_PROTECTED != 0 {
2930 sig.push_str("protected ");
2931 }
2932 let is_clinit = &*m.name == "<clinit>";
2933 let is_init = &*m.name == "<init>";
2934 if a & ACC_STATIC != 0 || is_clinit {
2935 sig.push_str("static ");
2936 }
2937 if a & ACC_FINAL != 0 {
2938 sig.push_str("final ");
2939 }
2940 // `abstract synchronized` is an illegal combination — obfuscated
2941 // builds mark abstract bridges synchronized (WhatsApp
2942 // SQLiteOpenHelper).
2943 if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
2944 sig.push_str("synchronized ");
2945 }
2946 if a & ACC_NATIVE != 0 {
2947 sig.push_str("native ");
2948 }
2949 if a & ACC_ABSTRACT != 0 {
2950 sig.push_str("abstract ");
2951 }
2952 if a & ACC_SYNTHETIC != 0 {
2953 sig.push_str("/* synthetic */ ");
2954 }
2955
2956 // Body (needed for parameter names even for abstract methods).
2957 let mut body = decompile_method(pool, class, m).ok().flatten();
2958 if &*m.name == "<clinit>" {
2959 if let Some(b) = &mut body {
2960 crate::passes::strip_clinit_hotfix_guard(&mut b.body);
2961 }
2962 }
2963 // An interface method WITH a body is a `default` method (JLS 9.4.3)
2964 // unless static/private — dex carries no `default` flag, so the
2965 // plain form rendered an abstract signature with a body and javac
2966 // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
2967 // default REQUIRES a body: only push it once the body is confirmed
2968 // (a failed decompile renders a body-less declaration).
2969 if class.is_interface()
2970 && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
2971 && body.is_some()
2972 {
2973 sig.insert_str(0, "default ");
2974 }
2975 let param_names: Vec<String> = body
2976 .as_ref()
2977 .map(|b| {
2978 let mut ps: Vec<(u16, String)> =
2979 b.vt.vars
2980 .iter()
2981 .filter(|v| v.is_param && v.name != "this")
2982 .map(|v| (v.slot, v.name.clone()))
2983 .collect();
2984 ps.sort_by_key(|(s, _)| *s);
2985 ps.into_iter().map(|(_, n)| n).collect()
2986 })
2987 .unwrap_or_else(|| {
2988 (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
2989 .map(|i| format!("p{}", i + 1))
2990 .collect()
2991 });
2992
2993 // Non-static member-inner ctor: the synthetic outer instance rides
2994 // as args[0] (typed as the direct enclosing class). Every emission
2995 // site already passes it implicitly — qualified `outer.new Inner(..)`,
2996 // `this.new Inner(..)` from inside the outer, `super(..)` after
2997 // skip_outer_arg — so the signature drops the param and the body's
2998 // references become `Outer.this`. Without this, every construction
2999 // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
3000 // guava androidx inner-class families, d8 capture lambdas).
3001 let mut inner_arg0 = 0usize;
3002 if is_init {
3003 let tail = class.name.rsplit('$').next().unwrap_or("");
3004 let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
3005 if let Some(d) = desc.as_ref() {
3006 if let Some(JavaType::Object(outer)) = d.args.first() {
3007 // The direct enclosing class: nesting annotation when
3008 // present, else the `$`-chain parent (find_outer_name —
3009 // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
3010 // a plain rsplit leaves a trailing `$`). It must agree
3011 // with the this$0 type for the rewrite to fire.
3012 let enclosing: Option<String> = class
3013 .nesting
3014 .enclosing_class
3015 .clone()
3016 .or_else(|| crate::find_outer_name(pool, &class.name));
3017 let direct = enclosing.as_deref() == Some(outer.as_ref());
3018 if direct && !digit_simple && ctx.class_has_this0(&class.name) {
3019 let param0 = body.as_ref().and_then(|b| {
3020 b.vt.vars
3021 .iter()
3022 .find(|v| v.is_param && v.name != "this")
3023 .map(|v| v.id)
3024 });
3025 let written = match (&body, param0) {
3026 (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
3027 _ => false,
3028 };
3029 if let (Some(p0), false) = (param0, written) {
3030 if let Some(b) = body.as_mut() {
3031 // this()-delegations to the SAME class drop the
3032 // outer arg (all of its ctors share the strip);
3033 // a super target joins when it is an inner of
3034 // the same enclosing family.
3035 let mut eligible: Vec<String> = vec![class.name.clone()];
3036 if let Some(sup) = &class.super_name {
3037 let sup_enclosing =
3038 crate::find_outer_name(pool, sup);
3039 if sup_enclosing.as_deref() == Some(outer.as_ref())
3040 && ctx.class_has_this0(sup)
3041 {
3042 eligible.push(sup.clone());
3043 }
3044 }
3045 let renamed = crate::apply_class_rename(outer);
3046 let display = dotted(renamed.as_ref());
3047 let ty =
3048 jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
3049 crate::passes::rewrite_inner_ctor_outer_param(
3050 &mut b.body,
3051 p0,
3052 &display,
3053 &ty,
3054 &eligible,
3055 );
3056 inner_arg0 = 1;
3057 }
3058 }
3059 }
3060 }
3061 }
3062 }
3063
3064 if is_clinit {
3065 // `static { ... }` — the caller strips the method name/params.
3066 } else {
3067 let Some(d) = &desc else { return Ok(false) };
3068 if is_init {
3069 // The ctor name must equal the DECLARED class name of its
3070 // file: flat `$` at depth 0 (own file), own segment when
3071 // inlined in the parent at depth > 0. Case-renamed classes
3072 // use their display name.
3073 let dname = crate::apply_class_rename(&class.name);
3074 let (_, simple) = split_name(&dname);
3075 // emit_method's depth is the METHOD indent = class depth + 1:
3076 // own-file classes (depth 0 header → method depth 1) need the
3077 // flat `$` ctor name; inline nested members use their segment.
3078 let base = if depth <= 1 {
3079 simple.to_string()
3080 } else {
3081 // R8 names can END in `$` (`ThreadMsg$$$`): the last
3082 // `$`-segment is empty — keep the whole simple name.
3083 let seg = simple.rsplit('$').next().unwrap_or(&simple);
3084 if seg.is_empty() {
3085 simple.to_string()
3086 } else {
3087 seg.to_string()
3088 }
3089 };
3090 let name = if is_restricted_type_name(&base) {
3091 format!("_{base}")
3092 } else {
3093 base
3094 };
3095 // The ctor name must equal the DECLARED class name — the same
3096 // injective `_u<hex>` sanitizer the header uses (java_ident's
3097 // lossy `-`→`_` fold left `class _u2dDeprecatedOkio` with a
3098 // `private _DeprecatedOkio()` ctor: "方法声明无效; 需要返回
3099 // 类型", which as a PARSE error also masks every semantic
3100 // error in the whole javac run).
3101 sig.push_str(&sanitize_ref(&name));
3102 } else {
3103 sig.push_str(&type_name(pool, &d.ret));
3104 sig.push(' ');
3105 let mname =
3106 jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
3107 sig.push_str(&java_ident(mname));
3108 }
3109 sig.push('(');
3110 // A promoted enum ctor's dex descriptor carries the compiler-
3111 // synthesized `(String name, int ordinal)` prefix — JLS forbids
3112 // declaring those (they are implicit in the `A(args)` constant
3113 // declarations the promotion emits, and strip_enum_ctor_super
3114 // already removed the `super(name, ordinal, ..)` delegation).
3115 // Skip the pair in the signature, or every constant declaration
3116 // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
3117 // when the body never reads the two params — code that genuinely
3118 // uses them keeps the declared form.
3119 // Robust hotpatch guard strip MUST run before the trace-param
3120 // analysis: the guard's isSupport(..) call is the ctor's only
3121 // reader of the (String, int) params for plain robust enums, and
3122 // its self-static read is what JLS 8.9.2 forbids.
3123 if enum_promoted && is_init {
3124 if let Some(b) = body.as_mut() {
3125 strip_robust_enum_ctor_guard(&mut b.body);
3126 }
3127 }
3128 // Fallback enum base ctor: store the (String, int) trace params
3129 // into the synthetic $ddcName/$ddcOrdinal fields that back the
3130 // synthesized name()/ordinal()/valueOf() (synth_enum_members).
3131 // Non-final fields: constant-subclass ctors reach this store
3132 // through their super(..) chain, and a `final` would demand
3133 // definite assignment on every ctor path.
3134 if is_init
3135 && !enum_promoted
3136 && class.is_enum()
3137 && enum_synth_active(class)
3138 && matches!(d.args.first(), Some(JavaType::Object(sx)) if sx.as_ref() == "java/lang/String")
3139 && matches!(d.args.get(1), Some(JavaType::Int))
3140 {
3141 if let Some(b) = body.as_mut() {
3142 // A ctor that DELEGATES first (the -IA synthetic bridge,
3143 // `this(str, p2)`) must stay delegation-first — the
3144 // delegated-to ctor performs the stores.
3145 // First-LEAF probe: the ctor chain leaves the delegation
3146 // Block-wrapped (`Block[this(..), assigns]`), and a
3147 // stripped hotfix guard can leave the wrapper as the
3148 // first top-level statement — unwrap to the real head.
3149 let starts_with_delegation =
3150 crate::passes::is_bare_ctor_call(crate::passes::first_leaf_stmt(&b.body));
3151 let ps: Vec<u32> = b
3152 .vt
3153 .vars
3154 .iter()
3155 .filter(|v| v.is_param && v.name != "this")
3156 .take(2)
3157 .map(|v| v.id)
3158 .collect();
3159 if ps.len() == 2 && !starts_with_delegation {
3160 let mk_store = |var: u32, fname: &str, fty: JavaType| {
3161 Stmt::ExprStmt(Expr::Assign {
3162 target: Box::new(Expr::Field {
3163 owner: Some(Box::new(Expr::This)),
3164 cls: std::sync::Arc::from(class.name.as_str()),
3165 name: std::sync::Arc::from(fname),
3166 ty: jdc_core::ir::expr::TypeRef::J(fty.clone()),
3167 is_static: false,
3168 }),
3169 op: jdc_core::ir::expr::AssignOp::Plain,
3170 value: Box::new(Expr::Local {
3171 var,
3172 ty: jdc_core::ir::expr::TypeRef::J(fty),
3173 }),
3174 })
3175 };
3176 let mut inj = vec![
3177 mk_store(ps[0], ENUM_NAME_FIELD, JavaType::Object("java/lang/String".into())),
3178 mk_store(ps[1], ENUM_ORD_FIELD, JavaType::Int),
3179 ];
3180 if let Stmt::Block(vs) = &mut b.body {
3181 let mut rest = std::mem::take(vs);
3182 inj.append(&mut rest);
3183 *vs = inj;
3184 } else {
3185 let prev = std::mem::replace(&mut b.body, Stmt::Block(Vec::new()));
3186 inj.push(prev);
3187 b.body = Stmt::Block(inj);
3188 }
3189 }
3190 }
3191 }
3192 let mut arg0 = 0;
3193 if enum_promoted
3194 && is_init
3195 && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
3196 && matches!(d.args.get(1), Some(JavaType::Int))
3197 {
3198 // The Kotlin default-arg bridge ctor reads name/ordinal in
3199 // its `this(str, p2, ..)` delegation only — the LEADING pair
3200 // of a this()-delegation drops together with the params, so
3201 // those reads do not block the strip (the constants' extra
3202 // args match the bridge's user params, not the 2-param user
3203 // ctor).
3204 let synthetic: Vec<u32> = body
3205 .as_ref()
3206 .map(|b| {
3207 b.vt
3208 .vars
3209 .iter()
3210 .filter(|v| v.is_param && v.name != "this")
3211 .take(2)
3212 .map(|v| v.id)
3213 .collect()
3214 })
3215 .unwrap_or_default();
3216 // The delegation-leading extension only applies to the
3217 // Kotlin default-arg BRIDGE: its descriptor ends with
3218 // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
3219 // ctor whose first two params are (String, int) and merely
3220 // forwards them must keep its signature (weixin +2.9k when
3221 // ungated).
3222 // Marker-name-free bridge shape: (.., int mask, marker).
3223 // R8 renames DefaultConstructorMarker with the rest of the
3224 // stdlib (weixin: kotlin/jvm/internal/i, an empty abstract
3225 // class), so gate on the package + the int-mask/refs pair
3226 // instead of the literal name — a real user enum ctor with
3227 // (int, kotlin/jvm/internal/*) tail params does not occur.
3228 let n_args = d.args.len();
3229 let is_bridge = n_args >= 2
3230 && matches!(&d.args[n_args - 1], JavaType::Object(ref m)
3231 if m.starts_with("kotlin/jvm/internal/"))
3232 && matches!(d.args[n_args - 2], JavaType::Int);
3233 // Meituan Robust instrumented ctors PACK the trace params
3234 // into dispatch arrays (`v3[0] = str; v3[1] = new
3235 // Integer(p2); PatchProxy.isSupport(v3, ..)`) — value reads
3236 // that refuse the strip, and a true enum cannot DECLARE
3237 // (String, int) params (JLS 8.9.1), so the whole class fell
3238 // back (illegal `Enum.valueOf` + no ordinal()). Rewrite pure
3239 // VALUE reads to the equivalent `name()`/`ordinal()` calls —
3240 // Enum's fields are set by the implicit super before the
3241 // body runs, so the pack sees identical values. Delegation
3242 // LEAD reads (this(str, p2) of Kotlin/-IA bridges) must keep
3243 // their Local shape for the lead-drop below, so skip any
3244 // ctor that has them; written params are never replaceable.
3245 if synthetic.len() == 2 && !is_bridge {
3246 if let Some(b) = body.as_ref() {
3247 let (p0, p1) = (synthetic[0], synthetic[1]);
3248 let mut lead_reads = 0usize;
3249 {
3250 let cls_name = class.name.as_str();
3251 crate::passes::visit_stmt_exprs_ro(&b.body, &mut |e| {
3252 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3253 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3254 for a in args.iter().take(2) {
3255 if let Expr::Local { var: v, .. } = a {
3256 if *v == p0 || *v == p1 {
3257 lead_reads += 1;
3258 }
3259 }
3260 }
3261 }
3262 }
3263 });
3264 }
3265 let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3266 let reads = uses.get(&p0).copied().unwrap_or(0)
3267 + uses.get(&p1).copied().unwrap_or(0);
3268 let written = crate::passes::local_is_written(&b.body, p0)
3269 || crate::passes::local_is_written(&b.body, p1);
3270 if lead_reads == 0 && reads > 0 && !written {
3271 let mk = |nm: &str, ret: JavaType| Expr::Method {
3272 owner: Some(Box::new(Expr::This)),
3273 cls: std::sync::Arc::from("java/lang/Enum"),
3274 name: std::sync::Arc::from(nm),
3275 desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
3276 ret,
3277 args: Vec::new(),
3278 }),
3279 args: Vec::new(),
3280 is_static: false,
3281 is_interface: false,
3282 is_special: false,
3283 is_super: false,
3284 is_dynamic: false,
3285 type_args: Vec::new(),
3286 };
3287 let name_call =
3288 mk("name", JavaType::Object("java/lang/String".into()));
3289 let ord_call = mk("ordinal", JavaType::Int);
3290 if let Some(b) = body.as_mut() {
3291 crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3292 crate::passes::deep_rewrite(e, &mut |x| {
3293 if let Expr::Local { var, .. } = x {
3294 if *var == p0 {
3295 *x = name_call.clone();
3296 } else if *var == p1 {
3297 *x = ord_call.clone();
3298 }
3299 }
3300 });
3301 });
3302 }
3303 }
3304 }
3305 }
3306 let mut lead = [0usize, 0usize];
3307 let refs_ok = body.as_ref().is_some_and(|b| {
3308 let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3309 let cls_name = class.name.as_str();
3310 crate::passes::visit_stmt_exprs_ro(&b.body, &mut |e| {
3311 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3312 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3313 for (k, a) in args.iter().enumerate() {
3314 if let Expr::Local { var: v, .. } = a {
3315 if let Some(pi) = synthetic.iter().position(|p| p == v) {
3316 if k == pi && k < 2 {
3317 lead[pi] += 1;
3318 }
3319 }
3320 }
3321 }
3322 }
3323 }
3324 });
3325 synthetic.iter().enumerate().all(|(pi, id)| {
3326 let total = uses.get(id).copied().unwrap_or(0);
3327 if is_bridge {
3328 total == lead[pi]
3329 } else {
3330 total == 0
3331 }
3332 })
3333 });
3334 if refs_ok {
3335 arg0 = 2;
3336 // Drop the leading (name, ordinal) args of the this()
3337 // delegations.
3338 if let (Some(b), true) = (body.as_mut(), is_bridge) {
3339 let cls_name = class.name.as_str();
3340 let syn = synthetic;
3341 crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3342 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3343 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3344 let mut drop_n = 0;
3345 for a in args.iter().take(2) {
3346 if let Expr::Local { var: v, .. } = a {
3347 if syn.contains(v) && drop_n == v - syn[0] {
3348 drop_n += 1;
3349 continue;
3350 }
3351 }
3352 break;
3353 }
3354 for _ in 0..drop_n {
3355 args.remove(0);
3356 }
3357 }
3358 }
3359 });
3360 }
3361 }
3362 }
3363 let n = d.args.len();
3364 let varargs = a & ACC_VARARGS != 0 && n > 0;
3365 let arg0 = arg0 + inner_arg0;
3366 for (i, arg) in d.args.iter().enumerate().skip(arg0) {
3367 if i > arg0 {
3368 sig.push_str(", ");
3369 }
3370 let name = param_names
3371 .get(i)
3372 .cloned()
3373 .unwrap_or_else(|| format!("p{}", i));
3374 if varargs && i + 1 == n {
3375 if let JavaType::Array(inner) = arg {
3376 sig.push_str(&type_name(pool, inner));
3377 sig.push_str("...");
3378 } else {
3379 sig.push_str(&type_name(pool, arg));
3380 }
3381 } else {
3382 sig.push_str(&type_name(pool, arg));
3383 }
3384 sig.push(' ');
3385 // Param names come from dex debug info — obfuscated apps
3386 // name them `_` (reserved since Java 9) or after keywords.
3387 sig.push_str(&java_ident(&name));
3388 }
3389 sig.push(')');
3390 }
3391
3392 if is_clinit {
3393 let Some(b) = body else { return Ok(false) };
3394 // Direct render: the printer starts at the method's ABSOLUTE
3395 // indent and appends into the same buffer that carries the
3396 // header — no intermediate body string, no per-line re-indent
3397 // pass (two full copies of every method body saved).
3398 out.push_str(&ind);
3399 out.push_str("static {\n");
3400 let hdr = out.len();
3401 let printer = Printer::new(ctx, &b.vt)
3402 .with_indent(depth + 1)
3403 .with_output(std::mem::take(out));
3404 let t_print = std::time::Instant::now();
3405 let mut rendered = printer.into_string(&b.body);
3406 crate::method::phase_hit(3, t_print);
3407 if rendered[hdr..].trim().is_empty() {
3408 rendered.truncate(hdr);
3409 }
3410 rendered.push_str(&ind);
3411 rendered.push_str("}\n");
3412 *out = rendered;
3413 return Ok(true);
3414 }
3415 if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
3416 out.push_str(&ind);
3417 out.push_str(&sig);
3418 out.push_str(";\n");
3419 return Ok(true);
3420 }
3421 let Some(b) = body else { return Ok(false) };
3422
3423 // Direct render at the absolute indent level (see the clinit path).
3424 let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
3425 match &b.desc.ret {
3426 JavaType::Boolean => {
3427 printer = printer.with_ret_bool(true);
3428 }
3429 JavaType::Char => {
3430 printer = printer.with_ret_char(true);
3431 }
3432 JavaType::Byte => {
3433 printer = printer.with_ret_narrow(true, false);
3434 }
3435 JavaType::Short => {
3436 printer = printer.with_ret_narrow(false, true);
3437 }
3438 _ => {}
3439 }
3440 out.push_str(&ind);
3441 out.push_str(&sig);
3442 out.push_str(" {\n");
3443 let hdr = out.len();
3444 let printer = printer.with_output(std::mem::take(out));
3445 let t_print = std::time::Instant::now();
3446 let mut rendered = printer.into_string(&b.body);
3447 crate::method::phase_hit(3, t_print);
3448 if rendered[hdr..].trim().is_empty() {
3449 rendered.truncate(hdr);
3450 }
3451 rendered.push_str(&ind);
3452 rendered.push_str("}\n");
3453 *out = rendered;
3454 Ok(true)
3455}
3456
3457// ---------------------------------------------------------------------------
3458// Naming helpers
3459// ---------------------------------------------------------------------------
3460
3461fn indent(depth: usize) -> String {
3462 " ".repeat(depth)
3463}
3464
3465/// `$`-separated nesting rendered with dots — but ONLY when every
3466/// segment is a clean Java identifier (a genuine member class chain).
3467/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
3468/// `...$$inlined$collect$1`) and local-class tails are NOT member
3469/// classes Java can name; the whole name stays flat with `$` (ddc emits
3470/// them as their own top-level files).
3471/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
3472/// any other non-identifier character) is not legal Java. Deterministic
3473/// mapping, applied identically at declaration and call sites.
3474pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
3475 // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
3476 // (millions per APK), where it profiled as __NSGetEnviron.
3477 static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3478 if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
3479 eprintln!("[ident] {name:?}");
3480 }
3481 let clean = name
3482 .chars()
3483 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
3484 // Kotlin names fields/methods `default` (Companion.default) — no Java
3485 // program can declare or reference a keyword; the identical mapping
3486 // lives in jdc-core's call-site sanitizer.
3487 let keyword = matches!(
3488 name,
3489 "abstract"
3490 | "assert"
3491 | "boolean"
3492 | "break"
3493 | "byte"
3494 | "case"
3495 | "catch"
3496 | "char"
3497 | "class"
3498 | "const"
3499 | "continue"
3500 | "default"
3501 | "do"
3502 | "double"
3503 | "else"
3504 | "enum"
3505 | "extends"
3506 | "final"
3507 | "finally"
3508 | "float"
3509 | "for"
3510 | "goto"
3511 | "if"
3512 | "implements"
3513 | "import"
3514 | "instanceof"
3515 | "int"
3516 | "interface"
3517 | "long"
3518 | "native"
3519 | "new"
3520 | "package"
3521 | "private"
3522 | "protected"
3523 | "public"
3524 | "return"
3525 | "short"
3526 | "static"
3527 | "strictfp"
3528 | "super"
3529 | "switch"
3530 | "synchronized"
3531 | "this"
3532 | "throw"
3533 | "throws"
3534 | "transient"
3535 | "try"
3536 | "void"
3537 | "volatile"
3538 | "while"
3539 | "true"
3540 | "false"
3541 | "null"
3542 // `_` is a reserved identifier since Java 9 (Alipay's
3543 // instant-run fields are named `_`) — the `_<name>` mapping
3544 // turns it into `__`, matching jdc-core's call sites.
3545 | "_"
3546 );
3547 // A simple name may not START with a digit either (WhatsApp nests
3548 // `X/0Xx`): the declaration site and every reference (jdc-core's
3549 // sanitize_source_name) prefix the same underscore.
3550 let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
3551 if clean && !keyword && !digit_start {
3552 std::borrow::Cow::Borrowed(name)
3553 } else if keyword || digit_start {
3554 std::borrow::Cow::Owned(format!("_{name}"))
3555 } else {
3556 // Non-ASCII single chars (Alipay names a field `支`) map to a
3557 // lone `_` — itself reserved since Java 9. Escape it.
3558 let mapped: String = name
3559 .chars()
3560 .map(|c| {
3561 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
3562 c
3563 } else {
3564 '_'
3565 }
3566 })
3567 .collect();
3568 if mapped == "_" {
3569 std::borrow::Cow::Owned("__".to_string())
3570 } else {
3571 std::borrow::Cow::Owned(mapped)
3572 }
3573 }
3574}
3575
3576fn split_name(internal: &str) -> (String, String) {
3577 match internal.rfind('/') {
3578 Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
3579 None => (String::new(), internal.to_string()),
3580 }
3581}
3582
3583/// Dotted source form of an internal name.
3584pub fn dotted(internal: &str) -> String {
3585 let mut out = internal.replace('/', ".");
3586 // `$` → `.` only when the following segment can start a Java
3587 // identifier (anonymous/synthetic tails stay `$`).
3588 let mut i = 0;
3589 while let Some(p) = out[i..].find('$') {
3590 let at = i + p;
3591 // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
3592 // source name — dotting it produced a leading `.Gson.Types`.
3593 let head_ok = at > 0
3594 && out[..at]
3595 .chars()
3596 .next_back()
3597 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
3598 let tail_ok = out[at + 1..]
3599 .chars()
3600 .next()
3601 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
3602 if tail_ok && head_ok {
3603 out.replace_range(at..at + 1, ".");
3604 i = at + 1;
3605 } else {
3606 i = at + 1;
3607 }
3608 }
3609 // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
3610 // reached `extends` with the digit start unmapped.
3611 sanitize_fq(&out)
3612}
3613
3614fn join_dotted(pool: &DexPool, names: &[String]) -> String {
3615 names
3616 .iter()
3617 .map(|n| print_class_name(pool, n))
3618 .collect::<Vec<_>>()
3619 .join(", ")
3620}
3621
3622/// A printable type name (arrays render with `[]` suffixes). Nested class
3623/// names dot their `$` when the outer chain is known to the pool.
3624/// Split a concatenated method-descriptor ARGUMENT string
3625/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
3626/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
3627/// referenced sig comes from the method-id table (no pool ancestor
3628/// method to read a parsed descriptor from).
3629pub(crate) fn split_arg_descs(sig: &str) -> Vec<JavaType> {
3630 let mut out = Vec::new();
3631 let b = sig.as_bytes();
3632 let mut i = 0usize;
3633 while i < b.len() {
3634 let start = i;
3635 while i < b.len() && b[i] == b'[' {
3636 i += 1;
3637 }
3638 if i < b.len() {
3639 if b[i] == b'L' {
3640 while i < b.len() && b[i] != b';' {
3641 i += 1;
3642 }
3643 if i < b.len() {
3644 i += 1; // consume ';'
3645 }
3646 } else {
3647 i += 1; // primitive single char
3648 }
3649 }
3650 if i > start {
3651 out.push(desc_type(&sig[start..i]));
3652 } else {
3653 break; // malformed: avoid an infinite loop
3654 }
3655 }
3656 out
3657}
3658
3659pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
3660 match t {
3661 JavaType::Void => "void".into(),
3662 JavaType::Object(n) => print_class_name(pool, n),
3663 JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
3664 other => other.to_java(false),
3665 }
3666}
3667
3668/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
3669/// its left side names a known class (literal `$` top-level names survive).
3670// ---------------------------------------------------------------------------
3671// The per-class import map (JLS 6.4.2 obscuring repair).
3672//
3673// A referenced FQN whose FIRST segment equals an in-scope class simple
3674// name binds the qualifier to the CLASS, not the package — `class j3
3675// implements j3.g` is cyclic inheritance and collapses javac's
3676// attribution for the whole package (the Object-cascade root, weixin
3677// 84 files + contagion). The repair: emit `import j3.g;` and render
3678// the SIMPLE name in every type position. The set is populated per
3679// class from a metadata pre-scan (supertypes, field types, method
3680// signature descriptors) and read by print_class_name and the
3681// DexCtx::obscured_simple implementation (which jdc-core's shorten /
3682// java_type_name consult).
3683struct ObscureState {
3684 /// The ROOT class being rendered (its simple name is the obscuring
3685 /// in-scope name).
3686 class: String,
3687 /// Field names in scope through the root class's inheritance chain
3688 /// (own + pool ancestors + framework ancestors' statics/instances +
3689 /// interface constants). An in-scope field obscures a package's
3690 /// first segment in expression position (JLS 6.4.2) — WhatsApp's
3691 /// View subclasses inherit the static Property X/Y/Z (API 31+) and
3692 /// its whole obfuscated codebase lives in package X (×30,171); the
3693 /// general set replaces the hardcoded X/Y/Z list. Obscured refs
3694 /// route through the import layer like own-simple obscuring.
3695 shadow: jdc_core::FxHashSet<String>,
3696 /// Metadata pre-scan results: internal → simple render.
3697 map: jdc_core::FxHashMap<String, String>,
3698 /// Expression-level obscured refs DISCOVERED during the body render
3699 /// (their imports emit at assembly time).
3700 recorded: jdc_core::FxHashSet<String>,
3701 /// Simple names of the CURRENT PACKAGE's own classes: importing a
3702 /// name that collides would SHADOW every same-package use of it in
3703 /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
3704 /// erroring) rather than corrupt.
3705 blocked: jdc_core::FxHashSet<String>,
3706 /// RENAMED display tails of same-package classes (zero-copy view
3707 /// into the process-wide cache): same blocking role as `blocked`
3708 /// for refs that render under the rename registry.
3709 blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3710}
3711
3712thread_local! {
3713 static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
3714 const { std::cell::RefCell::new(None) };
3715}
3716
3717/// Install the per-class render state (call at class render entry).
3718pub(crate) fn set_obscured_state(
3719 class: String,
3720 map: jdc_core::FxHashMap<String, String>,
3721 blocked: jdc_core::FxHashSet<String>,
3722 blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3723 shadow: jdc_core::FxHashSet<String>,
3724) {
3725 OBSCURE.with(|m| {
3726 *m.borrow_mut() = Some(ObscureState {
3727 class,
3728 shadow,
3729 map,
3730 recorded: jdc_core::FxHashSet::default(),
3731 blocked,
3732 blocked_renamed,
3733 });
3734 });
3735}
3736
3737/// Simple names this class's file renders through the import layer
3738/// (metadata map values + dynamically recorded nested tails). A method
3739/// local with one of these names captures the imported simple name in
3740/// expression position (locals beat single-type imports — `h.e` binds
3741/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
3742/// Empty when no render state is installed (progressive paths).
3743pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
3744 OBSCURE.with(|m| {
3745 let mut out = jdc_core::FxHashSet::default();
3746 let Ok(st) = m.try_borrow() else { return out };
3747 let Some(st) = st.as_ref() else { return out };
3748 for simple in st.map.values() {
3749 out.insert(simple.clone());
3750 }
3751 for internal in &st.recorded {
3752 let renamed = crate::apply_class_rename(internal);
3753 if let Some(tail) = renamed.rsplit(['/', '$']).next() {
3754 if !tail.is_empty() {
3755 out.insert(tail.to_string());
3756 }
3757 }
3758 }
3759 out
3760 })
3761}
3762
3763/// Take the recorded expression-level refs and clear the state.
3764pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
3765 OBSCURE.with(|m| {
3766 m.borrow_mut()
3767 .take()
3768 .map(|st| st.recorded)
3769 .unwrap_or_default()
3770 })
3771}
3772
3773// ---- cross-package access widening ----------------------------------
3774// ART tolerates cross-package access to non-public classes/members that
3775// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
3776// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
3777// l6 中是 protected"). The census scan records every cross-package
3778// reference; the installer keeps the non-public targets and the
3779// declaration renders widen them to public.
3780
3781struct WidenState {
3782 classes: jdc_core::FxHashSet<String>,
3783 methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
3784 fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
3785 /// (owner, field name) finals written outside the declaring class's
3786 /// ctor/clinit — rendered WITHOUT `final` (see refscan census).
3787 unfinal: jdc_core::FxHashSet<(String, String)>,
3788}
3789
3790static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
3791
3792pub(crate) fn install_access_widening(pool: &crate::DexPool) {
3793 let t0 = std::time::Instant::now();
3794 let raw = crate::refscan::access_widening_scan(pool.dexes());
3795 let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3796 for c in &raw.classes {
3797 if let Some(pc) = pool.get_if_materialized(c) {
3798 if pc.access & crate::access::ACC_PUBLIC == 0 {
3799 classes.insert(c.clone());
3800 }
3801 }
3802 }
3803 let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
3804 jdc_core::FxHashMap::default();
3805 for (owner, set) in &raw.methods {
3806 for (name, desc) in set {
3807 if name == "<clinit>" {
3808 continue; // never referenced
3809 }
3810 // A dex ref of an INHERITED member may name any subclass as
3811 // owner (ART resolves up the chain), while the declaration —
3812 // and its access modifier — lives on an ancestor. Register
3813 // the widening on the DECLARING class or the rendered decl
3814 // stays protected ("在 X 中是 protected 访问控制").
3815 let mut cur: Option<&str> = Some(owner.as_str());
3816 let mut hops = 0u32;
3817 while let Some(cname) = cur {
3818 hops += 1;
3819 if hops > 64 {
3820 break;
3821 }
3822 let Some(pc) = pool.get_if_materialized(cname) else {
3823 break;
3824 };
3825 // Enum ctors are source-level private-only: widening is
3826 // a javac "modifier not allowed here".
3827 if name == "<init>" && pc.access & crate::access::ACC_ENUM != 0 {
3828 break;
3829 }
3830 match pc.all_methods()
3831 .find(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str())
3832 {
3833 Some(m) if m.access & crate::access::ACC_PUBLIC == 0 => {
3834 methods
3835 .entry(cname.to_string())
3836 .or_default()
3837 .insert((name.clone(), desc.clone()));
3838 break;
3839 }
3840 Some(_) => break, // already public
3841 None => cur = pc.super_name.as_deref(),
3842 }
3843 }
3844 }
3845 }
3846 // Override-closure DOWN the hierarchy: a widened (public) super
3847 // method whose subclass override stays protected/package is
3848 // "无法覆盖...更低的访问权限" (lark +823). Every transitive
3849 // subclass declaring the same (name, desc) non-public widens too.
3850 {
3851 let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
3852 jdc_core::FxHashMap::default();
3853 for name in &pool.order {
3854 let Some(pc) = pool.get_if_materialized(name) else {
3855 continue;
3856 };
3857 if let Some(sup) = &pc.super_name {
3858 subs.entry(sup.clone()).or_default().push(name.clone());
3859 }
3860 for i in &pc.interfaces {
3861 subs.entry(i.clone()).or_default().push(name.clone());
3862 }
3863 }
3864 let seeds: Vec<(String, String, String)> = methods
3865 .iter()
3866 .flat_map(|(o, set)| {
3867 set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
3868 })
3869 .collect();
3870 for (owner, mname, mdesc) in seeds {
3871 let mut queue: Vec<String> = match subs.get(&owner) {
3872 Some(v) => v.clone(),
3873 None => continue,
3874 };
3875 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3876 while let Some(c) = queue.pop() {
3877 if !seen.insert(c.clone()) {
3878 continue;
3879 }
3880 let Some(pc) = pool.get_if_materialized(&c) else {
3881 continue;
3882 };
3883 let declares_nonpub = pc.all_methods().any(|m| {
3884 &*m.name == mname.as_str()
3885 && &*m.desc == mdesc.as_str()
3886 && m.access & crate::access::ACC_PUBLIC == 0
3887 });
3888 if declares_nonpub {
3889 methods
3890 .entry(c.clone())
3891 .or_default()
3892 .insert((mname.clone(), mdesc.clone()));
3893 }
3894 if let Some(v) = subs.get(&c) {
3895 queue.extend(v.iter().cloned());
3896 }
3897 }
3898 }
3899 // Subclass `super(..)` targets. A class C extends S; C's ctor
3900 // renders `super(..)` against S's ctor. When R8 INLINES S's
3901 // trivial ctor, C's dex ctor calls `Object.<init>` directly (not
3902 // `S.<init>`), so the bytecode census above never sees the S.<init>
3903 // reference and a PRIVATE S ctor stays private — the rendered
3904 // `super()` then fails ("R() 在 R 中是 private 访问控制", lark ×122:
3905 // pl.droidsonroids.gif.R extends com.ss.android.lark.R, whose ctor
3906 // R8 inlined to Object.<init>). Widen every private ctor of a
3907 // class that HAS subclasses so the implicit super() links. Enums
3908 // are excluded (their ctors are source-level private-only).
3909 for sup in subs.keys() {
3910 let Some(pc) = pool.get_if_materialized(sup) else {
3911 continue;
3912 };
3913 if pc.access & crate::access::ACC_ENUM != 0 {
3914 continue;
3915 }
3916 for m in pc.all_methods() {
3917 if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
3918 methods
3919 .entry(sup.clone())
3920 .or_default()
3921 .insert((m.name.to_string(), m.desc.to_string()));
3922 }
3923 }
3924 }
3925 }
3926 let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
3927 jdc_core::FxHashMap::default();
3928 for (owner, set) in &raw.fields {
3929 for name in set {
3930 // Same inherited-owner walk as methods: the protected field
3931 // lives on the ancestor that DECLARES it (weibo
3932 // mModuleContext/mData ×470 — refs named the module
3933 // subclass, the own-fields-only filter never widened the
3934 // declaring base).
3935 let mut cur: Option<&str> = Some(owner.as_str());
3936 let mut hops = 0u32;
3937 while let Some(cname) = cur {
3938 hops += 1;
3939 if hops > 64 {
3940 break;
3941 }
3942 let Some(pc) = pool.get_if_materialized(cname) else {
3943 break;
3944 };
3945 match pc.static_fields
3946 .iter()
3947 .chain(pc.instance_fields.iter())
3948 .find(|f| f.name == name.as_str())
3949 {
3950 Some(f) if f.access & crate::access::ACC_PUBLIC == 0 => {
3951 fields
3952 .entry(cname.to_string())
3953 .or_default()
3954 .insert(name.clone());
3955 break;
3956 }
3957 Some(_) => break, // already public
3958 None => cur = pc.super_name.as_deref(),
3959 }
3960 }
3961 }
3962 }
3963 if std::env::var("DDC_STATS").is_ok() {
3964 eprintln!(
3965 "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
3966 classes.len(),
3967 methods.len(),
3968 fields.len(),
3969 t0.elapsed()
3970 );
3971 }
3972
3973 let unfinal: jdc_core::FxHashSet<(String, String)> =
3974 raw.unfinal_fields.into_iter().collect();
3975 let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
3976}
3977
3978#[inline]
3979pub(crate) fn widen_class(internal: &str) -> bool {
3980 WIDEN.get().is_some_and(|w| w.classes.contains(internal))
3981}
3982
3983#[inline]
3984pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
3985 WIDEN.get().is_some_and(|w| {
3986 w.methods
3987 .get(owner)
3988 .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
3989 })
3990}
3991
3992#[inline]
3993pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
3994 WIDEN.get().is_some_and(|w| {
3995 w.fields.get(owner).is_some_and(|s| s.contains(name))
3996 })
3997}
3998
3999/// True when the census saw this final field written outside the
4000/// declaring class's `<init>`/`<clinit>` — the `final` modifier must
4001/// not render ("无法为 final 变量 v 分配值" at every foreign write).
4002#[inline]
4003pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
4004 WIDEN.get().is_some_and(|w| {
4005 w.unfinal.contains(&(owner.to_string(), name.to_string()))
4006 })
4007}
4008
4009/// Render-time lookup: the metadata map first; otherwise an
4010/// expression-level ref whose first segment equals the root class's
4011/// simple name — record it (its import emits at assembly) and render
4012/// the simple name NOW.
4013pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
4014 OBSCURE.with(|m| {
4015 let mut st = m.borrow_mut();
4016 let st = st.as_mut()?;
4017 if let Some(simple) = st.map.get(internal) {
4018 return Some(simple.clone());
4019 }
4020 let first = internal.split('/').next().unwrap_or("");
4021 // The DISPLAY own simple: when the root class itself was renamed
4022 // (case-collision `widget/d` → `widget/d2`), no in-scope type
4023 // bears the raw name any more — the package's first segment is
4024 // NOT obscured and the qualified render is the correct one
4025 // (reqable d2.java: importing d.h let the class's own boolean
4026 // field h shadow it — 无法在调用超类型构造器之前引用h ×23).
4027 let own_renamed = crate::apply_class_rename(&st.class);
4028 let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
4029 let seg_obscured = first == own || st.shadow.contains(first);
4030 if seg_obscured && internal.split('/').count() >= 2 {
4031 // A NESTED internal's in-scope simple name is its `$` tail
4032 // (`x/a$b` imports/renders as `b`) — the slash-tail left the
4033 // `$` in the render (`t2$a` flat against a nested emission).
4034 // The tail must come from the RENAMED display: case-collision
4035 // renames (`X/00i` → `X/_00i_2`) live in the class registry —
4036 // the raw tail rendered `_00i` against the declaration's
4037 // `_00i_2` (WhatsApp cannot-find ×27k after the View-shadow
4038 // escape opened this path at scale).
4039 let renamed = crate::apply_class_rename(internal);
4040 let simple = renamed
4041 .rsplit(['/', '$'])
4042 .next()
4043 .unwrap_or("")
4044 .to_string();
4045 // NOTE: no digit-lead rejection here — a digit-leading RAW
4046 // tail (`X/00i`) is sanitized to an importable `_00i` at
4047 // the import line and the refs render through the registry;
4048 // rejecting them dropped WhatsApp's whole field-X-shadowed
4049 // package import layer (cannot-find:变量 ×17k cascade).
4050 if !simple.is_empty()
4051 && !st.blocked.contains(&simple)
4052 && !st.blocked_renamed.is_some_and(|e| e.contains(&simple))
4053 {
4054 st.recorded.insert(internal.to_string());
4055 return Some(simple);
4056 }
4057 }
4058 None
4059 })
4060}
4061
4062/// Field names in scope for `class` through inheritance: own fields,
4063/// pool ancestors (supers + interfaces, transitively), and framework
4064/// ancestors via the embedded API database (chain sets memoized
4065/// process-wide — every View subclass shares the View/ViewGroup/Object
4066/// field union, so the per-file cost is its own pool chain only).
4067fn inherited_field_shadows(
4068 pool: &DexPool,
4069 class: &PoolClass,
4070) -> jdc_core::FxHashSet<String> {
4071 fn fw_chain_fields(boundary: &str) -> std::sync::Arc<jdc_core::FxHashSet<String>> {
4072 use std::sync::{Mutex, OnceLock};
4073 static CACHE: OnceLock<Mutex<jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>>>> =
4074 OnceLock::new();
4075 let cache = CACHE.get_or_init(|| Mutex::new(jdc_core::FxHashMap::default()));
4076 if let Some(hit) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(boundary) {
4077 return hit.clone();
4078 }
4079 let mut set: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4080 let mut stack: Vec<&str> = vec![boundary];
4081 let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
4082 let mut hops = 0usize;
4083 while let Some(c) = stack.pop() {
4084 hops += 1;
4085 if hops > 256 || !seen.insert(c) {
4086 continue;
4087 }
4088 crate::fwdb::for_each_field(c, |n, f| {
4089 // STATIC only: instance fields shadow qualified refs in
4090 // instance contexts but NOT in static ones, and the
4091 // render decision is context-free — the static subset
4092 // is the sound approximation (View.X/Y/Z are static;
4093 // including instance fields cost +33 battery).
4094 if f & crate::fwdb::MF_STATIC != 0 {
4095 set.insert(n.to_string());
4096 }
4097 });
4098 if let Some(sup) = crate::fwdb::super_of(c) {
4099 stack.push(sup);
4100 }
4101 crate::fwdb::for_each_interface(c, |i| stack.push(i));
4102 }
4103 let arc = std::sync::Arc::new(set);
4104 cache
4105 .lock()
4106 .unwrap_or_else(|e| e.into_inner())
4107 .insert(boundary.to_string(), arc.clone());
4108 arc
4109 }
4110
4111 let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4112 let mut stack: Vec<String> = vec![class.name.to_string()];
4113 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4114 let mut hops = 0usize;
4115 while let Some(cn) = stack.pop() {
4116 hops += 1;
4117 if hops > 512 || !seen.insert(cn.clone()) {
4118 continue;
4119 }
4120 let Some(pc) = pool.get(&cn) else {
4121 // Framework boundary: memoized chain set.
4122 out.extend(fw_chain_fields(&cn).iter().cloned());
4123 continue;
4124 };
4125 // Static-only (pool side too): ANCESTOR private instance
4126 // fields are NOT inherited into lexical scope (JLS 6.4.3.1) —
4127 // including them over-triggered the obscured-import route and
4128 // the emitted simple static qualifiers were captured by real
4129 // in-scope variables (reqable SearchView `a.H` "变量 H" ×128).
4130 // Own instance fields shadowing packages are handled at the
4131 // ROOT: the collision-mint loops avoid package first segments
4132 // (lib.rs root_segs guard), and raw-name collisions go
4133 // through field_deshadow_renames.
4134 for f in pc.static_fields.iter() {
4135 out.insert(f.name.to_string());
4136 }
4137 if let Some(sup) = &pc.super_name {
4138 if sup != "java/lang/Object" {
4139 stack.push(sup.clone());
4140 } else {
4141 // Object itself lives in the DB (its fields shadow too —
4142 // none exist, but interface constants reached via
4143 // interfaces below still do).
4144 }
4145 } else {
4146 // Interface (no super): java/lang/Object contributes nothing.
4147 }
4148 stack.extend(pc.interfaces.iter().cloned());
4149 }
4150 out
4151}
4152
4153/// The pre-scan: internal names referenced by the class's metadata
4154/// (supertypes, field types, method descriptors) whose first segment
4155/// equals the class's own simple name and which exist in the pool —
4156/// these get imports and simple-name renders.
4157/// Base simple name of an internal (`pq/c$x` → `c`), if it lives in
4158/// `pkg` (the bare-name segment a same-package ref renders under).
4159fn sibling_simple(internal: &str, pkg: &str) -> Option<String> {
4160 let (p, tail) = match internal.rfind('/') {
4161 Some(i) => (&internal[..i], &internal[i + 1..]),
4162 None => ("", internal),
4163 };
4164 if p != pkg || tail.is_empty() {
4165 return None;
4166 }
4167 let base = tail.split('$').next().unwrap_or("");
4168 if base.is_empty() {
4169 None
4170 } else {
4171 Some(base.to_string())
4172 }
4173}
4174
4175/// Simple names of SAME-PACKAGE classes this file's family references
4176/// from method bodies (raw insn ref scan — decode only, no lift) or
4177/// metadata (supertypes, field descriptors, signature descriptors).
4178/// JLS 7.5.1: a single-type import legally shadows same-package
4179/// siblings throughout the file — it only breaks the refs the file
4180/// ITSELF makes to them, so the package-wide blocked set narrows to
4181/// this. Undercounting would silently rebind a sibling ref (semantic
4182/// corruption), so the universe is exhaustive over code refs:
4183/// invoke/field owners, type-index instructions, catch types.
4184fn sibling_ref_simples(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashSet<String> {
4185 let pkg = class.name.rfind('/').map(|i| &class.name[..i]).unwrap_or("");
4186 let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4187 let add_internal = |n: &str, out: &mut jdc_core::FxHashSet<String>| {
4188 if let Some(s) = sibling_simple(n, pkg) {
4189 out.insert(s);
4190 }
4191 };
4192 let add_desc = |d: &str, out: &mut jdc_core::FxHashSet<String>| {
4193 // descriptor → internal(s): arrays peel, L..; strips
4194 let mut t = d;
4195 while t.starts_with('[') {
4196 t = &t[1..];
4197 }
4198 if t.len() > 1 && t.starts_with('L') && t.ends_with(';') {
4199 add_internal(&t[1..t.len() - 1], out);
4200 }
4201 };
4202 // metadata: supertypes, fields, signatures (family-wide below)
4203 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4204 let mut queue: Vec<String> = vec![class.name.clone()];
4205 while let Some(cn) = queue.pop() {
4206 if !seen.insert(cn.clone()) {
4207 continue;
4208 }
4209 queue.extend(pool.children_of(&cn).iter().cloned());
4210 let Some(pc) = pool.get(&cn) else { continue };
4211 add_internal(&pc.name, &mut out);
4212 if let Some(sup) = &pc.super_name {
4213 add_internal(sup, &mut out);
4214 }
4215 for i in &pc.interfaces {
4216 add_internal(i, &mut out);
4217 }
4218 for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
4219 add_desc(&f.desc, &mut out);
4220 }
4221 for m in pc.all_methods() {
4222 if let Some(d) = m.parsed_desc() {
4223 for a in &d.args {
4224 if let JavaType::Object(n) = a {
4225 add_internal(n, &mut out);
4226 } else if let JavaType::Array(_) = a {
4227 add_desc(&a.to_descriptor(), &mut out);
4228 }
4229 }
4230 if let JavaType::Object(n) = &d.ret {
4231 add_internal(n, &mut out);
4232 } else if let JavaType::Array(_) = &d.ret {
4233 add_desc(&d.ret.to_descriptor(), &mut out);
4234 }
4235 }
4236 let Some(dex) = pool.dex(m.dex_idx) else {
4237 continue;
4238 };
4239 let Some(code) = dex.code_at(m.code_off) else {
4240 continue;
4241 };
4242 for ins in code.insns.iter() {
4243 use ddc_dex::insn::InsnKind;
4244 match &ins.kind {
4245 InsnKind::Invoke { method_idx, .. } => {
4246 let mm = dex.method(*method_idx);
4247 add_internal(&dex.class_name(mm.class_idx), &mut out);
4248 }
4249 InsnKind::IGet { field_idx, .. }
4250 | InsnKind::IPut { field_idx, .. }
4251 | InsnKind::SGet { field_idx, .. }
4252 | InsnKind::SPut { field_idx, .. } => {
4253 let f = dex.field(*field_idx);
4254 add_internal(&dex.class_name(f.class_idx), &mut out);
4255 }
4256 InsnKind::ConstClass { type_idx, .. }
4257 | InsnKind::NewInstance { type_idx, .. }
4258 | InsnKind::NewArray { type_idx, .. }
4259 | InsnKind::FilledNewArray { type_idx, .. }
4260 | InsnKind::CheckCast { type_idx, .. }
4261 | InsnKind::InstanceOf { type_idx, .. } => {
4262 add_desc(dex.type_name(*type_idx), &mut out);
4263 }
4264 _ => {}
4265 }
4266 }
4267 for h in code.handlers.iter() {
4268 for (tidx, _) in &h.catches {
4269 add_desc(dex.type_name(*tidx), &mut out);
4270 }
4271 }
4272 }
4273 }
4274 // the file's OWN family names are not import-shadow risks for
4275 // themselves (own-simple drops happen later); keep them anyway —
4276 // a sibling simple equal to a family base only blocks when a
4277 // same-package OTHER class shares it, which is the true collision.
4278 out
4279}
4280
4281fn compute_obscured_renders(
4282 pool: &DexPool,
4283 class: &PoolClass,
4284 shadow: &jdc_core::FxHashSet<String>,
4285) -> jdc_core::FxHashMap<String, String> {
4286 let own_renamed = crate::apply_class_rename(&class.name);
4287 let own_simple = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
4288 if own_simple.is_empty() {
4289 return jdc_core::FxHashMap::default();
4290 }
4291 let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4292 for sup in class.interfaces.iter() {
4293 refs.insert(sup.clone());
4294 }
4295 if let Some(sup) = &class.super_name {
4296 refs.insert(sup.clone());
4297 }
4298 for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
4299 if let JavaType::Object(n) = crate::desc_type(&f.desc) {
4300 refs.insert(n.to_string());
4301 }
4302 }
4303 for m in class.all_methods() {
4304 if let Some(d) = m.parsed_desc() {
4305 for a in d.args.iter() {
4306 if let JavaType::Object(n) = a {
4307 refs.insert(n.to_string());
4308 }
4309 }
4310 if let JavaType::Object(n) = &d.ret {
4311 refs.insert(n.to_string());
4312 }
4313 }
4314 }
4315 let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
4316 for r in refs {
4317 // First segment == the class's own simple name: the qualified
4318 // render would be obscured. The name must exist in the pool for
4319 // the import to resolve. The KEY stays internal (renders look
4320 // refs up by internal name), but the import line and the
4321 // simple name use the RENAMED display — a collision-renamed
4322 // class renders under its display name and an import of the
4323 // internal name does not resolve.
4324 let first = r.split('/').next().unwrap_or("");
4325 let seg_obscured = first == own_simple || shadow.contains(first);
4326 if seg_obscured && r.split('/').count() >= 2 && pool.get(&r).is_some() {
4327 // Own-family refs (the class itself, its nested members)
4328 // render through member scope — an import would be redundant
4329 // (self-import) or shadow a same-package sibling named like
4330 // the tail (`import t.t2.a` hijacks every bare `a` that
4331 // meant sibling class t.a — lark t/t2).
4332 if r == class.name || r.starts_with(&format!("{}$", class.name)) {
4333 continue;
4334 }
4335 let display = crate::apply_class_rename(&r);
4336 let simple = display
4337 .rsplit(['/', '$'])
4338 .next()
4339 .unwrap_or("")
4340 .to_string();
4341 if !simple.is_empty() {
4342 out.insert(r.clone(), simple);
4343 }
4344 }
4345 }
4346 out
4347}
4348
4349pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
4350 if let Some(simple) = obscured_render_pub(internal) {
4351 return sanitize_ref(&simple);
4352 }
4353 let orig_internal: &str = internal;
4354 let cow = crate::apply_class_rename(internal);
4355 let internal: &str = &cow;
4356 // The pool indexes PRE-rename internals. The ddcroot ROOT-PACKAGE
4357 // RELOCATION renames `Foo$Bar` to `ddcroot/Foo$Bar` and the pool
4358 // lookups below must fall back to the pre-rename name, or the
4359 // class reads as off-pool — the framework-nesting fallback then
4360 // dots the `$` (`ddcroot.UserCustomStatusExtraParams.
4361 // CalendarAutomaticStatus` against a FLAT `...$...` declaration —
4362 // "不可见" ×138 on lark's R8-outer-deleted enum family). STRICTLY
4363 // the relocation (renamed == rp + "/" + orig): collision-renamed
4364 // nested families (weibo x0$a$b) keep their pre-existing
4365 // generic-loop treatment — a general orig fallback flipped their
4366 // `!pool.get(display)` dotting condition and regressed weibo
4367 // +483.
4368 let reloc_orig: Option<&str> = if orig_internal != internal {
4369 match crate::root_pkg_display() {
4370 Some(rp)
4371 if internal.len() > rp.len() + 1
4372 && internal.starts_with(rp.as_str())
4373 && internal.as_bytes()[rp.len()] == b'/'
4374 && &internal[rp.len() + 1..] == orig_internal
4375 && pool.get(orig_internal).is_some() =>
4376 {
4377 Some(orig_internal)
4378 }
4379 _ => None,
4380 }
4381 } else {
4382 None
4383 };
4384 let pool_name: &str = if pool.get(internal).is_some() {
4385 internal
4386 } else {
4387 reloc_orig.unwrap_or(internal)
4388 };
4389 // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
4390 // shape, `Outer$lruCache$1`) is emitted as its own top-level file
4391 // whose simple name keeps every `$` — the `$` boundaries inside
4392 // that unit are part of the NAME, not nesting. References must use
4393 // the flat unit name; the generic loop below dotted the first
4394 // boundary (`LruCacheKt.lruCache$1`) against the declaration
4395 // `class LruCacheKt$lruCache$1` — every use of the type failed and
4396 // javac's attribution for the whole file collapsed (3627 pure-
4397 // cascade files on weibo).
4398 if pool_name.contains('$') && pool.get(pool_name).is_some() {
4399 if let Some(root) = emission_root(pool, pool_name) {
4400 if root.contains('$') {
4401 let below = &pool_name[root.len()..];
4402 if below.is_empty() {
4403 // The unit itself: the flat name IS the reference.
4404 return sanitize_ref(&internal.replace('/', "."));
4405 }
4406 let segs: Vec<&str> = below[1..].split('$').collect();
4407 if segs.iter().any(|s| s.is_empty()) {
4408 // R8 tails like `ThreadMsg$$$` cannot be dotted at
4409 // all — keep the whole name flat.
4410 return sanitize_ref(&internal.replace('/', "."));
4411 }
4412 let root_cow = crate::apply_class_rename(&root);
4413 let mut out = root_cow.replace('/', ".");
4414 for s in segs {
4415 out.push('.');
4416 out.push_str(s);
4417 }
4418 return sanitize_ref(&out);
4419 }
4420 }
4421 }
4422 let mut out = String::new();
4423 // `known` must test the ACCUMULATED internal prefix, not the bare
4424 // inter-`$` segment: the per-segment shape checked `pool.get("a")`
4425 // for the second level of `s5/o$a$b`, missed, and rendered the
4426 // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
4427 // nested type.
4428 let mut off = 0usize;
4429 loop {
4430 let rest = &internal[off..];
4431 match rest.find('$') {
4432 Some(i) => {
4433 let seg = &rest[..i];
4434 let prefix = &internal[..off + i];
4435 let known = pool.get(prefix).is_some()
4436 || jdc_core::rename::is_renamed_display(prefix)
4437 // The FULL name is not a pool class (also under
4438 // the ddcroot-relocation pre-rename form — the
4439 // migration hides pool classes from the renamed
4440 // lookup): this `$` cannot be a literal name
4441 // (pool literal classes — an app's own
4442 // `View$OnUnhandledKeyEventListener` — keep their
4443 // `$` here AND at their declaration), so it can
4444 // only be an external framework nesting boundary
4445 // (`View$OnClickListener` → `.OnClickListener`).
4446 || !(pool.get(internal).is_some() || reloc_orig.is_some());
4447 // The `$` may only become a nesting dot when the tail
4448 // segment STARTS a Java identifier: R8's desugared-
4449 // library names carry `$` inside PACKAGE paths
4450 // (`j$/util/...` dotted into `j..util`) and suffixes
4451 // like `Collection$-EL` or anonymous `RequestId$1`
4452 // cannot be dotted under any reading.
4453 //
4454 // Sanitizer-escaped tails DO dot for POOL classes: the
4455 // injective `_u<hex>` escape turns any non-ASCII (or
4456 // ASCII-punctuation) leading char into `_`-initial —
4457 // a valid identifier start — and the declaration side
4458 // renders the member that way (weibo's unicode-named
4459 // nested `a$Ꮺ`: decl `class a_u2dIA` inside a, ctor
4460 // params dotted `a.a_u2dIA`, but casts stayed flat
4461 // `a$a_u2dIA` — 找不到符号 类 ×~500). Digit and `$`
4462 // tails stay flat (emission-unit files keep the `$`;
4463 // framework binary names are unreachable either way).
4464 let tail_next = rest[i + 1..].chars().next();
4465 let tail_ok = match tail_next {
4466 Some(c) if c.is_ascii_alphabetic() || c == '_' => true,
4467 // `/` = a `$` INSIDE the package path (desugar's
4468 // `j$/util/...` — dotting produced `j..util`,
4469 // deepseek 非法'.' ×3,661); digit tails are flat
4470 // emission units; `-`-led tails fail
4471 // clean_member_tail and emit flat too; `$` tails
4472 // ($$-orphans) keep the old behavior.
4473 Some(c) if !c.is_ascii_digit() && c != '$' && c != '/' && c != '-' => {
4474 pool.get(internal).is_some()
4475 }
4476 _ => false,
4477 };
4478 out.push_str(&seg.replace('/', "."));
4479 out.push_str(if known && tail_ok { "." } else { "$" });
4480 off += i + 1;
4481 }
4482 None => {
4483 out.push_str(&rest.replace('/', "."));
4484 return sanitize_ref(&out);
4485 }
4486 }
4487 }
4488}
4489
4490/// The top-level EMISSION UNIT ancestor of a pool class: walking the
4491/// `$`-chain upward (outer_of), the first ancestor that is itself
4492/// emitted as its own compilation unit — a digit-tail rest marks a
4493/// flat boundary (see top_level_classes). Clean members render inline
4494/// in their outer, so only the unit's own name may carry `$`.
4495fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
4496 let mut cur = internal.to_string();
4497 loop {
4498 let outer = pool.outer_of(&cur)?;
4499 let rest = cur
4500 .strip_prefix(outer)
4501 .and_then(|t| t.strip_prefix('$'))
4502 .unwrap_or("");
4503 if !crate::clean_member_tail(rest) {
4504 return Some(cur);
4505 }
4506 cur = outer.to_string();
4507 }
4508}
4509
4510/// Class-file names may contain characters Java source identifiers
4511/// cannot (`Collection$-EL`); the deterministic mapping matches the
4512/// declaration sites (java_ident).
4513/// Every `.`-segment of a fully-qualified name must start a Java
4514/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
4515///
4516/// PUBLIC on purpose: this is the single source of truth for the
4517/// declaration↔file-name mapping. The CLI writer used to keep its own
4518/// copy (`sanitize_file_seg`) which had drifted — it lacked the lone-`_`
4519/// escape below — so `l.֡` declared `class __` inside a file named
4520/// `_.java`. Callers reach it through `sanitize_seg`/`sanitize_internal`.
4521///
4522/// INJECTIVE on the characters, which is the property the writer needs.
4523/// The old mapping folded every non-identifier character to `_`, so
4524/// `l.᩻ܶ`, `l.᩻ۡ` and `l.֫᩷` all became `l.__`: on bin.mt.plus 22,636
4525/// distinct classes in package `l` collapsed onto three file names, the
4526/// writer's EEXIST branch overwrote them, and 22,633 sources were
4527/// destroyed with exit code 0. `_u<hex>` keeps them distinct, is
4528/// self-describing (it encodes the original code point) and costs the
4529/// decompiler nothing — the alternative, minting 22,636 registry
4530/// renames, makes every type reference allocate and took this sample
4531/// from 5.2 s to 26.8 s.
4532///
4533/// The only residual collisions are *lookalikes*: a literal class named
4534/// `_u1a7b` beside the class `᩻` (same code point), or the pre-existing
4535/// keyword form `_do` beside `do`. Those are detected and repaired
4536/// deterministically by `lossy_sanitize_renames`, which is why that pass
4537/// still exists.
4538pub fn sanitize_fq(dotted: &str) -> String {
4539 dotted
4540 .split('.')
4541 .map(sanitize_fq_seg)
4542 .collect::<Vec<_>>()
4543 .join(".")
4544}
4545
4546/// One `.`-segment: escape, then repair identifier-position problems.
4547fn sanitize_fq_seg(seg: &str) -> String {
4548 // Ident-safe ASCII passes through unchanged (the overwhelmingly
4549 // common case, and the only one on the borrow-fast path); every
4550 // other character — non-ASCII, and ASCII punctuation such as the
4551 // `-` in `Collection$-EL` — becomes `_u<hex>`.
4552 let plain = seg
4553 .chars()
4554 .all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
4555 let mut out = if plain {
4556 seg.to_string()
4557 } else {
4558 use std::fmt::Write as _;
4559 let mut s = String::with_capacity(seg.len() + 8);
4560 for c in seg.chars() {
4561 if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
4562 s.push(c);
4563 } else {
4564 s.push_str("_u");
4565 let _ = write!(s, "{:x}", c as u32);
4566 }
4567 }
4568 s
4569 };
4570 // `out` is pure ASCII from here: the identifier-position repairs.
4571 if is_java_keyword_name(&out)
4572 || is_restricted_type_name(&out)
4573 || out.chars().next().is_some_and(|c| c.is_ascii_digit())
4574 {
4575 out.insert(0, '_');
4576 }
4577 // A lone `_` is a reserved IDENTIFIER since Java 9.
4578 if out == "_" {
4579 out = "__".to_string();
4580 }
4581 out
4582}
4583
4584/// Restricted contextual TYPE names — legal as member/local names
4585/// (rt.jar compiles `var` locals), illegal in class declarations and
4586/// type references. Consulted only on CLASS-name paths.
4587pub(crate) fn is_restricted_type_name(s: &str) -> bool {
4588 matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
4589}
4590
4591fn is_java_keyword_name(s: &str) -> bool {
4592 matches!(
4593 s,
4594 "abstract"
4595 | "assert"
4596 | "boolean"
4597 | "break"
4598 | "byte"
4599 | "case"
4600 | "catch"
4601 | "char"
4602 | "class"
4603 | "const"
4604 | "continue"
4605 | "default"
4606 | "do"
4607 | "double"
4608 | "else"
4609 | "enum"
4610 | "extends"
4611 | "final"
4612 | "finally"
4613 | "float"
4614 | "for"
4615 | "goto"
4616 | "if"
4617 | "implements"
4618 | "import"
4619 | "instanceof"
4620 | "int"
4621 | "interface"
4622 | "long"
4623 | "native"
4624 | "new"
4625 | "package"
4626 | "private"
4627 | "protected"
4628 | "public"
4629 | "return"
4630 | "short"
4631 | "static"
4632 | "strictfp"
4633 | "super"
4634 | "switch"
4635 | "synchronized"
4636 | "this"
4637 | "throw"
4638 | "throws"
4639 | "transient"
4640 | "try"
4641 | "void"
4642 | "volatile"
4643 | "while"
4644 | "true"
4645 | "false"
4646 | "null"
4647 // `_` is a reserved identifier since Java 9 (Alipay's
4648 // instant-run fields are named `_`) — the `_<name>` mapping
4649 // turns it into `__`, matching jdc-core's call sites.
4650 | "_"
4651 )
4652}
4653
4654fn sanitize_ref(name: &str) -> String {
4655 sanitize_fq(name)
4656}
4657
4658/// Dotted source form of an internal name.
4659pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
4660 print_class_name(pool, internal)
4661}
4662
4663/// Unused import silencer.
4664#[allow(dead_code)]
4665fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
4666 let _ = java_type_to_generic;
4667}
4668
4669#[cfg(test)]
4670mod sanitize_tests {
4671 use super::sanitize_fq;
4672
4673 /// The property the file writer depends on: distinct class names must
4674 /// reach distinct output paths. The lossy predecessor of this mapping
4675 /// folded every non-identifier character to `_`, so 22,636 classes of
4676 /// `bin.mt.plus` (names built from Thai/Yi/Syriac code points) became
4677 /// three (`l.__`, `l.___`, `l._`) and all but three were overwritten.
4678 #[test]
4679 fn distinct_obfuscated_names_stay_distinct() {
4680 // The three names that collapsed onto `l/__.java` in the field.
4681 let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
4682 let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
4683 let before = mapped.len();
4684 mapped.sort();
4685 mapped.dedup();
4686 assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
4687 assert!(
4688 mapped.iter().all(|m| m.is_ascii()),
4689 "sanitizer must stay ASCII: {mapped:?}"
4690 );
4691 // Self-describing: the escape carries the original code point.
4692 assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
4693 }
4694
4695 /// Every output must be a legal Java identifier segment: no leading
4696 /// digit, no keyword, not the lone `_` reserved since Java 9.
4697 #[test]
4698 fn output_is_a_legal_identifier() {
4699 for (input, want) in [
4700 ("do", "_do"),
4701 ("_", "__"),
4702 ("0Xx", "_0Xx"),
4703 ("Collection$-EL", "Collection$_u2dEL"),
4704 ("᩻", "_u1a7b"),
4705 ("a", "a"),
4706 ("A$B", "A$B"),
4707 ("x_y", "x_y"),
4708 ] {
4709 assert_eq!(sanitize_fq(input), want, "input {input:?}");
4710 }
4711 }
4712
4713 /// Documented residual: ident-safe ASCII passes through unchanged, so a
4714 /// literal `_u1a7b` collides with the single character U+1A7B. The
4715 /// collision is real — which is why `lossy_sanitize_renames` still owns
4716 /// a repair pass — and this test pins the shape the guard must catch.
4717 #[test]
4718 fn lookalike_collision_is_known_and_bounded() {
4719 assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
4720 }
4721}