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