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