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 view_shadow = is_view_subclass(pool, class);
154 let mut obscured_map = compute_obscured_renders(pool, class, view_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 view_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/// Standard ctor argument signatures of the common Throwable-family
1075/// framework classes. R8 strips custom exceptions to empty shells
1076/// (`final class k extends Error {}`) whose callers reach Error(String)
1077/// through OUTLINED ctor invokes — the method ref owner is the
1078/// FRAMEWORK super, invisible to the owner-keyed ctor_ref_sigs, so the
1079/// ref-driven bridges cannot fire (uuyc `throw new Bd.k("...")` ×525).
1080/// Mirroring the classic set is dex-faithful (ART resolves <init> up
1081/// the chain too) and each bridge is a plain super(..) delegation.
1082fn throwable_ctor_sigs(fw: &str) -> Option<&'static [&'static str]> {
1083 // The empty sig is included because emitting ANY explicit ctor
1084 // suppresses Java's implicit default: dex callers legally reach
1085 // Throwable() up the chain (`new X()` against the empty shell), and
1086 // without the no-arg bridge they strand (weixin +1,464 找不到合适
1087 // 的构造器 when the set omitted it). `public X() { super(); }` is
1088 // exactly the implicit default.
1089 const SIGS: &[&str] = &[
1090 "",
1091 "Ljava/lang/String;",
1092 "Ljava/lang/String;Ljava/lang/Throwable;",
1093 "Ljava/lang/Throwable;",
1094 ];
1095 matches!(
1096 fw,
1097 "java/lang/Throwable"
1098 | "java/lang/Error"
1099 | "java/lang/Exception"
1100 | "java/lang/RuntimeException"
1101 | "java/io/IOException"
1102 | "java/io/InterruptedIOException"
1103 | "java/io/UnsupportedEncodingException"
1104 | "java/io/EOFException"
1105 | "java/io/FileNotFoundException"
1106 | "java/net/SocketException"
1107 | "java/net/URISyntaxException"
1108 | "java/lang/CloneNotSupportedException"
1109 | "java/lang/InterruptedException"
1110 | "java/lang/ReflectiveOperationException"
1111 | "java/lang/ClassNotFoundException"
1112 | "java/lang/NoSuchFieldException"
1113 | "java/lang/NoSuchMethodException"
1114 | "java/lang/InstantiationException"
1115 | "java/lang/IllegalAccessException"
1116 | "java/lang/reflect/InvocationTargetException"
1117 | "java/security/GeneralSecurityException"
1118 | "java/util/concurrent/ExecutionException"
1119 | "java/util/concurrent/TimeoutException"
1120 | "java/text/ParseException"
1121 )
1122 .then_some(SIGS)
1123}
1124
1125fn synth_missing_interface_stubs(
1126 pool: &DexPool,
1127 class: &PoolClass,
1128 out: &mut String,
1129 depth: usize,
1130 emitted_any: &mut bool,
1131) {
1132 use crate::access::*;
1133 // Concrete classes only: interfaces / abstract classes / annotations
1134 // legitimately leave methods unimplemented.
1135 if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
1136 return;
1137 }
1138 fn argsig(d: &str) -> &str {
1139 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1140 let hi = d.find(')').unwrap_or(d.len());
1141 &d[lo..hi]
1142 }
1143 // Classic framework interfaces whose abstract methods a concrete
1144 // implementer must carry. R8 strips an implementation whose call
1145 // sites it proved unreachable, and the closure walk cannot see
1146 // framework method lists (not in the dex) — without the table,
1147 // `implements Runnable` with a stripped run() fails javac (lark
1148 // Runnable.run ×8, reqable Parcelable.writeToParcel ×15,
1149 // Closeable.close ×3). The stub stays ART-faithful: dispatch on
1150 // the missing proto throws AbstractMethodError at runtime too.
1151 const FW_IFACE_METHODS: &[(&str, &[(&str, &str)])] = &[
1152 ("java/lang/Runnable", &[("run", "()V")]),
1153 ("java/lang/AutoCloseable", &[("close", "()V")]),
1154 ("java/io/Closeable", &[("close", "()V")]),
1155 ("java/io/Flushable", &[("flush", "()V")]),
1156 ("java/util/concurrent/Callable", &[("call", "()Ljava/lang/Object;")]),
1157 ("java/lang/Comparable", &[("compareTo", "(Ljava/lang/Object;)I")]),
1158 ("java/util/Iterator", &[("hasNext", "()Z"), ("next", "()Ljava/lang/Object;")]),
1159 ("java/util/Iterable", &[("iterator", "()Ljava/util/Iterator;")]),
1160 ("java/util/Enumeration", &[("hasMoreElements", "()Z"), ("nextElement", "()Ljava/lang/Object;")]),
1161 ("java/util/Comparator", &[("compare", "(Ljava/lang/Object;Ljava/lang/Object;)I")]),
1162 (
1163 "android/os/Parcelable",
1164 &[("writeToParcel", "(Landroid/os/Parcel;I)V"), ("describeContents", "()I")],
1165 ),
1166 ("android/view/View$OnClickListener", &[("onClick", "(Landroid/view/View;)V")]),
1167 (
1168 "android/content/ServiceConnection",
1169 &[
1170 ("onServiceConnected", "(Landroid/content/ComponentName;Landroid/os/IBinder;)V"),
1171 ("onServiceDisconnected", "(Landroid/content/ComponentName;)V"),
1172 ],
1173 ),
1174 (
1175 "java/lang/Thread$UncaughtExceptionHandler",
1176 &[("uncaughtException", "(Ljava/lang/Thread;Ljava/lang/Throwable;)V")],
1177 ),
1178 ];
1179 // Transitive interface closure — seeded from the WHOLE superclass
1180 // chain, not just the class's own `implements`: weixin rd extends
1181 // abstract k0 implements ey2.a, R8 stripped rd's onAttach, and the
1182 // old own-interfaces-only seed saw an empty requirement (×51
1183 // weixin a.onAttach family). Pool interfaces contribute their
1184 // method lists; framework interfaces consult the table above.
1185 // (name, argsig) -> the unique declaring desc, or None when two
1186 // ancestors declare the same erasure with DIFFERENT descriptors —
1187 // a Java-inexpressible diamond where any synthesized stub would
1188 // itself fail "无法覆盖/无法实现" (weibo +1016 regression when the
1189 // first-wins desc was stubbed against a conflicting sibling).
1190 let mut required: jdc_core::FxHashMap<(String, String), Option<(String, String)>> =
1191 jdc_core::FxHashMap::default();
1192 let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1193 let mut stack: Vec<String> = Vec::new();
1194 {
1195 let mut cur: Option<&PoolClass> = Some(class);
1196 let mut hops = 0;
1197 while let Some(c) = cur {
1198 hops += 1;
1199 if hops > 64 {
1200 break;
1201 }
1202 stack.extend(c.interfaces.iter().cloned());
1203 let mut hit_fw = false;
1204 cur = c.super_name.as_ref().and_then(|s| {
1205 if s == "java/lang/Object" {
1206 None
1207 } else {
1208 let pc = pool.get(s);
1209 if pc.is_none() {
1210 hit_fw = true;
1211 }
1212 pc
1213 }
1214 });
1215 if hit_fw {
1216 // A FRAMEWORK superclass contributes methods the pool
1217 // walk cannot see: the class may already provide (or be
1218 // barred from overriding — Context.getString is final)
1219 // any requirement, and a stub here collided ×669 weibo.
1220 // No stubs past the framework boundary.
1221 return;
1222 }
1223 }
1224 }
1225 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1226 while let Some(iname) = stack.pop() {
1227 if !seen.insert(iname.clone()) {
1228 continue;
1229 }
1230 let Some(ic) = pool.get(&iname) else {
1231 for (iface, methods) in FW_IFACE_METHODS {
1232 if *iface == iname.as_str() {
1233 for (mn, md) in *methods {
1234 let key = (mn.to_string(), argsig(md).to_string());
1235 match required.entry(key) {
1236 std::collections::hash_map::Entry::Vacant(v) => {
1237 v.insert(Some((mn.to_string(), md.to_string())));
1238 }
1239 std::collections::hash_map::Entry::Occupied(mut o) => {
1240 if o.get().as_ref().is_some_and(|(_, d)| d != md) {
1241 o.insert(None);
1242 }
1243 }
1244 }
1245 }
1246 }
1247 }
1248 continue;
1249 };
1250 for m in ic.all_methods() {
1251 if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
1252 continue;
1253 }
1254 let key = (m.name.to_string(), argsig(&m.desc).to_string());
1255 if m.code_off == 0 {
1256 let mdesc = m.desc.to_string();
1257 match required.entry(key) {
1258 std::collections::hash_map::Entry::Vacant(v) => {
1259 v.insert(Some((m.name.to_string(), mdesc)));
1260 }
1261 std::collections::hash_map::Entry::Occupied(mut o) => {
1262 if o.get().as_ref().is_some_and(|(_, d)| *d != mdesc) {
1263 o.insert(None);
1264 }
1265 }
1266 }
1267 } else {
1268 defaults.insert(key);
1269 }
1270 }
1271 stack.extend(ic.interfaces.iter().cloned());
1272 }
1273 if required.is_empty() {
1274 return;
1275 }
1276 // Methods provided by the class + its concrete superclass chain, plus
1277 // the java/lang/Object publics that satisfy common requirements.
1278 let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1279 provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
1280 provided.insert(("hashCode".into(), String::new()));
1281 provided.insert(("toString".into(), String::new()));
1282 // (name, desc) pairs whose erasure twin exists elsewhere in the
1283 // chain with a DIFFERENT desc — stub-hostile shapes (see below).
1284 let mut chain_sigs: jdc_core::FxHashMap<(String, String), jdc_core::FxHashSet<String>> =
1285 jdc_core::FxHashMap::default();
1286 let mut cur: Option<&PoolClass> = Some(class);
1287 while let Some(c) = cur {
1288 for m in c.all_methods() {
1289 if !m.is_static() && &*m.name != "<init>" && &*m.name != "<clinit>" {
1290 chain_sigs
1291 .entry((m.name.to_string(), argsig(&m.desc).to_string()))
1292 .or_default()
1293 .insert(m.desc.to_string());
1294 }
1295 }
1296 for m in c.all_methods() {
1297 // A method PROVIDES the implementation if it is not abstract:
1298 // bytecode (code_off != 0) OR native (JNI body, code_off == 0
1299 // but ACC_NATIVE — `…ToNative` methods satisfy their interface
1300 // and must not be stubbed twice).
1301 if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
1302 // A RENAMED method no longer provides its raw (name,
1303 // argsig): impostor overrides (return type satisfying no
1304 // ancestor declaration) and erasure-clash losers render
1305 // under their registry display, and the ancestor
1306 // requirement falls to the stub below — matching ART,
1307 // where the interface proto never resolved either.
1308 let renamed = jdc_core::rename::field_display(&c.name, &m.name, &m.desc)
1309 .is_some();
1310 if !renamed {
1311 provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
1312 }
1313 }
1314 }
1315 cur = c.super_name.as_ref().and_then(|s| {
1316 if s == "java/lang/Object" {
1317 None
1318 } else {
1319 pool.get(s)
1320 }
1321 });
1322 }
1323 let mut chain_conflict: jdc_core::FxHashSet<(String, String)> =
1324 jdc_core::FxHashSet::default();
1325 for ((name, _argsig), descs) in chain_sigs.iter() {
1326 if descs.len() > 1 {
1327 for d in descs {
1328 chain_conflict.insert((name.clone(), d.clone()));
1329 }
1330 }
1331 }
1332 let mut missing: Vec<(String, String)> = Vec::new();
1333 for (key, m) in &required {
1334 let Some(m) = m else { continue }; // conflicting ancestor descs
1335 if defaults.contains(key) || provided.contains(key) {
1336 continue;
1337 }
1338 // Kotlin bridge variant (return kotlin/Unit beside the real
1339 // boolean/object twin a framework interface declares): stubbing
1340 // it renders `java.lang.Void addAll(Collection)` that neither
1341 // implements the twin nor serves its callers — the poisoned
1342 // return type cascades through every call site (weibo +703
1343 // regression). The honest 未覆盖 error for the twin stays.
1344 if m.1.ends_with(")Lkotlin/Unit;") {
1345 continue;
1346 }
1347 // A chain method sharing the erasure but NOT the descriptor
1348 // (abstract super with a different return, an impostor the
1349 // rename pass left because it satisfies another ancestor):
1350 // the stub would clash with it — skip; the class keeps the
1351 // single honest 未覆盖 error instead of gaining a 无法覆盖 one.
1352 if chain_conflict.contains(&(m.0.clone(), m.1.clone())) {
1353 continue;
1354 }
1355 missing.push(m.clone());
1356 }
1357 missing.sort();
1358 let ind = " ".repeat(depth + 1);
1359 for (mname, mdesc) in missing {
1360 let lo = mdesc.find('(').map(|i| i + 1).unwrap_or(0);
1361 let hi = mdesc.find(')').unwrap_or(mdesc.len());
1362 let ret_s = &mdesc[hi + 1..];
1363 let args_v = split_arg_descs(&mdesc[lo..hi]);
1364 let d = ParsedStub { ret: crate::desc_type(ret_s), args: args_v };
1365 if *emitted_any {
1366 out.push('\n');
1367 }
1368 out.push_str(&ind);
1369 out.push_str("public ");
1370 out.push_str(&type_name(pool, &d.ret));
1371 out.push(' ');
1372 out.push_str(&java_ident(&mname));
1373 out.push('(');
1374 for (i, a) in d.args.iter().enumerate() {
1375 if i > 0 {
1376 out.push_str(", ");
1377 }
1378 out.push_str(&type_name(pool, a));
1379 out.push_str(&format!(" p{}", i + 1));
1380 }
1381 out.push_str(") {\n");
1382 out.push_str(&ind);
1383 out.push_str(" throw new AbstractMethodError();\n");
1384 out.push_str(&ind);
1385 out.push_str("}\n");
1386 *emitted_any = true;
1387 }
1388}
1389
1390/// Synthetic storage fields backing name()/ordinal() on a FALLBACK enum
1391/// (a plain class cannot inherit java.lang.Enum's). The enum ctor's
1392/// (String, int) trace params feed them (injected in emit_method).
1393const ENUM_NAME_FIELD: &str = "$ddcName";
1394const ENUM_ORD_FIELD: &str = "$ddcOrdinal";
1395
1396/// True when the class renders as a fallback enum BASE: ACC_ENUM,
1397/// restoration failed (enum_consts None — caller-gated), super is the
1398/// Enum root. Constant subclasses inherit the synthesized members.
1399fn fallback_enum_base(class: &PoolClass) -> bool {
1400 class.is_enum()
1401 && class
1402 .super_name
1403 .as_ref()
1404 .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
1405 .unwrap_or(true)
1406}
1407
1408/// Full gate for the synthetic Enum surface: fallback base WITH the
1409/// trace ctor and no `$ddc*` field-name clash. The ctor injection in
1410/// emit_method, the member synthesis and the dex-valueOf suppression
1411/// must all agree, or fields/ctor/renders desynchronize.
1412fn enum_synth_active(class: &PoolClass) -> bool {
1413 fallback_enum_base(class)
1414 && enum_trace_ctor(class)
1415 && !class
1416 .static_fields
1417 .iter()
1418 .chain(class.instance_fields.iter())
1419 .any(|f| f.name == ENUM_NAME_FIELD || f.name == ENUM_ORD_FIELD)
1420}
1421
1422/// Does the class have the compiler-generated `(String, int, ..)` enum
1423/// ctor whose leading params feed the synthetic name/ordinal fields?
1424fn enum_trace_ctor(class: &PoolClass) -> bool {
1425 class.all_methods().any(|m| {
1426 &*m.name == "<init>"
1427 && m.parsed_desc()
1428 .map(|d| {
1429 matches!(d.args.first(), Some(JavaType::Object(s))
1430 if s.as_ref() == "java/lang/String")
1431 && matches!(d.args.get(1), Some(JavaType::Int))
1432 })
1433 .unwrap_or(false)
1434 })
1435}
1436
1437/// Synthesize the java.lang.Enum member surface a FALLBACK enum base
1438/// lacks (it renders as a plain class — `extends Enum` is illegal in
1439/// source): storage fields `$ddcName`/`$ddcOrdinal` (fed by the ctor
1440/// trace-param injection in emit_method), `name()`, `ordinal()`,
1441/// `valueOf(String)` and `compareTo`, plus `values()` when R8 stripped
1442/// the dex copy. Kills the illegal dex `Enum.valueOf(X.class, ..)`
1443/// ("无法将类 Enum<E>中的方法 valueOf应用到给定类型" — weixin ×1,307,
1444/// weibo ×477) and makes ordinal() EXACT (the interim values()-scan
1445/// misread enums whose $VALUES carries register-reuse duplicates,
1446/// protobuf ub). Purely additive: the dex never declares these members
1447/// on the enum class (they are Enum inheritances), so no collision —
1448/// except the dex valueOf, suppressed at the method loop.
1449fn synth_enum_members(
1450 pool: &DexPool,
1451 class: &PoolClass,
1452 out: &mut String,
1453 depth: usize,
1454 emitted_any: &mut bool,
1455) {
1456 use crate::access::*;
1457 if !fallback_enum_base(class) {
1458 return;
1459 }
1460 let self_ty = JavaType::Object(class.name.as_str().into());
1461 let self_name = type_name(pool, &self_ty);
1462 let self_arr = JavaType::Array(Box::new(self_ty));
1463 let arr_name = type_name(pool, &self_arr);
1464 let traced = enum_synth_active(class);
1465 let has_values = class.all_methods().any(|m| {
1466 &*m.name == "values"
1467 && m.access & ACC_STATIC != 0
1468 && m.parsed_desc()
1469 .map(|d| d.ret == self_arr && d.args.is_empty())
1470 .unwrap_or(false)
1471 });
1472 let ind = " ".repeat(depth + 1);
1473 let push_line = |out: &mut String, l: &str| {
1474 out.push_str(&ind);
1475 out.push_str(l);
1476 out.push('\n');
1477 };
1478 // values(): from the $VALUES array field when the dex copy is gone.
1479 if !has_values {
1480 let want = format!("[L{};", class.name);
1481 if let Some(vf) = class.static_fields.iter().find(|f| f.desc == want) {
1482 if *emitted_any {
1483 out.push('\n');
1484 }
1485 *emitted_any = true;
1486 push_line(out, &format!("public static {} values() {{", arr_name));
1487 push_line(
1488 out,
1489 &format!(" return ({}) {}.clone();", arr_name, java_ident(&vf.name)),
1490 );
1491 push_line(out, "}");
1492 }
1493 }
1494 if !traced {
1495 return; // no (String,int) ctor: fields would stay default — keep
1496 // the scan-based ordinal only (below) and the dex valueOf.
1497 }
1498 if *emitted_any {
1499 out.push('\n');
1500 }
1501 *emitted_any = true;
1502 push_line(out, &format!("private java.lang.String {};", ENUM_NAME_FIELD));
1503 push_line(out, &format!("private int {};", ENUM_ORD_FIELD));
1504 out.push('\n');
1505 push_line(out, &format!(
1506 "public java.lang.String name() {{\n{} return this.{};\n{}}}",
1507 ind, ENUM_NAME_FIELD, ind
1508 ));
1509 out.push('\n');
1510 push_line(out, &format!(
1511 "public int ordinal() {{\n{} return this.{};\n{}}}",
1512 ind, ENUM_ORD_FIELD, ind
1513 ));
1514 out.push('\n');
1515 push_line(out, &format!(
1516 "public int compareTo({0} o) {{\n{1} return this.{2} - o.{2};\n{1}}}",
1517 self_name, ind, ENUM_ORD_FIELD
1518 ));
1519 out.push('\n');
1520 push_line(out, &format!(
1521 "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}}}",
1522 self_name, ind
1523 ));
1524}
1525
1526/// Scan-based ordinal() fallback for fallback enums WITHOUT the trace
1527/// ctor (rare): position in values(). Kept separate so synth_enum_members
1528/// can bail after values() synthesis.
1529fn synth_enum_ordinal_scan(
1530 pool: &DexPool,
1531 class: &PoolClass,
1532 out: &mut String,
1533 depth: usize,
1534 emitted_any: &mut bool,
1535) {
1536 if !fallback_enum_base(class) || enum_trace_ctor(class) {
1537 return;
1538 }
1539 if class
1540 .all_methods()
1541 .any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
1542 {
1543 return;
1544 }
1545 let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
1546 let arr_name = type_name(pool, &self_arr);
1547 let ind = " ".repeat(depth + 1);
1548 if *emitted_any {
1549 out.push('\n');
1550 }
1551 *emitted_any = true;
1552 out.push_str(&ind);
1553 out.push_str("public int ordinal() {\n");
1554 out.push_str(&ind);
1555 out.push_str(&format!(" {} vs = values();\n", arr_name));
1556 out.push_str(&ind);
1557 out.push_str(" for (int i = 0; i < vs.length; i++) {\n");
1558 out.push_str(&ind);
1559 out.push_str(" if (vs[i] == this) {\n");
1560 out.push_str(&ind);
1561 out.push_str(" return i;\n");
1562 out.push_str(&ind);
1563 out.push_str(" }\n");
1564 out.push_str(&ind);
1565 out.push_str(" }\n");
1566 out.push_str(&ind);
1567 out.push_str(" return 0;\n");
1568 out.push_str(&ind);
1569 out.push_str("}\n");
1570}
1571
1572/// Strip the Meituan Robust hotpatch guard from a RESTORED enum ctor:
1573/// `Object[] v3 = {name, ordinal}; Class[] v4 = ..; boolean s =
1574/// PatchProxy.isSupport(v3, this, changeQuickRedirect, ..); if (s) {
1575/// PatchProxy.accessDispatch(..); return; }`. The guard reads the enum's
1576/// own static field — bytecode-legal (Robust instruments after javac),
1577/// source-illegal (JLS 8.9.2, both simple AND qualified forms). It is
1578/// dead code for decompiled output (no patch is ever loaded). Removal is
1579/// surgical: statements containing a PatchProxy call, the locals feeding
1580/// its arguments, their defs and element writes — a fixpoint over the
1581/// top-level list. collect_enum_constants' dex gate already refused
1582/// restoration when self-static reads go beyond the redirect field.
1583fn strip_robust_enum_ctor_guard(body: &mut Stmt) {
1584 let is_patchy = |e: &Expr| {
1585 matches!(e, Expr::Method { cls, name, .. }
1586 if cls.contains("PatchProxy")
1587 && (&**name == "isSupport"
1588 || &**name == "accessDispatch"
1589 || &**name == "proxy"
1590 || &**name == "isSupportClinit"
1591 || &**name == "accessDispatchClinit"))
1592 };
1593 let Stmt::Block(vs) = body else { return };
1594 if !vs.iter().any(|st| {
1595 let mut h = false;
1596 crate::passes::visit_all_exprs(st, &mut |x| {
1597 if is_patchy(x) {
1598 h = true;
1599 }
1600 });
1601 h
1602 }) {
1603 return;
1604 }
1605 let mut dead: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
1606 // Locals read inside the PatchProxy call args of a statement (the
1607 // pack arrays) — dead once the statement goes.
1608 let patchy_arg_locals = |st: &Stmt, out: &mut jdc_core::FxHashSet<u32>| {
1609 crate::passes::visit_all_exprs(st, &mut |x| {
1610 if let Expr::Method { args, .. } = x {
1611 if is_patchy(x) {
1612 for a in args {
1613 let mut aa = a.clone();
1614 crate::passes::deep_rewrite(&mut aa, &mut |y| {
1615 if let Expr::Local { var, .. } = y {
1616 out.insert(*var);
1617 }
1618 });
1619 }
1620 }
1621 }
1622 });
1623 };
1624 for _ in 0..4 {
1625 let before = vs.len();
1626 let mut i = 0usize;
1627 while i < vs.len() {
1628 let action = {
1629 let st = &vs[i];
1630 match st {
1631 Stmt::LocalDef { var, .. } if dead.contains(var) => Some((*var, false)),
1632 Stmt::LocalDef { var, init: Some(e), .. } => {
1633 let mut has_patchy = false;
1634 let mut ec = e.clone();
1635 crate::passes::deep_rewrite(&mut ec, &mut |x| {
1636 if is_patchy(x) {
1637 has_patchy = true;
1638 }
1639 });
1640 if has_patchy {
1641 Some((*var, true))
1642 } else {
1643 None
1644 }
1645 }
1646 Stmt::If { .. } => {
1647 let mut h = false;
1648 crate::passes::visit_all_exprs(st, &mut |x| {
1649 if is_patchy(x) {
1650 h = true;
1651 }
1652 });
1653 if h {
1654 Some((u32::MAX, true))
1655 } else {
1656 None
1657 }
1658 }
1659 // `v3[0] = str;` / `v3 = ..;` writes to a dead pack.
1660 Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1661 let mut tgt_dead = false;
1662 let mut tc = target.clone();
1663 crate::passes::deep_rewrite(&mut tc, &mut |x| {
1664 if let Expr::Local { var, .. } = x {
1665 if dead.contains(var) {
1666 tgt_dead = true;
1667 }
1668 }
1669 });
1670 if tgt_dead {
1671 Some((u32::MAX, false))
1672 } else {
1673 None
1674 }
1675 }
1676 _ => None,
1677 }
1678 };
1679 if let Some((var, collect_args)) = action {
1680 if collect_args {
1681 patchy_arg_locals(&vs[i], &mut dead);
1682 }
1683 if var != u32::MAX {
1684 dead.insert(var);
1685 }
1686 vs.remove(i);
1687 } else {
1688 i += 1;
1689 }
1690 }
1691 if vs.len() == before {
1692 break;
1693 }
1694 }
1695}
1696
1697#[allow(clippy::only_used_in_recursion)]
1698fn emit_class_body(
1699 pool: &DexPool,
1700 class: &PoolClass,
1701 ctx: &DexCtx<'_>,
1702 opts: &ClassOptions,
1703 out: &mut String,
1704 depth: usize,
1705) -> anyhow::Result<()> {
1706 let ind = indent(depth);
1707 // The class's own header must use the RENAMED display name — the
1708 // ctor path (is_init) already renames; a header declaring `class a`
1709 // while the file/ctor say `a_2` leaves the ctor looking like a
1710 // method with no return type (MinisApp's Y2.a vs y2.a package
1711 // case-collision: 2,518 javac parse errors).
1712 let cname = crate::apply_class_rename(&class.name);
1713 let (_, simple) = split_name(&cname);
1714 // A class emitted as its OWN top-level file (depth 0) must declare a
1715 // flat `$` name — `class Outer.Inner` is not declarable at file
1716 // scope. An INLINE nested member (depth > 0) declares its own
1717 // segment inside the parent's body.
1718 let simple = if depth == 0 {
1719 simple.to_string()
1720 } else {
1721 // R8 names can END in `$`: an empty last `$`-segment would
1722 // render `class {` — keep the whole simple name.
1723 let seg = simple.rsplit('$').next().unwrap_or(&simple);
1724 if seg.is_empty() {
1725 simple.to_string()
1726 } else {
1727 seg.to_string()
1728 }
1729 };
1730 // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
1731 // declared — restricted contextual type names escape here and in
1732 // the ctor, file name, and every type reference.
1733 let simple = if is_restricted_type_name(&simple) {
1734 format!("_{simple}")
1735 } else {
1736 simple
1737 };
1738 let is_iface = class.is_interface();
1739 let is_enum = class.is_enum();
1740
1741 let mut head = String::new();
1742 let a = class.access;
1743 if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
1744 head.push_str("public ");
1745 }
1746 // Inline nested members need their `static` (interfaces/annotations
1747 // are implicitly static; a missing `static` on a member class makes
1748 // every `new Report(...)` site an "outer instance required" error).
1749 if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
1750 head.push_str("static ");
1751 }
1752 if a & ACC_FINAL != 0 && !is_enum {
1753 head.push_str("final ");
1754 }
1755 if a & ACC_ABSTRACT != 0 && !is_iface {
1756 head.push_str("abstract ");
1757 }
1758 if a & ACC_ANNOTATION != 0 {
1759 head.push('@');
1760 }
1761 let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
1762 collect_enum_constants(pool, class, ctx)
1763 } else {
1764 None
1765 };
1766 if let Some(ecs) = &enum_consts {
1767 // True `enum` declaration: constants render in the header, the
1768 // desugared boilerplate (const fields, their <clinit> inits —
1769 // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
1770 // disappears. R8-renamed values()/valueOf() stay (they do not
1771 // collide with the compiler-generated ones); javac-named ones
1772 // are skipped at the method loop below.
1773 head.push_str("enum ");
1774 head.push_str(&sanitize_ref(&simple));
1775 let _ = ecs;
1776 } else if is_enum {
1777 // An enum with constant-specific bodies carries ACC_ABSTRACT —
1778 // `abstract final` is an illegal modifier combination; abstract
1779 // (already pushed above) suppresses the hardcoded final.
1780 let abstract_ = a & ACC_ABSTRACT != 0;
1781 head.push_str(if abstract_ {
1782 "/* enum */ class "
1783 } else {
1784 "/* enum */ final class "
1785 });
1786 head.push_str(&sanitize_ref(&simple));
1787 } else if is_iface {
1788 head.push_str("interface ");
1789 head.push_str(&sanitize_ref(&simple));
1790 } else {
1791 head.push_str("class ");
1792 head.push_str(&sanitize_ref(&simple));
1793 }
1794 // The obscured-super import was emitted at the package line; the
1795 // clause must render the SIMPLE name (the qualified form binds to
1796 // the class itself even with the import present).
1797 let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
1798 if is_iface {
1799 // A @interface cannot declare extends at all (JLS 9.6): the dex
1800 // interface table lists java/lang/annotation/Annotation for
1801 // every annotation type — rendering it produced "对于
1802 // @interfaces, 不允许 'extends'" (weibo 1100).
1803 let is_annot = a & ACC_ANNOTATION != 0;
1804 if !class.interfaces.is_empty() && !is_annot {
1805 head.push_str(" extends ");
1806 head.push_str(&join_dotted(pool, &class.interfaces));
1807 }
1808 } else {
1809 if let Some(sup) = &class.super_name {
1810 // Suppress `extends` for TRUE `enum` declarations (they
1811 // implicitly extend java.lang.Enum) and for the enum BASE in
1812 // fallback form (super IS java/lang/Enum — `class X extends
1813 // Enum` is illegal). But an ENUM CONSTANT SUBCLASS rendered
1814 // in fallback (`/* enum */ class j$1`, ACC_ENUM, super = the
1815 // enum base) MUST keep `extends j`: without it the constant
1816 // loses its subtype relation and every `field = new j$1(..)`
1817 // fails ("j$1无法转换为j", weibo jsoup TokeniserState ×172).
1818 let suppress_extends =
1819 is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
1820 if sup != "java/lang/Object" && !suppress_extends {
1821 head.push_str(" extends ");
1822 head.push_str(&render_super(sup));
1823 }
1824 }
1825 if !class.interfaces.is_empty() {
1826 head.push_str(if is_iface {
1827 " extends "
1828 } else {
1829 " implements "
1830 });
1831 head.push_str(&join_dotted(pool, &class.interfaces));
1832 }
1833 }
1834
1835 out.push_str(&ind);
1836 out.push_str(&head);
1837 out.push_str(" {\n");
1838
1839 // True-enum constant list: the constants lead the body, ahead of
1840 // any remaining fields.
1841 if let Some((ecs, _)) = &enum_consts {
1842 for (i, ec) in ecs.iter().enumerate() {
1843 out.push_str(&indent(depth + 1));
1844 out.push_str(&java_ident(&ec.name));
1845 if !ec.extra_args.is_empty() {
1846 let mut line = String::from("(");
1847 render_enum_args(ctx, pool, &ec.extra_args, &mut line);
1848 line.push(')');
1849 out.push_str(&line);
1850 }
1851 if i + 1 < ecs.len() {
1852 out.push_str(",\n");
1853 } else {
1854 out.push_str(";\n");
1855 }
1856 }
1857 out.push('\n');
1858 }
1859
1860 // Fields.
1861 // A static final whose dex-encoded static value is OVERWRITTEN by
1862 // the clinit must render as a blank final: the dex encoded array
1863 // value is just the class-load default (ART lets <clinit> reassign
1864 // static finals), but Java forbids touching an initialized final
1865 // ("无法为 static final 变量 $stable 分配值" — Compose `$stable = 0`
1866 // + clinit `= 8`; WalletBaseUI HARDCODE_* = 0 + clinit computed
1867 // value; the original null-valued shape: Kotlin `object` INSTANCE,
1868 // okio SegmentPool). Scan the clinit's raw SPuts once.
1869 let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
1870 // Gate: only classes with a VALUED final static can hit the
1871 // conflict — skip the clinit decode otherwise (decoding it for
1872 // every class cost seconds on 98k-class corpora).
1873 (|| {
1874 let has_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
1875 f.access & crate::access::ACC_FINAL != 0
1876 && class.static_values.get(i).is_some()
1877 });
1878 if !has_valued_final {
1879 return None;
1880 }
1881 let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
1882 let dex = pool.dex(m.dex_idx)?;
1883 let ci = dex.code_at(m.code_off)?;
1884 let mut set: jdc_core::FxHashSet<(
1885 std::sync::Arc<str>,
1886 std::sync::Arc<str>,
1887 )> = jdc_core::FxHashSet::default();
1888 for ins in &ci.insns {
1889 if let InsnKind::SPut { field_idx, .. } = ins.kind {
1890 let fid = dex.field(field_idx);
1891 // clinit only writes own-class fields here; match
1892 // (name, type) against the field list below.
1893 set.insert((
1894 std::sync::Arc::from(dex.string(fid.name_idx)),
1895 std::sync::Arc::from(dex.type_name(fid.type_idx)),
1896 ));
1897 }
1898 }
1899 Some(set)
1900 })();
1901 let mut field_emitted = false;
1902 for (i, f) in class.static_fields.iter().enumerate() {
1903 // Enum constant fields became the header list above.
1904 if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
1905 continue;
1906 }
1907 if field_emitted || !class.instance_fields.is_empty() {
1908 out.push('\n');
1909 }
1910 field_emitted = true;
1911 // A blank-final: static final, clinit-assigned. Interfaces are
1912 // excluded — their fields are implicitly `public static final`
1913 // and MUST carry an initializer.
1914 let blank_final = f.access & crate::access::ACC_FINAL != 0
1915 && !class.is_interface()
1916 && clinit_sputs
1917 .as_ref()
1918 .is_some_and(|s| {
1919 s.contains(&(
1920 std::sync::Arc::from(f.name.as_str()),
1921 std::sync::Arc::from(f.desc.as_str()),
1922 ))
1923 });
1924 emit_field(
1925 pool,
1926 f,
1927 if blank_final {
1928 None
1929 } else {
1930 class.static_values.get(i)
1931 },
1932 &class.name,
1933 out,
1934 depth + 1,
1935 true,
1936 class.is_interface(),
1937 );
1938 }
1939 if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1940 out.push('\n');
1941 }
1942 for (i, f) in class.instance_fields.iter().enumerate() {
1943 if i > 0 {
1944 out.push('\n');
1945 }
1946 emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1947 }
1948
1949 // Methods.
1950 // Duplicated (name, parameter-types) pairs in one class are javac
1951 // "method is already defined" errors (weibo: 11.5k hits). Source
1952 // cannot declare them, so exactly one survives: covariant bridges
1953 // (`Object get(int)` beside `ByteString get(int)` — the compiler
1954 // GENERATES bridges), plus R8 output that lost the bridge flag
1955 // (okio Buffer.clone, interface getView re-declarations). A
1956 // non-bridge method outranks a bridge for the same signature
1957 // (bridge bodies are delegation stubs); first occurrence otherwise.
1958 // Never drop a signature outright — the last method standing for a
1959 // key is always rendered.
1960 // Keyed on the DISPLAY name: a member-renamed method (return-type-
1961 // only collisions on isolated classes) no longer collides with its
1962 // sibling — both render, each caller resolving through the registry.
1963 fn sig_key<'m>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
1964 let d: &'m str = &m.desc;
1965 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1966 let hi = d.find(')').unwrap_or(d.len());
1967 let n = if renames_on {
1968 jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
1969 } else {
1970 &*m.name
1971 };
1972 (n, &d[lo..hi])
1973 }
1974 let renames_on = jdc_core::rename::member_rename_active();
1975 let suppress_dex_valueof = class.is_enum()
1976 && enum_consts.is_none()
1977 && enum_synth_active(class);
1978 let methods: Vec<&PoolMethod> = class.all_methods().collect();
1979 let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1980 for (i, m) in methods.iter().enumerate() {
1981 let key = sig_key(&class.name, renames_on, m);
1982 match claim.get(&key) {
1983 Some(&j)
1984 if m.access & crate::access::ACC_BRIDGE == 0
1985 && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1986 {
1987 claim.insert(key, i);
1988 }
1989 None => {
1990 claim.insert(key, i);
1991 }
1992 _ => {}
1993 }
1994 }
1995 // NOTE: a same-signature bridge is already retired by the claim map
1996 // above. An ERASURE-shaped bridge (SAM/variance: params are the
1997 // Object-erased types of a sibling's) must NOT be skipped: with raw
1998 // (non-generic) interface rendering the bridge is exactly what
1999 // satisfies the interface — skipping it turned every Kotlin lambda
2000 // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
2001 // +17), while the "对invoke的引用不明确" it appeared to fix was a
2002 // missing-kotlin-classpath cascade, not a ddc bug.
2003 let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
2004 for (i, m) in methods.iter().enumerate() {
2005 if &*m.name == "<clinit>" {
2006 continue; // rendered after the fields
2007 }
2008 if claim.get(&sig_key(&class.name, renames_on, m)) != Some(&i) {
2009 continue;
2010 }
2011 // Fallback enum base: the dex valueOf body is
2012 // `Enum.valueOf(X.class, str)` — illegal once X is a plain class
2013 // (T is not bounded by Enum). Suppress it; synth_enum_members
2014 // renders the values()-scan replacement.
2015 if suppress_dex_valueof
2016 && m.is_static()
2017 && &*m.name == "valueOf"
2018 && m.parsed_desc().map(|d| {
2019 d.args.len() == 1
2020 && matches!(d.args[0], JavaType::Object(ref sn)
2021 if sn.as_ref() == "java/lang/String")
2022 && d.ret == JavaType::Object(class.name.as_str().into())
2023 }).unwrap_or(false)
2024 {
2025 continue;
2026 }
2027 // True-enum rendering: javac auto-generates values()/valueOf()
2028 // — the original-named ones must not re-declare (R8-renamed
2029 // copies stay, they do not collide).
2030 if let Some((ecs, _)) = &enum_consts {
2031 let d = m.parsed_desc();
2032 let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
2033 let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
2034 let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
2035 let one_str = d
2036 .as_ref()
2037 .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
2038 .unwrap_or(false);
2039 let static_ = m.is_static();
2040 if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
2041 continue;
2042 }
2043 if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
2044 continue;
2045 }
2046 // Enum constructors must be private in source form.
2047 let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
2048 let mut c = (*m).clone();
2049 c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
2050 Some(c)
2051 } else {
2052 None
2053 };
2054 let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
2055 let mark = out.len();
2056 if emitted_any {
2057 out.push('\n');
2058 }
2059 if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
2060 emitted_any = true;
2061 } else {
2062 out.truncate(mark);
2063 }
2064 let _ = ecs;
2065 continue;
2066 }
2067 let mark = out.len();
2068 if emitted_any {
2069 out.push('\n');
2070 }
2071 if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
2072 emitted_any = true;
2073 } else {
2074 out.truncate(mark);
2075 }
2076 }
2077 // Inherited-ctor bridges: dex method refs resolve through the
2078 // hierarchy, so `new C(args)` against a class C with NO declared
2079 // <init> legally targets the SUPERCLASS ctor (weixin tenpay
2080 // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
2081 // has no inherited constructors: without a bridge, C's implicit
2082 // default ctor is the only one and every arg-carrying construction
2083 // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
2084 // Mirror the nearest ctor-declaring ancestor's public/protected
2085 // ctors as thin `super(..)` delegations.
2086 if !class.is_interface()
2087 && enum_consts.is_none()
2088 && !class.all_methods().any(|m| &*m.name == "<init>")
2089 {
2090 if let Some(mut sup) = class.super_name.clone() {
2091 loop {
2092 if sup == "java/lang/Object" {
2093 break;
2094 }
2095 let Some(sc) = pool.get(&sup) else { break };
2096 let ctors: Vec<&PoolMethod> = sc
2097 .all_methods()
2098 .filter(|m| &*m.name == "<init>")
2099 .collect();
2100 if !ctors.is_empty() {
2101 for sm in ctors {
2102 if sm.access & crate::access::ACC_PRIVATE != 0
2103 || (sm.access
2104 & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2105 == 0)
2106 {
2107 continue; // private/package-private: no legal bridge
2108 }
2109 let Some(d) = sm.parsed_desc() else { continue };
2110 let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2111 "public "
2112 } else {
2113 "protected "
2114 };
2115 if emitted_any {
2116 out.push('\n');
2117 }
2118 out.push_str(&format!(" {}", " ".repeat(depth)));
2119 out.push_str(mods);
2120 out.push_str(&sanitize_ref(&simple));
2121 out.push('(');
2122 let mut names = Vec::with_capacity(d.args.len());
2123 for (i, a) in d.args.iter().enumerate() {
2124 if i > 0 {
2125 out.push_str(", ");
2126 }
2127 out.push_str(&type_name(pool, a));
2128 let nm = format!("p{}", i + 1);
2129 out.push(' ');
2130 out.push_str(&nm);
2131 names.push(nm);
2132 }
2133 out.push_str(") {\n");
2134 out.push_str(&format!(" {} super({});\n", " ".repeat(depth), names.join(", ")));
2135 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2136 emitted_any = true;
2137 }
2138 break;
2139 }
2140 match &sc.super_name {
2141 Some(n) => sup = n.clone(),
2142 None => break,
2143 }
2144 }
2145 }
2146 }
2147
2148 // Referenced-but-undeclared inherited-ctor bridges. The block above
2149 // covers a class with NO declared ctor; this covers a class that
2150 // DECLARES ctors but is missing one that dex callers reach THROUGH
2151 // it. Dex resolves a `<init>` method-ref up the superclass chain, so
2152 // a subclass `super(2, recv, owner, name, sig, flags)` may target
2153 // `m.<init>(int,Object,Class,String,String,int)` that `m`
2154 // (FunctionReferenceImpl) does NOT declare — its super `l` does.
2155 // Java has no inherited ctors, so without a bridge on `m` every such
2156 // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
2157 // referenced sig C lacks from the nearest ancestor declaring it, as a
2158 // thin `super(..)` delegation. Ref-gated (only sigs actually called
2159 // through C) so ordinary classes gain nothing.
2160 if !class.is_interface()
2161 && enum_consts.is_none()
2162 && class.all_methods().any(|m| &*m.name == "<init>")
2163 {
2164 if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2165 fn arg_sig(d: &str) -> &str {
2166 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2167 let hi = d.find(')').unwrap_or(d.len());
2168 &d[lo..hi]
2169 }
2170 let declared: jdc_core::FxHashSet<&str> = class
2171 .all_methods()
2172 .filter(|m| &*m.name == "<init>")
2173 .map(|m| arg_sig(&m.desc))
2174 .collect();
2175 let mut missing: Vec<&String> = refs
2176 .iter()
2177 .filter(|s| !declared.contains(s.as_str()))
2178 .collect();
2179 missing.sort();
2180 for sig in missing {
2181 // Nearest ancestor declaring a public/protected <init>
2182 // with this exact arg signature.
2183 let mut sup = class.super_name.clone();
2184 let mut found: Option<&PoolMethod> = None;
2185 while let Some(s) = sup {
2186 if s == "java/lang/Object" {
2187 break;
2188 }
2189 let Some(sc) = pool.get(&s) else { break };
2190 if let Some(sm) = sc.all_methods().find(|m| {
2191 &*m.name == "<init>"
2192 && arg_sig(&m.desc) == sig.as_str()
2193 && m.access & crate::access::ACC_PRIVATE == 0
2194 && m.access
2195 & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2196 != 0
2197 }) {
2198 found = Some(sm);
2199 break;
2200 }
2201 sup = sc.super_name.clone();
2202 }
2203 let Some(sm) = found else {
2204 // The walk terminated at java/lang/Object — which
2205 // always declares <init>()V, so an empty-sig ref
2206 // through this class resolved there dex-wise. The
2207 // Java implicit default ctor is gone (the class
2208 // declares other ctors); mirror it as an explicit
2209 // public bridge (AgentMenuTask$Request →
2210 // AppBrandProxyUIProcessTask$ProcessRequest:
2211 // "需要: Parcel 找到: 没有参数", weixin 205).
2212 if sig.is_empty() {
2213 if emitted_any {
2214 out.push('\n');
2215 }
2216 out.push_str(&format!(" {}", " ".repeat(depth)));
2217 out.push_str("public ");
2218 out.push_str(&sanitize_ref(&simple));
2219 out.push_str("() {\n");
2220 out.push_str(&format!(
2221 " {} super();\n",
2222 " ".repeat(depth)
2223 ));
2224 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2225 emitted_any = true;
2226 }
2227 continue;
2228 };
2229 let Some(d) = sm.parsed_desc() else { continue };
2230 let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2231 "public "
2232 } else {
2233 "protected "
2234 };
2235 if emitted_any {
2236 out.push('\n');
2237 }
2238 out.push_str(&format!(" {}", " ".repeat(depth)));
2239 out.push_str(mods);
2240 out.push_str(&sanitize_ref(&simple));
2241 out.push('(');
2242 let mut names = Vec::with_capacity(d.args.len());
2243 for (i, a) in d.args.iter().enumerate() {
2244 if i > 0 {
2245 out.push_str(", ");
2246 }
2247 out.push_str(&type_name(pool, a));
2248 let nm = format!("p{}", i + 1);
2249 out.push(' ');
2250 out.push_str(&nm);
2251 names.push(nm);
2252 }
2253 out.push_str(") {\n");
2254 out.push_str(&format!(
2255 " {} super({});\n",
2256 " ".repeat(depth),
2257 names.join(", ")
2258 ));
2259 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2260 emitted_any = true;
2261 }
2262 }
2263 }
2264
2265 // Framework-ancestor ctor bridge. The two blocks above mirror POOL
2266 // ancestors; both stop at a framework (off-pool) superclass. A class
2267 // whose DIRECT superclass is a framework type (in android.jar) that
2268 // declares a ctor the class lacks also needs a bridge — `super(args)`
2269 // resolves against the jar. Only the DIRECT-super-is-framework case is
2270 // safe: an intermediate pool ancestor with no matching ctor would
2271 // break the `super()` chain (that ancestor needs its own bridge, which
2272 // its own refs may not trigger). Covers weixin g3 extends
2273 // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
2274 // the custom-exception / Parcelable family the pool walk can't reach.
2275 if !class.is_interface() && enum_consts.is_none() {
2276 let direct_fw = class
2277 .super_name
2278 .as_ref()
2279 .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
2280 .unwrap_or(false);
2281 if direct_fw {
2282 if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2283 fn arg_sig(d: &str) -> &str {
2284 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2285 let hi = d.find(')').unwrap_or(d.len());
2286 &d[lo..hi]
2287 }
2288 let declared: jdc_core::FxHashSet<&str> = class
2289 .all_methods()
2290 .filter(|m| &*m.name == "<init>")
2291 .map(|m| arg_sig(&m.desc))
2292 .collect();
2293 let mut missing: Vec<&String> = refs
2294 .iter()
2295 .filter(|s| !declared.contains(s.as_str()))
2296 .collect();
2297 missing.sort();
2298 for sig in missing {
2299 // Never bridge the empty sig here: a no-ctor class
2300 // gets Java's implicit default, and a class WITH
2301 // ctors is covered by the referenced-but-undeclared
2302 // block's Object-walk tail (emitting in both was
2303 // `已在类 b0中定义了构造器 b0()` ×6 weixin).
2304 if sig.is_empty() {
2305 continue;
2306 }
2307 let args = split_arg_descs(sig);
2308 if emitted_any {
2309 out.push('\n');
2310 }
2311 out.push_str(&format!(" {}", " ".repeat(depth)));
2312 out.push_str("public ");
2313 out.push_str(&sanitize_ref(&simple));
2314 out.push('(');
2315 let mut names = Vec::with_capacity(args.len());
2316 for (i, a) in args.iter().enumerate() {
2317 if i > 0 {
2318 out.push_str(", ");
2319 }
2320 out.push_str(&type_name(pool, a));
2321 let nm = format!("p{}", i + 1);
2322 out.push(' ');
2323 out.push_str(&nm);
2324 names.push(nm);
2325 }
2326 out.push_str(") {\n");
2327 out.push_str(&format!(
2328 " {} super({});\n",
2329 " ".repeat(depth),
2330 names.join(", ")
2331 ));
2332 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2333 emitted_any = true;
2334 }
2335 }
2336 }
2337 }
2338
2339 // Throwable-family ctor bridge for empty-shell exception classes
2340 // (see throwable_ctor_sigs). Walks the pool chain to the nearest
2341 // FRAMEWORK ancestor, stopping at any pool ancestor that DECLARES
2342 // ctors (the mirror block above covers that shape) — so chains of
2343 // empty shells (C extends P extends Error) each get the bridges and
2344 // every super(..) resolves against its parent's bridge.
2345 if !class.is_interface()
2346 && enum_consts.is_none()
2347 && !class.all_methods().any(|m| &*m.name == "<init>")
2348 {
2349 let mut fw_anc: Option<&str> = None;
2350 if let Some(sup) = class.super_name.as_deref() {
2351 let mut cur = sup;
2352 let mut hops = 0;
2353 loop {
2354 hops += 1;
2355 if hops > 32 || cur == "java/lang/Object" {
2356 break;
2357 }
2358 match pool.get(cur) {
2359 Some(sc) => {
2360 if sc.all_methods().any(|m| &*m.name == "<init>") {
2361 break;
2362 }
2363 match sc.super_name.as_deref() {
2364 Some(n) => cur = n,
2365 None => break,
2366 }
2367 }
2368 None => {
2369 fw_anc = Some(cur);
2370 break;
2371 }
2372 }
2373 }
2374 }
2375 if let Some(sigs) = fw_anc.and_then(throwable_ctor_sigs) {
2376 // The framework-ancestor bridge above already emitted every
2377 // sig the dex method-ref table attributes to this class —
2378 // emitting the table copy too was `已在类 z中定义了构造器
2379 // z(String)` (+60 weixin dup-def). The empty sig is exempt
2380 // there, so the table keeps it.
2381 let ref_sigs = pool.ctor_ref_sigs(&class.name);
2382 for sig in sigs {
2383 if !sig.is_empty()
2384 && ref_sigs.is_some_and(|rs| rs.contains(*sig))
2385 {
2386 continue;
2387 }
2388 let args = split_arg_descs(sig);
2389 if emitted_any {
2390 out.push('\n');
2391 }
2392 out.push_str(&format!(" {}", " ".repeat(depth)));
2393 out.push_str("public ");
2394 out.push_str(&sanitize_ref(&simple));
2395 out.push('(');
2396 let mut names = Vec::with_capacity(args.len());
2397 for (i, a) in args.iter().enumerate() {
2398 if i > 0 {
2399 out.push_str(", ");
2400 }
2401 out.push_str(&type_name(pool, a));
2402 let nm = format!("p{}", i + 1);
2403 out.push(' ');
2404 out.push_str(&nm);
2405 names.push(nm);
2406 }
2407 out.push_str(") {\n");
2408 out.push_str(&format!(
2409 " {} super({});\n",
2410 " ".repeat(depth),
2411 names.join(", ")
2412 ));
2413 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
2414 emitted_any = true;
2415 }
2416 }
2417 }
2418
2419 // Static resolver helpers extracted from branched-delegation ctors
2420 // (passes::extract_branched_delegation_helper) — the computation the
2421 // Java first-statement rule cannot host inline.
2422 for h in crate::passes::take_ctor_helpers(&class.name) {
2423 if emitted_any {
2424 out.push('\n');
2425 }
2426 out.push_str(&indent(depth + 1));
2427 out.push_str("private static ");
2428 out.push_str(&type_name(pool, &h.ret));
2429 out.push(' ');
2430 out.push_str(&h.name);
2431 out.push('(');
2432 for (i, (ty, nm)) in h.params.iter().enumerate() {
2433 if i > 0 {
2434 out.push_str(", ");
2435 }
2436 out.push_str(&type_name(pool, ty));
2437 out.push(' ');
2438 out.push_str(&java_ident(nm));
2439 }
2440 out.push_str(") {\n");
2441 let p = Printer::new(ctx, &h.vt);
2442 let text = p.with_indent(depth + 2).into_string(&h.body);
2443 for line in text.lines() {
2444 if line.trim().is_empty() {
2445 continue;
2446 }
2447 out.push_str(line);
2448 out.push('\n');
2449 }
2450 out.push_str(&indent(depth + 1));
2451 out.push_str("}\n");
2452 emitted_any = true;
2453 }
2454
2455 // Missing-abstract-method stubs (R8 tree-shook an interface method a
2456 // concrete class no longer implements; javac rejects the incomplete
2457 // class). Synthesized last so they sit after the real members.
2458 synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
2459
2460 // Fallback enum base: recover the Enum member surface the
2461 // plain-class rendering lacks (name/ordinal/valueOf/compareTo +
2462 // synthetic storage fields fed by the ctor injection).
2463 if class.is_enum() && enum_consts.is_none() {
2464 synth_enum_members(pool, class, out, depth, &mut emitted_any);
2465 synth_enum_ordinal_scan(pool, class, out, depth, &mut emitted_any);
2466 }
2467
2468 // Static initializer. INTERFACES cannot carry a `static { }` block in
2469 // Java — their clinit only assigns constants, which static_values (or
2470 // the `= null` default) already render as field initializers; skip
2471 // the block entirely.
2472 let skip_clinit = class.is_interface();
2473 if let Some((_, clinit_body)) = enum_consts.take() {
2474 // True-enum <clinit>: the constant assignments are already gone;
2475 // render the remainder (the $VALUES array build) directly.
2476 // Skip an empty remainder (all statements were constant inits).
2477 let empty = match &clinit_body.body {
2478 Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
2479 _ => false,
2480 };
2481 if !empty {
2482 if emitted_any {
2483 out.push('\n');
2484 }
2485 out.push_str(&indent(depth + 1));
2486 out.push_str("static {\n");
2487 let p = Printer::new(ctx, &clinit_body.vt);
2488 let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
2489 for line in text.lines() {
2490 if line.trim().is_empty() {
2491 continue;
2492 }
2493 out.push_str(line);
2494 out.push('\n');
2495 }
2496 out.push_str(&indent(depth + 1));
2497 out.push_str("}\n");
2498 emitted_any = true;
2499 }
2500 } else if let Some(clinit) = (!skip_clinit)
2501 .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
2502 .flatten()
2503 {
2504 let mark = out.len();
2505 if emitted_any {
2506 out.push('\n');
2507 }
2508 if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
2509 emitted_any = true;
2510 } else {
2511 out.truncate(mark);
2512 }
2513 }
2514
2515 // Nested member classes (clean `$` tails only; anonymous/local/lambda
2516 // classes are emitted as their own top-level files by the driver).
2517 for nested in nested_members(pool, class, ctx) {
2518 if emitted_any {
2519 out.push('\n');
2520 }
2521 let nested_ctx = DexCtx::new(pool, nested);
2522 out.push('\n');
2523 emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
2524 emitted_any = true;
2525 }
2526
2527 out.push_str(&ind);
2528 out.push_str("}\n");
2529 Ok(())
2530}
2531
2532/// Member classes of `class` (direct children by the `$` chain / member
2533/// annotations), excluding anonymous / local / lambda shapes.
2534fn nested_members<'a>(
2535 pool: &'a DexPool,
2536 class: &'a PoolClass,
2537 ctx: &DexCtx<'_>,
2538) -> Vec<&'a PoolClass> {
2539 let mut out = Vec::new();
2540 for name in pool.children_of(&class.name) {
2541 let Some(pc) = pool.get(name) else { continue };
2542 // Only a REAL `outer$tail` name is an inline member: the
2543 // children index also carries annotation-derived outers
2544 // (EnclosingClass) with no naming relationship to the child
2545 // (obfuscated apps pair a 1-char name with a long enclosing
2546 // descriptor) — the blind slice panicked there (alipay
2547 // `a.a.a.a.c`, exposed once children_of actually returned data).
2548 let Some(rest) = name
2549 .strip_prefix(class.name.as_str())
2550 .and_then(|t| t.strip_prefix('$'))
2551 else {
2552 continue;
2553 };
2554 if rest.is_empty() || rest.starts_with('-') {
2555 continue;
2556 }
2557 if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
2558 continue;
2559 }
2560 let tail = rest.rsplit('$').next().unwrap_or(rest);
2561 if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
2562 continue; // anonymous
2563 }
2564 if tail.starts_with(|c: char| c.is_ascii_digit()) {
2565 continue; // local
2566 }
2567 out.push(pc);
2568 }
2569 out
2570}
2571
2572// Eight parameters are all load-bearing (pool, field, static value,
2573// owner class, output, depth, staticness, interface-init requirement);
2574// bundling them into a struct would obscure each call site.
2575#[allow(clippy::too_many_arguments)]
2576fn emit_field(
2577 pool: &DexPool,
2578 f: &crate::PoolField,
2579 init: Option<&StaticValue>,
2580 f_class: &str,
2581 out: &mut String,
2582 depth: usize,
2583 is_static: bool,
2584 require_init: bool,
2585) {
2586 let ind = indent(depth);
2587 let mut line = String::new();
2588 let a = f.access;
2589 if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
2590 line.push_str("public ");
2591 } else if a & ACC_PRIVATE != 0 {
2592 line.push_str("private ");
2593 } else if a & ACC_PROTECTED != 0 {
2594 line.push_str("protected ");
2595 }
2596 if is_static {
2597 line.push_str("static ");
2598 }
2599 if a & ACC_FINAL != 0 && !unfinal_field(f_class, &f.name) {
2600 line.push_str("final ");
2601 }
2602 if a & ACC_SYNTHETIC != 0 {
2603 line.push_str("/* synthetic */ ");
2604 }
2605 if a & ACC_TRANSIENT_HINT != 0 {
2606 line.push_str("transient ");
2607 }
2608 if a & ACC_VOLATILE_HINT != 0 {
2609 line.push_str("volatile ");
2610 }
2611 let ty = type_name(pool, &desc_type(&f.desc));
2612 line.push_str(&ty);
2613 line.push(' ');
2614 let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
2615 line.push_str(&java_ident(fname));
2616 let mut rendered = None;
2617 if let Some(v) = init {
2618 rendered = render_static_value(pool, v, f_class);
2619 }
2620 if rendered.is_none() && require_init {
2621 // Interface fields MUST have an initializer in Java; the dex may
2622 // not carry a static_values entry for a compile-time-constant the
2623 // compiler folded away. Keep it compilable.
2624 let default = match desc_type(&f.desc) {
2625 JavaType::Boolean => "false",
2626 JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
2627 JavaType::Long => "0L",
2628 JavaType::Float => "0.0F",
2629 JavaType::Double => "0.0",
2630 _ => "null",
2631 };
2632 rendered = Some(default.to_string());
2633 }
2634 if let Some(text) = rendered {
2635 // A long field holding an `Int(i64)` static value needs the `L`
2636 // suffix: without it the literal is an int and overflows
2637 // (`long d = -6343169151696340687` failed javac).
2638 let text = if matches!(desc_type(&f.desc), JavaType::Long)
2639 && text.chars().all(|c| c.is_ascii_digit() || c == '-')
2640 {
2641 format!("{text}L")
2642 } else {
2643 text
2644 };
2645 line.push_str(" = ");
2646 line.push_str(&text);
2647 }
2648 line.push(';');
2649 out.push_str(&ind);
2650 out.push_str(&line);
2651 out.push('\n');
2652}
2653
2654// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
2655// volatile(0x40)/transient(0x80) for fields.
2656pub const ACC_VOLATILE_HINT: u32 = 0x40;
2657pub const ACC_TRANSIENT_HINT: u32 = 0x80;
2658
2659fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
2660 Some(match v {
2661 StaticValue::Int(i) => i.to_string(),
2662 StaticValue::Float(f) => format_float(*f as f64, true),
2663 StaticValue::Double(d) => format_float(*d, false),
2664 StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
2665 StaticValue::Type(t) => format!("{}.class", dotted(t)),
2666 StaticValue::Boolean(b) => b.to_string(),
2667 StaticValue::Null => "null".into(),
2668 StaticValue::Field(cls, name, desc) => {
2669 // The rename registry owns the display name (obscuring-field
2670 // renames key on (owner, name, desc)) — a const field ref must
2671 // render the SAME name as the declaration.
2672 let n: std::borrow::Cow<str> = match jdc_core::rename::field_display(cls, name, desc) {
2673 Some(d) => std::borrow::Cow::Borrowed(d),
2674 None => java_ident(name),
2675 };
2676 if cls == owner {
2677 n.into_owned()
2678 } else {
2679 format!("{}.{}", print_class_name(pool, cls), n)
2680 }
2681 }
2682 StaticValue::Other => return None,
2683 })
2684}
2685
2686/// Render one method straight into the class buffer `out`. Returns
2687/// whether anything was written (skips: no descriptor, deferred to a
2688/// monitored thread). The old shape returned a per-method String that
2689/// the caller copied in — one extra full copy of every method body.
2690/// `enum_promoted`: the class rendered as a true `enum` declaration
2691/// (constants in the header), which changes what a ctor signature may
2692/// declare.
2693fn emit_method(
2694 pool: &DexPool,
2695 class: &PoolClass,
2696 ctx: &DexCtx<'_>,
2697 m: &PoolMethod,
2698 depth: usize,
2699 enum_promoted: bool,
2700 out: &mut String,
2701) -> anyhow::Result<bool> {
2702 let ind = indent(depth);
2703 let desc = m.parsed_desc();
2704
2705 // Signature.
2706 let mut sig = String::with_capacity(192);
2707 let a = m.access;
2708 if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
2709 sig.push_str("public ");
2710 } else if a & ACC_PRIVATE != 0 {
2711 // A fallback enum BASE (`/* enum */ class`, not a promoted
2712 // `enum`) renders its constant-specific-body subclasses as
2713 // SEPARATE same-package classes (`j$6 extends j`); their ctors
2714 // call `super(name, ordinal, ..)`, which a PRIVATE base ctor
2715 // forbids ("j(String,int) has private access"). Widen the base
2716 // ctor to package-private so the subclass links. The base is
2717 // abstract → no external instantiation risk. Only the BASE
2718 // (super is Enum/Object), not the constant subclass itself.
2719 let fallback_enum_base_ctor = class.is_enum()
2720 && !enum_promoted
2721 && &*m.name == "<init>"
2722 && class
2723 .super_name
2724 .as_ref()
2725 .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
2726 .unwrap_or(true);
2727 if !fallback_enum_base_ctor {
2728 sig.push_str("private ");
2729 }
2730 } else if a & ACC_PROTECTED != 0 {
2731 sig.push_str("protected ");
2732 }
2733 let is_clinit = &*m.name == "<clinit>";
2734 let is_init = &*m.name == "<init>";
2735 if a & ACC_STATIC != 0 || is_clinit {
2736 sig.push_str("static ");
2737 }
2738 if a & ACC_FINAL != 0 {
2739 sig.push_str("final ");
2740 }
2741 // `abstract synchronized` is an illegal combination — obfuscated
2742 // builds mark abstract bridges synchronized (WhatsApp
2743 // SQLiteOpenHelper).
2744 if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
2745 sig.push_str("synchronized ");
2746 }
2747 if a & ACC_NATIVE != 0 {
2748 sig.push_str("native ");
2749 }
2750 if a & ACC_ABSTRACT != 0 {
2751 sig.push_str("abstract ");
2752 }
2753 if a & ACC_SYNTHETIC != 0 {
2754 sig.push_str("/* synthetic */ ");
2755 }
2756
2757 // Body (needed for parameter names even for abstract methods).
2758 let mut body = decompile_method(pool, class, m).ok().flatten();
2759 // An interface method WITH a body is a `default` method (JLS 9.4.3)
2760 // unless static/private — dex carries no `default` flag, so the
2761 // plain form rendered an abstract signature with a body and javac
2762 // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
2763 // default REQUIRES a body: only push it once the body is confirmed
2764 // (a failed decompile renders a body-less declaration).
2765 if class.is_interface()
2766 && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
2767 && body.is_some()
2768 {
2769 sig.insert_str(0, "default ");
2770 }
2771 let param_names: Vec<String> = body
2772 .as_ref()
2773 .map(|b| {
2774 let mut ps: Vec<(u16, String)> =
2775 b.vt.vars
2776 .iter()
2777 .filter(|v| v.is_param && v.name != "this")
2778 .map(|v| (v.slot, v.name.clone()))
2779 .collect();
2780 ps.sort_by_key(|(s, _)| *s);
2781 ps.into_iter().map(|(_, n)| n).collect()
2782 })
2783 .unwrap_or_else(|| {
2784 (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
2785 .map(|i| format!("p{}", i + 1))
2786 .collect()
2787 });
2788
2789 // Non-static member-inner ctor: the synthetic outer instance rides
2790 // as args[0] (typed as the direct enclosing class). Every emission
2791 // site already passes it implicitly — qualified `outer.new Inner(..)`,
2792 // `this.new Inner(..)` from inside the outer, `super(..)` after
2793 // skip_outer_arg — so the signature drops the param and the body's
2794 // references become `Outer.this`. Without this, every construction
2795 // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
2796 // guava androidx inner-class families, d8 capture lambdas).
2797 let mut inner_arg0 = 0usize;
2798 if is_init {
2799 let tail = class.name.rsplit('$').next().unwrap_or("");
2800 let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
2801 if let Some(d) = desc.as_ref() {
2802 if let Some(JavaType::Object(outer)) = d.args.first() {
2803 // The direct enclosing class: nesting annotation when
2804 // present, else the `$`-chain parent (find_outer_name —
2805 // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
2806 // a plain rsplit leaves a trailing `$`). It must agree
2807 // with the this$0 type for the rewrite to fire.
2808 let enclosing: Option<String> = class
2809 .nesting
2810 .enclosing_class
2811 .clone()
2812 .or_else(|| crate::find_outer_name(pool, &class.name));
2813 let direct = enclosing.as_deref() == Some(outer.as_ref());
2814 if direct && !digit_simple && ctx.class_has_this0(&class.name) {
2815 let param0 = body.as_ref().and_then(|b| {
2816 b.vt.vars
2817 .iter()
2818 .find(|v| v.is_param && v.name != "this")
2819 .map(|v| v.id)
2820 });
2821 let written = match (&body, param0) {
2822 (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
2823 _ => false,
2824 };
2825 if let (Some(p0), false) = (param0, written) {
2826 if let Some(b) = body.as_mut() {
2827 // this()-delegations to the SAME class drop the
2828 // outer arg (all of its ctors share the strip);
2829 // a super target joins when it is an inner of
2830 // the same enclosing family.
2831 let mut eligible: Vec<String> = vec![class.name.clone()];
2832 if let Some(sup) = &class.super_name {
2833 let sup_enclosing =
2834 crate::find_outer_name(pool, sup);
2835 if sup_enclosing.as_deref() == Some(outer.as_ref())
2836 && ctx.class_has_this0(sup)
2837 {
2838 eligible.push(sup.clone());
2839 }
2840 }
2841 let renamed = crate::apply_class_rename(outer);
2842 let display = dotted(renamed.as_ref());
2843 let ty =
2844 jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
2845 crate::passes::rewrite_inner_ctor_outer_param(
2846 &mut b.body,
2847 p0,
2848 &display,
2849 &ty,
2850 &eligible,
2851 );
2852 inner_arg0 = 1;
2853 }
2854 }
2855 }
2856 }
2857 }
2858 }
2859
2860 if is_clinit {
2861 // `static { ... }` — the caller strips the method name/params.
2862 } else {
2863 let Some(d) = &desc else { return Ok(false) };
2864 if is_init {
2865 // The ctor name must equal the DECLARED class name of its
2866 // file: flat `$` at depth 0 (own file), own segment when
2867 // inlined in the parent at depth > 0. Case-renamed classes
2868 // use their display name.
2869 let dname = crate::apply_class_rename(&class.name);
2870 let (_, simple) = split_name(&dname);
2871 // emit_method's depth is the METHOD indent = class depth + 1:
2872 // own-file classes (depth 0 header → method depth 1) need the
2873 // flat `$` ctor name; inline nested members use their segment.
2874 let base = if depth <= 1 {
2875 simple.to_string()
2876 } else {
2877 // R8 names can END in `$` (`ThreadMsg$$$`): the last
2878 // `$`-segment is empty — keep the whole simple name.
2879 let seg = simple.rsplit('$').next().unwrap_or(&simple);
2880 if seg.is_empty() {
2881 simple.to_string()
2882 } else {
2883 seg.to_string()
2884 }
2885 };
2886 let name = if is_restricted_type_name(&base) {
2887 format!("_{base}")
2888 } else {
2889 base
2890 };
2891 // The ctor name must equal the DECLARED class name — the same
2892 // injective `_u<hex>` sanitizer the header uses (java_ident's
2893 // lossy `-`→`_` fold left `class _u2dDeprecatedOkio` with a
2894 // `private _DeprecatedOkio()` ctor: "方法声明无效; 需要返回
2895 // 类型", which as a PARSE error also masks every semantic
2896 // error in the whole javac run).
2897 sig.push_str(&sanitize_ref(&name));
2898 } else {
2899 sig.push_str(&type_name(pool, &d.ret));
2900 sig.push(' ');
2901 let mname =
2902 jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
2903 sig.push_str(&java_ident(mname));
2904 }
2905 sig.push('(');
2906 // A promoted enum ctor's dex descriptor carries the compiler-
2907 // synthesized `(String name, int ordinal)` prefix — JLS forbids
2908 // declaring those (they are implicit in the `A(args)` constant
2909 // declarations the promotion emits, and strip_enum_ctor_super
2910 // already removed the `super(name, ordinal, ..)` delegation).
2911 // Skip the pair in the signature, or every constant declaration
2912 // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
2913 // when the body never reads the two params — code that genuinely
2914 // uses them keeps the declared form.
2915 // Robust hotpatch guard strip MUST run before the trace-param
2916 // analysis: the guard's isSupport(..) call is the ctor's only
2917 // reader of the (String, int) params for plain robust enums, and
2918 // its self-static read is what JLS 8.9.2 forbids.
2919 if enum_promoted && is_init {
2920 if let Some(b) = body.as_mut() {
2921 strip_robust_enum_ctor_guard(&mut b.body);
2922 }
2923 }
2924 // Fallback enum base ctor: store the (String, int) trace params
2925 // into the synthetic $ddcName/$ddcOrdinal fields that back the
2926 // synthesized name()/ordinal()/valueOf() (synth_enum_members).
2927 // Non-final fields: constant-subclass ctors reach this store
2928 // through their super(..) chain, and a `final` would demand
2929 // definite assignment on every ctor path.
2930 if is_init
2931 && !enum_promoted
2932 && class.is_enum()
2933 && enum_synth_active(class)
2934 && matches!(d.args.first(), Some(JavaType::Object(sx)) if sx.as_ref() == "java/lang/String")
2935 && matches!(d.args.get(1), Some(JavaType::Int))
2936 {
2937 if let Some(b) = body.as_mut() {
2938 // A ctor that DELEGATES first (the -IA synthetic bridge,
2939 // `this(str, p2)`) must stay delegation-first — the
2940 // delegated-to ctor performs the stores.
2941 let starts_with_delegation = match &b.body {
2942 Stmt::Block(vs) => vs.first().is_some_and(crate::passes::is_bare_ctor_call),
2943 other => crate::passes::is_bare_ctor_call(other),
2944 };
2945 let ps: Vec<u32> = b
2946 .vt
2947 .vars
2948 .iter()
2949 .filter(|v| v.is_param && v.name != "this")
2950 .take(2)
2951 .map(|v| v.id)
2952 .collect();
2953 if ps.len() == 2 && !starts_with_delegation {
2954 let mk_store = |var: u32, fname: &str, fty: JavaType| {
2955 Stmt::ExprStmt(Expr::Assign {
2956 target: Box::new(Expr::Field {
2957 owner: Some(Box::new(Expr::This)),
2958 cls: std::sync::Arc::from(class.name.as_str()),
2959 name: std::sync::Arc::from(fname),
2960 ty: jdc_core::ir::expr::TypeRef::J(fty.clone()),
2961 is_static: false,
2962 }),
2963 op: jdc_core::ir::expr::AssignOp::Plain,
2964 value: Box::new(Expr::Local {
2965 var,
2966 ty: jdc_core::ir::expr::TypeRef::J(fty),
2967 }),
2968 })
2969 };
2970 let mut inj = vec![
2971 mk_store(ps[0], ENUM_NAME_FIELD, JavaType::Object("java/lang/String".into())),
2972 mk_store(ps[1], ENUM_ORD_FIELD, JavaType::Int),
2973 ];
2974 if let Stmt::Block(vs) = &mut b.body {
2975 let mut rest = std::mem::take(vs);
2976 inj.append(&mut rest);
2977 *vs = inj;
2978 } else {
2979 let prev = std::mem::replace(&mut b.body, Stmt::Block(Vec::new()));
2980 inj.push(prev);
2981 b.body = Stmt::Block(inj);
2982 }
2983 }
2984 }
2985 }
2986 let mut arg0 = 0;
2987 if enum_promoted
2988 && is_init
2989 && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
2990 && matches!(d.args.get(1), Some(JavaType::Int))
2991 {
2992 // The Kotlin default-arg bridge ctor reads name/ordinal in
2993 // its `this(str, p2, ..)` delegation only — the LEADING pair
2994 // of a this()-delegation drops together with the params, so
2995 // those reads do not block the strip (the constants' extra
2996 // args match the bridge's user params, not the 2-param user
2997 // ctor).
2998 let synthetic: Vec<u32> = body
2999 .as_ref()
3000 .map(|b| {
3001 b.vt
3002 .vars
3003 .iter()
3004 .filter(|v| v.is_param && v.name != "this")
3005 .take(2)
3006 .map(|v| v.id)
3007 .collect()
3008 })
3009 .unwrap_or_default();
3010 // The delegation-leading extension only applies to the
3011 // Kotlin default-arg BRIDGE: its descriptor ends with
3012 // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
3013 // ctor whose first two params are (String, int) and merely
3014 // forwards them must keep its signature (weixin +2.9k when
3015 // ungated).
3016 // Marker-name-free bridge shape: (.., int mask, marker).
3017 // R8 renames DefaultConstructorMarker with the rest of the
3018 // stdlib (weixin: kotlin/jvm/internal/i, an empty abstract
3019 // class), so gate on the package + the int-mask/refs pair
3020 // instead of the literal name — a real user enum ctor with
3021 // (int, kotlin/jvm/internal/*) tail params does not occur.
3022 let n_args = d.args.len();
3023 let is_bridge = n_args >= 2
3024 && matches!(&d.args[n_args - 1], JavaType::Object(ref m)
3025 if m.starts_with("kotlin/jvm/internal/"))
3026 && matches!(d.args[n_args - 2], JavaType::Int);
3027 // Meituan Robust instrumented ctors PACK the trace params
3028 // into dispatch arrays (`v3[0] = str; v3[1] = new
3029 // Integer(p2); PatchProxy.isSupport(v3, ..)`) — value reads
3030 // that refuse the strip, and a true enum cannot DECLARE
3031 // (String, int) params (JLS 8.9.1), so the whole class fell
3032 // back (illegal `Enum.valueOf` + no ordinal()). Rewrite pure
3033 // VALUE reads to the equivalent `name()`/`ordinal()` calls —
3034 // Enum's fields are set by the implicit super before the
3035 // body runs, so the pack sees identical values. Delegation
3036 // LEAD reads (this(str, p2) of Kotlin/-IA bridges) must keep
3037 // their Local shape for the lead-drop below, so skip any
3038 // ctor that has them; written params are never replaceable.
3039 if synthetic.len() == 2 && !is_bridge {
3040 if let Some(b) = body.as_ref() {
3041 let (p0, p1) = (synthetic[0], synthetic[1]);
3042 let mut lead_reads = 0usize;
3043 {
3044 let cls_name = class.name.as_str();
3045 let mut c = b.body.clone();
3046 crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
3047 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3048 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3049 for a in args.iter().take(2) {
3050 if let Expr::Local { var: v, .. } = a {
3051 if *v == p0 || *v == p1 {
3052 lead_reads += 1;
3053 }
3054 }
3055 }
3056 }
3057 }
3058 });
3059 }
3060 let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3061 let reads = uses.get(&p0).copied().unwrap_or(0)
3062 + uses.get(&p1).copied().unwrap_or(0);
3063 let written = crate::passes::local_is_written(&b.body, p0)
3064 || crate::passes::local_is_written(&b.body, p1);
3065 if lead_reads == 0 && reads > 0 && !written {
3066 let mk = |nm: &str, ret: JavaType| Expr::Method {
3067 owner: Some(Box::new(Expr::This)),
3068 cls: std::sync::Arc::from("java/lang/Enum"),
3069 name: std::sync::Arc::from(nm),
3070 desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
3071 ret,
3072 args: Vec::new(),
3073 }),
3074 args: Vec::new(),
3075 is_static: false,
3076 is_interface: false,
3077 is_special: false,
3078 is_super: false,
3079 is_dynamic: false,
3080 type_args: Vec::new(),
3081 };
3082 let name_call =
3083 mk("name", JavaType::Object("java/lang/String".into()));
3084 let ord_call = mk("ordinal", JavaType::Int);
3085 if let Some(b) = body.as_mut() {
3086 crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3087 crate::passes::deep_rewrite(e, &mut |x| {
3088 if let Expr::Local { var, .. } = x {
3089 if *var == p0 {
3090 *x = name_call.clone();
3091 } else if *var == p1 {
3092 *x = ord_call.clone();
3093 }
3094 }
3095 });
3096 });
3097 }
3098 }
3099 }
3100 }
3101 let mut lead = [0usize, 0usize];
3102 let refs_ok = body.as_ref().is_some_and(|b| {
3103 let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3104 let mut c = b.body.clone();
3105 let cls_name = class.name.as_str();
3106 crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
3107 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3108 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3109 for (k, a) in args.iter().enumerate() {
3110 if let Expr::Local { var: v, .. } = a {
3111 if let Some(pi) = synthetic.iter().position(|p| p == v) {
3112 if k == pi && k < 2 {
3113 lead[pi] += 1;
3114 }
3115 }
3116 }
3117 }
3118 }
3119 }
3120 });
3121 synthetic.iter().enumerate().all(|(pi, id)| {
3122 let total = uses.get(id).copied().unwrap_or(0);
3123 if is_bridge {
3124 total == lead[pi]
3125 } else {
3126 total == 0
3127 }
3128 })
3129 });
3130 if refs_ok {
3131 arg0 = 2;
3132 // Drop the leading (name, ordinal) args of the this()
3133 // delegations.
3134 if let (Some(b), true) = (body.as_mut(), is_bridge) {
3135 let cls_name = class.name.as_str();
3136 let syn = synthetic;
3137 crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3138 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3139 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3140 let mut drop_n = 0;
3141 for a in args.iter().take(2) {
3142 if let Expr::Local { var: v, .. } = a {
3143 if syn.contains(v) && drop_n == v - syn[0] {
3144 drop_n += 1;
3145 continue;
3146 }
3147 }
3148 break;
3149 }
3150 for _ in 0..drop_n {
3151 args.remove(0);
3152 }
3153 }
3154 }
3155 });
3156 }
3157 }
3158 }
3159 let n = d.args.len();
3160 let varargs = a & ACC_VARARGS != 0 && n > 0;
3161 let arg0 = arg0 + inner_arg0;
3162 for (i, arg) in d.args.iter().enumerate().skip(arg0) {
3163 if i > arg0 {
3164 sig.push_str(", ");
3165 }
3166 let name = param_names
3167 .get(i)
3168 .cloned()
3169 .unwrap_or_else(|| format!("p{}", i));
3170 if varargs && i + 1 == n {
3171 if let JavaType::Array(inner) = arg {
3172 sig.push_str(&type_name(pool, inner));
3173 sig.push_str("...");
3174 } else {
3175 sig.push_str(&type_name(pool, arg));
3176 }
3177 } else {
3178 sig.push_str(&type_name(pool, arg));
3179 }
3180 sig.push(' ');
3181 // Param names come from dex debug info — obfuscated apps
3182 // name them `_` (reserved since Java 9) or after keywords.
3183 sig.push_str(&java_ident(&name));
3184 }
3185 sig.push(')');
3186 }
3187
3188 if is_clinit {
3189 let Some(b) = body else { return Ok(false) };
3190 // Direct render: the printer starts at the method's ABSOLUTE
3191 // indent and appends into the same buffer that carries the
3192 // header — no intermediate body string, no per-line re-indent
3193 // pass (two full copies of every method body saved).
3194 out.push_str(&ind);
3195 out.push_str("static {\n");
3196 let hdr = out.len();
3197 let printer = Printer::new(ctx, &b.vt)
3198 .with_indent(depth + 1)
3199 .with_output(std::mem::take(out));
3200 let t_print = std::time::Instant::now();
3201 let mut rendered = printer.into_string(&b.body);
3202 crate::method::phase_hit(3, t_print);
3203 if rendered[hdr..].trim().is_empty() {
3204 rendered.truncate(hdr);
3205 }
3206 rendered.push_str(&ind);
3207 rendered.push_str("}\n");
3208 *out = rendered;
3209 return Ok(true);
3210 }
3211 if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
3212 out.push_str(&ind);
3213 out.push_str(&sig);
3214 out.push_str(";\n");
3215 return Ok(true);
3216 }
3217 let Some(b) = body else { return Ok(false) };
3218
3219 // Direct render at the absolute indent level (see the clinit path).
3220 let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
3221 match &b.desc.ret {
3222 JavaType::Boolean => {
3223 printer = printer.with_ret_bool(true);
3224 }
3225 JavaType::Char => {
3226 printer = printer.with_ret_char(true);
3227 }
3228 JavaType::Byte => {
3229 printer = printer.with_ret_narrow(true, false);
3230 }
3231 JavaType::Short => {
3232 printer = printer.with_ret_narrow(false, true);
3233 }
3234 _ => {}
3235 }
3236 out.push_str(&ind);
3237 out.push_str(&sig);
3238 out.push_str(" {\n");
3239 let hdr = out.len();
3240 let printer = printer.with_output(std::mem::take(out));
3241 let t_print = std::time::Instant::now();
3242 let mut rendered = printer.into_string(&b.body);
3243 crate::method::phase_hit(3, t_print);
3244 if rendered[hdr..].trim().is_empty() {
3245 rendered.truncate(hdr);
3246 }
3247 rendered.push_str(&ind);
3248 rendered.push_str("}\n");
3249 *out = rendered;
3250 Ok(true)
3251}
3252
3253// ---------------------------------------------------------------------------
3254// Naming helpers
3255// ---------------------------------------------------------------------------
3256
3257fn indent(depth: usize) -> String {
3258 " ".repeat(depth)
3259}
3260
3261/// `$`-separated nesting rendered with dots — but ONLY when every
3262/// segment is a clean Java identifier (a genuine member class chain).
3263/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
3264/// `...$$inlined$collect$1`) and local-class tails are NOT member
3265/// classes Java can name; the whole name stays flat with `$` (ddc emits
3266/// them as their own top-level files).
3267/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
3268/// any other non-identifier character) is not legal Java. Deterministic
3269/// mapping, applied identically at declaration and call sites.
3270pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
3271 // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
3272 // (millions per APK), where it profiled as __NSGetEnviron.
3273 static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3274 if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
3275 eprintln!("[ident] {name:?}");
3276 }
3277 let clean = name
3278 .chars()
3279 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
3280 // Kotlin names fields/methods `default` (Companion.default) — no Java
3281 // program can declare or reference a keyword; the identical mapping
3282 // lives in jdc-core's call-site sanitizer.
3283 let keyword = matches!(
3284 name,
3285 "abstract"
3286 | "assert"
3287 | "boolean"
3288 | "break"
3289 | "byte"
3290 | "case"
3291 | "catch"
3292 | "char"
3293 | "class"
3294 | "const"
3295 | "continue"
3296 | "default"
3297 | "do"
3298 | "double"
3299 | "else"
3300 | "enum"
3301 | "extends"
3302 | "final"
3303 | "finally"
3304 | "float"
3305 | "for"
3306 | "goto"
3307 | "if"
3308 | "implements"
3309 | "import"
3310 | "instanceof"
3311 | "int"
3312 | "interface"
3313 | "long"
3314 | "native"
3315 | "new"
3316 | "package"
3317 | "private"
3318 | "protected"
3319 | "public"
3320 | "return"
3321 | "short"
3322 | "static"
3323 | "strictfp"
3324 | "super"
3325 | "switch"
3326 | "synchronized"
3327 | "this"
3328 | "throw"
3329 | "throws"
3330 | "transient"
3331 | "try"
3332 | "void"
3333 | "volatile"
3334 | "while"
3335 | "true"
3336 | "false"
3337 | "null"
3338 // `_` is a reserved identifier since Java 9 (Alipay's
3339 // instant-run fields are named `_`) — the `_<name>` mapping
3340 // turns it into `__`, matching jdc-core's call sites.
3341 | "_"
3342 );
3343 // A simple name may not START with a digit either (WhatsApp nests
3344 // `X/0Xx`): the declaration site and every reference (jdc-core's
3345 // sanitize_source_name) prefix the same underscore.
3346 let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
3347 if clean && !keyword && !digit_start {
3348 std::borrow::Cow::Borrowed(name)
3349 } else if keyword || digit_start {
3350 std::borrow::Cow::Owned(format!("_{name}"))
3351 } else {
3352 // Non-ASCII single chars (Alipay names a field `支`) map to a
3353 // lone `_` — itself reserved since Java 9. Escape it.
3354 let mapped: String = name
3355 .chars()
3356 .map(|c| {
3357 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
3358 c
3359 } else {
3360 '_'
3361 }
3362 })
3363 .collect();
3364 if mapped == "_" {
3365 std::borrow::Cow::Owned("__".to_string())
3366 } else {
3367 std::borrow::Cow::Owned(mapped)
3368 }
3369 }
3370}
3371
3372fn split_name(internal: &str) -> (String, String) {
3373 match internal.rfind('/') {
3374 Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
3375 None => (String::new(), internal.to_string()),
3376 }
3377}
3378
3379/// Dotted source form of an internal name.
3380pub fn dotted(internal: &str) -> String {
3381 let mut out = internal.replace('/', ".");
3382 // `$` → `.` only when the following segment can start a Java
3383 // identifier (anonymous/synthetic tails stay `$`).
3384 let mut i = 0;
3385 while let Some(p) = out[i..].find('$') {
3386 let at = i + p;
3387 // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
3388 // source name — dotting it produced a leading `.Gson.Types`.
3389 let head_ok = at > 0
3390 && out[..at]
3391 .chars()
3392 .next_back()
3393 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
3394 let tail_ok = out[at + 1..]
3395 .chars()
3396 .next()
3397 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
3398 if tail_ok && head_ok {
3399 out.replace_range(at..at + 1, ".");
3400 i = at + 1;
3401 } else {
3402 i = at + 1;
3403 }
3404 }
3405 // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
3406 // reached `extends` with the digit start unmapped.
3407 sanitize_fq(&out)
3408}
3409
3410fn join_dotted(pool: &DexPool, names: &[String]) -> String {
3411 names
3412 .iter()
3413 .map(|n| print_class_name(pool, n))
3414 .collect::<Vec<_>>()
3415 .join(", ")
3416}
3417
3418/// A printable type name (arrays render with `[]` suffixes). Nested class
3419/// names dot their `$` when the outer chain is known to the pool.
3420/// Split a concatenated method-descriptor ARGUMENT string
3421/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
3422/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
3423/// referenced sig comes from the method-id table (no pool ancestor
3424/// method to read a parsed descriptor from).
3425fn split_arg_descs(sig: &str) -> Vec<JavaType> {
3426 let mut out = Vec::new();
3427 let b = sig.as_bytes();
3428 let mut i = 0usize;
3429 while i < b.len() {
3430 let start = i;
3431 while i < b.len() && b[i] == b'[' {
3432 i += 1;
3433 }
3434 if i < b.len() {
3435 if b[i] == b'L' {
3436 while i < b.len() && b[i] != b';' {
3437 i += 1;
3438 }
3439 if i < b.len() {
3440 i += 1; // consume ';'
3441 }
3442 } else {
3443 i += 1; // primitive single char
3444 }
3445 }
3446 if i > start {
3447 out.push(desc_type(&sig[start..i]));
3448 } else {
3449 break; // malformed: avoid an infinite loop
3450 }
3451 }
3452 out
3453}
3454
3455pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
3456 match t {
3457 JavaType::Void => "void".into(),
3458 JavaType::Object(n) => print_class_name(pool, n),
3459 JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
3460 other => other.to_java(false),
3461 }
3462}
3463
3464/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
3465/// its left side names a known class (literal `$` top-level names survive).
3466// ---------------------------------------------------------------------------
3467// The per-class import map (JLS 6.4.2 obscuring repair).
3468//
3469// A referenced FQN whose FIRST segment equals an in-scope class simple
3470// name binds the qualifier to the CLASS, not the package — `class j3
3471// implements j3.g` is cyclic inheritance and collapses javac's
3472// attribution for the whole package (the Object-cascade root, weixin
3473// 84 files + contagion). The repair: emit `import j3.g;` and render
3474// the SIMPLE name in every type position. The set is populated per
3475// class from a metadata pre-scan (supertypes, field types, method
3476// signature descriptors) and read by print_class_name and the
3477// DexCtx::obscured_simple implementation (which jdc-core's shorten /
3478// java_type_name consult).
3479struct ObscureState {
3480 /// The ROOT class being rendered (its simple name is the obscuring
3481 /// in-scope name).
3482 class: String,
3483 /// The root class transitively extends android.view.View: it
3484 /// INHERITS the static Property fields View.X/Y/Z (API 31+), and
3485 /// an inherited member variable obscures a package's first segment
3486 /// in expression position (JLS 6.4.2) — every `X.foo` qualified
3487 /// ref inside the file would bind to the field (WhatsApp ×30,171:
3488 /// 431 custom-View files against its obfuscated package X). Those
3489 /// refs route through the import layer like own-simple obscuring.
3490 view_shadow: bool,
3491 /// Metadata pre-scan results: internal → simple render.
3492 map: jdc_core::FxHashMap<String, String>,
3493 /// Expression-level obscured refs DISCOVERED during the body render
3494 /// (their imports emit at assembly time).
3495 recorded: jdc_core::FxHashSet<String>,
3496 /// Simple names of the CURRENT PACKAGE's own classes: importing a
3497 /// name that collides would SHADOW every same-package use of it in
3498 /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
3499 /// erroring) rather than corrupt.
3500 blocked: jdc_core::FxHashSet<String>,
3501 /// RENAMED display tails of same-package classes (zero-copy view
3502 /// into the process-wide cache): same blocking role as `blocked`
3503 /// for refs that render under the rename registry.
3504 blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3505}
3506
3507thread_local! {
3508 static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
3509 const { std::cell::RefCell::new(None) };
3510}
3511
3512/// Install the per-class render state (call at class render entry).
3513pub(crate) fn set_obscured_state(
3514 class: String,
3515 map: jdc_core::FxHashMap<String, String>,
3516 blocked: jdc_core::FxHashSet<String>,
3517 blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3518 view_shadow: bool,
3519) {
3520 OBSCURE.with(|m| {
3521 *m.borrow_mut() = Some(ObscureState {
3522 class,
3523 view_shadow,
3524 map,
3525 recorded: jdc_core::FxHashSet::default(),
3526 blocked,
3527 blocked_renamed,
3528 });
3529 });
3530}
3531
3532/// Simple names this class's file renders through the import layer
3533/// (metadata map values + dynamically recorded nested tails). A method
3534/// local with one of these names captures the imported simple name in
3535/// expression position (locals beat single-type imports — `h.e` binds
3536/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
3537/// Empty when no render state is installed (progressive paths).
3538pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
3539 OBSCURE.with(|m| {
3540 let mut out = jdc_core::FxHashSet::default();
3541 let Ok(st) = m.try_borrow() else { return out };
3542 let Some(st) = st.as_ref() else { return out };
3543 for simple in st.map.values() {
3544 out.insert(simple.clone());
3545 }
3546 for internal in &st.recorded {
3547 let renamed = crate::apply_class_rename(internal);
3548 if let Some(tail) = renamed.rsplit(['/', '$']).next() {
3549 if !tail.is_empty() {
3550 out.insert(tail.to_string());
3551 }
3552 }
3553 }
3554 out
3555 })
3556}
3557
3558/// Take the recorded expression-level refs and clear the state.
3559pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
3560 OBSCURE.with(|m| {
3561 m.borrow_mut()
3562 .take()
3563 .map(|st| st.recorded)
3564 .unwrap_or_default()
3565 })
3566}
3567
3568// ---- cross-package access widening ----------------------------------
3569// ART tolerates cross-package access to non-public classes/members that
3570// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
3571// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
3572// l6 中是 protected"). The census scan records every cross-package
3573// reference; the installer keeps the non-public targets and the
3574// declaration renders widen them to public.
3575
3576struct WidenState {
3577 classes: jdc_core::FxHashSet<String>,
3578 methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
3579 fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
3580 /// (owner, field name) finals written outside the declaring class's
3581 /// ctor/clinit — rendered WITHOUT `final` (see refscan census).
3582 unfinal: jdc_core::FxHashSet<(String, String)>,
3583}
3584
3585static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
3586
3587pub(crate) fn install_access_widening(pool: &crate::DexPool) {
3588 let t0 = std::time::Instant::now();
3589 let raw = crate::refscan::access_widening_scan(pool.dexes());
3590 let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3591 for c in &raw.classes {
3592 if let Some(pc) = pool.get_if_materialized(c) {
3593 if pc.access & crate::access::ACC_PUBLIC == 0 {
3594 classes.insert(c.clone());
3595 }
3596 }
3597 }
3598 let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
3599 jdc_core::FxHashMap::default();
3600 for (owner, set) in &raw.methods {
3601 for (name, desc) in set {
3602 if name == "<clinit>" {
3603 continue; // never referenced
3604 }
3605 // A dex ref of an INHERITED member may name any subclass as
3606 // owner (ART resolves up the chain), while the declaration —
3607 // and its access modifier — lives on an ancestor. Register
3608 // the widening on the DECLARING class or the rendered decl
3609 // stays protected ("在 X 中是 protected 访问控制").
3610 let mut cur: Option<&str> = Some(owner.as_str());
3611 let mut hops = 0u32;
3612 while let Some(cname) = cur {
3613 hops += 1;
3614 if hops > 64 {
3615 break;
3616 }
3617 let Some(pc) = pool.get_if_materialized(cname) else {
3618 break;
3619 };
3620 // Enum ctors are source-level private-only: widening is
3621 // a javac "modifier not allowed here".
3622 if name == "<init>" && pc.access & crate::access::ACC_ENUM != 0 {
3623 break;
3624 }
3625 match pc.all_methods()
3626 .find(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str())
3627 {
3628 Some(m) if m.access & crate::access::ACC_PUBLIC == 0 => {
3629 methods
3630 .entry(cname.to_string())
3631 .or_default()
3632 .insert((name.clone(), desc.clone()));
3633 break;
3634 }
3635 Some(_) => break, // already public
3636 None => cur = pc.super_name.as_deref(),
3637 }
3638 }
3639 }
3640 }
3641 // Override-closure DOWN the hierarchy: a widened (public) super
3642 // method whose subclass override stays protected/package is
3643 // "无法覆盖...更低的访问权限" (lark +823). Every transitive
3644 // subclass declaring the same (name, desc) non-public widens too.
3645 {
3646 let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
3647 jdc_core::FxHashMap::default();
3648 for name in &pool.order {
3649 let Some(pc) = pool.get_if_materialized(name) else {
3650 continue;
3651 };
3652 if let Some(sup) = &pc.super_name {
3653 subs.entry(sup.clone()).or_default().push(name.clone());
3654 }
3655 for i in &pc.interfaces {
3656 subs.entry(i.clone()).or_default().push(name.clone());
3657 }
3658 }
3659 let seeds: Vec<(String, String, String)> = methods
3660 .iter()
3661 .flat_map(|(o, set)| {
3662 set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
3663 })
3664 .collect();
3665 for (owner, mname, mdesc) in seeds {
3666 let mut queue: Vec<String> = match subs.get(&owner) {
3667 Some(v) => v.clone(),
3668 None => continue,
3669 };
3670 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3671 while let Some(c) = queue.pop() {
3672 if !seen.insert(c.clone()) {
3673 continue;
3674 }
3675 let Some(pc) = pool.get_if_materialized(&c) else {
3676 continue;
3677 };
3678 let declares_nonpub = pc.all_methods().any(|m| {
3679 &*m.name == mname.as_str()
3680 && &*m.desc == mdesc.as_str()
3681 && m.access & crate::access::ACC_PUBLIC == 0
3682 });
3683 if declares_nonpub {
3684 methods
3685 .entry(c.clone())
3686 .or_default()
3687 .insert((mname.clone(), mdesc.clone()));
3688 }
3689 if let Some(v) = subs.get(&c) {
3690 queue.extend(v.iter().cloned());
3691 }
3692 }
3693 }
3694 // Subclass `super(..)` targets. A class C extends S; C's ctor
3695 // renders `super(..)` against S's ctor. When R8 INLINES S's
3696 // trivial ctor, C's dex ctor calls `Object.<init>` directly (not
3697 // `S.<init>`), so the bytecode census above never sees the S.<init>
3698 // reference and a PRIVATE S ctor stays private — the rendered
3699 // `super()` then fails ("R() 在 R 中是 private 访问控制", lark ×122:
3700 // pl.droidsonroids.gif.R extends com.ss.android.lark.R, whose ctor
3701 // R8 inlined to Object.<init>). Widen every private ctor of a
3702 // class that HAS subclasses so the implicit super() links. Enums
3703 // are excluded (their ctors are source-level private-only).
3704 for sup in subs.keys() {
3705 let Some(pc) = pool.get_if_materialized(sup) else {
3706 continue;
3707 };
3708 if pc.access & crate::access::ACC_ENUM != 0 {
3709 continue;
3710 }
3711 for m in pc.all_methods() {
3712 if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
3713 methods
3714 .entry(sup.clone())
3715 .or_default()
3716 .insert((m.name.to_string(), m.desc.to_string()));
3717 }
3718 }
3719 }
3720 }
3721 let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
3722 jdc_core::FxHashMap::default();
3723 for (owner, set) in &raw.fields {
3724 for name in set {
3725 // Same inherited-owner walk as methods: the protected field
3726 // lives on the ancestor that DECLARES it (weibo
3727 // mModuleContext/mData ×470 — refs named the module
3728 // subclass, the own-fields-only filter never widened the
3729 // declaring base).
3730 let mut cur: Option<&str> = Some(owner.as_str());
3731 let mut hops = 0u32;
3732 while let Some(cname) = cur {
3733 hops += 1;
3734 if hops > 64 {
3735 break;
3736 }
3737 let Some(pc) = pool.get_if_materialized(cname) else {
3738 break;
3739 };
3740 match pc.static_fields
3741 .iter()
3742 .chain(pc.instance_fields.iter())
3743 .find(|f| f.name == name.as_str())
3744 {
3745 Some(f) if f.access & crate::access::ACC_PUBLIC == 0 => {
3746 fields
3747 .entry(cname.to_string())
3748 .or_default()
3749 .insert(name.clone());
3750 break;
3751 }
3752 Some(_) => break, // already public
3753 None => cur = pc.super_name.as_deref(),
3754 }
3755 }
3756 }
3757 }
3758 if std::env::var("DDC_STATS").is_ok() {
3759 eprintln!(
3760 "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
3761 classes.len(),
3762 methods.len(),
3763 fields.len(),
3764 t0.elapsed()
3765 );
3766 }
3767
3768 let unfinal: jdc_core::FxHashSet<(String, String)> =
3769 raw.unfinal_fields.into_iter().collect();
3770 let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
3771}
3772
3773#[inline]
3774pub(crate) fn widen_class(internal: &str) -> bool {
3775 WIDEN.get().is_some_and(|w| w.classes.contains(internal))
3776}
3777
3778#[inline]
3779pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
3780 WIDEN.get().is_some_and(|w| {
3781 w.methods
3782 .get(owner)
3783 .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
3784 })
3785}
3786
3787#[inline]
3788pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
3789 WIDEN.get().is_some_and(|w| {
3790 w.fields.get(owner).is_some_and(|s| s.contains(name))
3791 })
3792}
3793
3794/// True when the census saw this final field written outside the
3795/// declaring class's `<init>`/`<clinit>` — the `final` modifier must
3796/// not render ("无法为 final 变量 v 分配值" at every foreign write).
3797#[inline]
3798pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
3799 WIDEN.get().is_some_and(|w| {
3800 w.unfinal.contains(&(owner.to_string(), name.to_string()))
3801 })
3802}
3803
3804/// Render-time lookup: the metadata map first; otherwise an
3805/// expression-level ref whose first segment equals the root class's
3806/// simple name — record it (its import emits at assembly) and render
3807/// the simple name NOW.
3808pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
3809 OBSCURE.with(|m| {
3810 let mut st = m.borrow_mut();
3811 let st = st.as_mut()?;
3812 if let Some(simple) = st.map.get(internal) {
3813 return Some(simple.clone());
3814 }
3815 let first = internal.split('/').next().unwrap_or("");
3816 // The DISPLAY own simple: when the root class itself was renamed
3817 // (case-collision `widget/d` → `widget/d2`), no in-scope type
3818 // bears the raw name any more — the package's first segment is
3819 // NOT obscured and the qualified render is the correct one
3820 // (reqable d2.java: importing d.h let the class's own boolean
3821 // field h shadow it — 无法在调用超类型构造器之前引用h ×23).
3822 let own_renamed = crate::apply_class_rename(&st.class);
3823 let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
3824 let seg_obscured =
3825 first == own || (st.view_shadow && matches!(first, "X" | "Y" | "Z"));
3826 if seg_obscured && internal.split('/').count() >= 2 {
3827 // A NESTED internal's in-scope simple name is its `$` tail
3828 // (`x/a$b` imports/renders as `b`) — the slash-tail left the
3829 // `$` in the render (`t2$a` flat against a nested emission).
3830 // The tail must come from the RENAMED display: case-collision
3831 // renames (`X/00i` → `X/_00i_2`) live in the class registry —
3832 // the raw tail rendered `_00i` against the declaration's
3833 // `_00i_2` (WhatsApp cannot-find ×27k after the View-shadow
3834 // escape opened this path at scale).
3835 let renamed = crate::apply_class_rename(internal);
3836 let simple = renamed
3837 .rsplit(['/', '$'])
3838 .next()
3839 .unwrap_or("")
3840 .to_string();
3841 if !simple.is_empty()
3842 && !st.blocked.contains(&simple)
3843 && !st.blocked_renamed.is_some_and(|e| e.contains(&simple))
3844 {
3845 st.recorded.insert(internal.to_string());
3846 return Some(simple);
3847 }
3848 }
3849 None
3850 })
3851}
3852
3853/// True when `class` transitively extends android.view.View (pool
3854/// chain, then a framework-boundary prefix check: any android/widget,
3855/// android/view, android/webkit or inputmethodservice ancestor is a
3856/// View descendant and carries the inherited View.X/Y/Z statics).
3857fn is_view_subclass(pool: &DexPool, class: &PoolClass) -> bool {
3858 let mut cur = class.super_name.clone();
3859 let mut hops = 0;
3860 while let Some(s) = cur {
3861 hops += 1;
3862 if hops > 64 {
3863 break;
3864 }
3865 if s == "android/view/View" {
3866 return true;
3867 }
3868 match pool.get(&s) {
3869 Some(sc) => cur = sc.super_name.clone(),
3870 None => {
3871 return s.starts_with("android/widget/")
3872 || s.starts_with("android/view/")
3873 || s.starts_with("android/webkit/")
3874 || s.starts_with("android/inputmethodservice/");
3875 }
3876 }
3877 }
3878 false
3879}
3880
3881/// The pre-scan: internal names referenced by the class's metadata
3882/// (supertypes, field types, method descriptors) whose first segment
3883/// equals the class's own simple name and which exist in the pool —
3884/// these get imports and simple-name renders.
3885fn compute_obscured_renders(
3886 pool: &DexPool,
3887 class: &PoolClass,
3888 view_shadow: bool,
3889) -> jdc_core::FxHashMap<String, String> {
3890 let own_renamed = crate::apply_class_rename(&class.name);
3891 let own_simple = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
3892 if own_simple.is_empty() {
3893 return jdc_core::FxHashMap::default();
3894 }
3895 let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3896 for sup in class.interfaces.iter() {
3897 refs.insert(sup.clone());
3898 }
3899 if let Some(sup) = &class.super_name {
3900 refs.insert(sup.clone());
3901 }
3902 for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
3903 if let JavaType::Object(n) = crate::desc_type(&f.desc) {
3904 refs.insert(n.to_string());
3905 }
3906 }
3907 for m in class.all_methods() {
3908 if let Some(d) = m.parsed_desc() {
3909 for a in d.args.iter() {
3910 if let JavaType::Object(n) = a {
3911 refs.insert(n.to_string());
3912 }
3913 }
3914 if let JavaType::Object(n) = &d.ret {
3915 refs.insert(n.to_string());
3916 }
3917 }
3918 }
3919 let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
3920 for r in refs {
3921 // First segment == the class's own simple name: the qualified
3922 // render would be obscured. The name must exist in the pool for
3923 // the import to resolve. The KEY stays internal (renders look
3924 // refs up by internal name), but the import line and the
3925 // simple name use the RENAMED display — a collision-renamed
3926 // class renders under its display name and an import of the
3927 // internal name does not resolve.
3928 let first = r.split('/').next().unwrap_or("");
3929 let seg_obscured =
3930 first == own_simple || (view_shadow && matches!(first, "X" | "Y" | "Z"));
3931 if seg_obscured && r.split('/').count() >= 2 && pool.get(&r).is_some() {
3932 // Own-family refs (the class itself, its nested members)
3933 // render through member scope — an import would be redundant
3934 // (self-import) or shadow a same-package sibling named like
3935 // the tail (`import t.t2.a` hijacks every bare `a` that
3936 // meant sibling class t.a — lark t/t2).
3937 if r == class.name || r.starts_with(&format!("{}$", class.name)) {
3938 continue;
3939 }
3940 let display = crate::apply_class_rename(&r);
3941 let simple = display
3942 .rsplit(['/', '$'])
3943 .next()
3944 .unwrap_or("")
3945 .to_string();
3946 if !simple.is_empty() {
3947 out.insert(r.clone(), simple);
3948 }
3949 }
3950 }
3951 out
3952}
3953
3954pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
3955 if let Some(simple) = obscured_render_pub(internal) {
3956 return sanitize_ref(&simple);
3957 }
3958 let cow = crate::apply_class_rename(internal);
3959 let internal: &str = &cow;
3960 // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
3961 // shape, `Outer$lruCache$1`) is emitted as its own top-level file
3962 // whose simple name keeps every `$` — the `$` boundaries inside
3963 // that unit are part of the NAME, not nesting. References must use
3964 // the flat unit name; the generic loop below dotted the first
3965 // boundary (`LruCacheKt.lruCache$1`) against the declaration
3966 // `class LruCacheKt$lruCache$1` — every use of the type failed and
3967 // javac's attribution for the whole file collapsed (3627 pure-
3968 // cascade files on weibo).
3969 if internal.contains('$') && pool.get(internal).is_some() {
3970 if let Some(root) = emission_root(pool, internal) {
3971 if root.contains('$') {
3972 let below = &internal[root.len()..];
3973 if below.is_empty() {
3974 // The unit itself: the flat name IS the reference.
3975 return sanitize_ref(&internal.replace('/', "."));
3976 }
3977 let segs: Vec<&str> = below[1..].split('$').collect();
3978 if segs.iter().any(|s| s.is_empty()) {
3979 // R8 tails like `ThreadMsg$$$` cannot be dotted at
3980 // all — keep the whole name flat.
3981 return sanitize_ref(&internal.replace('/', "."));
3982 }
3983 let mut out = root.replace('/', ".");
3984 for s in segs {
3985 out.push('.');
3986 out.push_str(s);
3987 }
3988 return sanitize_ref(&out);
3989 }
3990 }
3991 }
3992 let mut out = String::new();
3993 // `known` must test the ACCUMULATED internal prefix, not the bare
3994 // inter-`$` segment: the per-segment shape checked `pool.get("a")`
3995 // for the second level of `s5/o$a$b`, missed, and rendered the
3996 // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
3997 // nested type.
3998 let mut off = 0usize;
3999 loop {
4000 let rest = &internal[off..];
4001 match rest.find('$') {
4002 Some(i) => {
4003 let seg = &rest[..i];
4004 let prefix = &internal[..off + i];
4005 let known = pool.get(prefix).is_some()
4006 || jdc_core::rename::is_renamed_display(prefix)
4007 // The FULL name is not a pool class: this `$` cannot
4008 // be a literal name (pool literal classes — an app's
4009 // own `View$OnUnhandledKeyEventListener` — keep their
4010 // `$` here AND at their declaration), so it can only
4011 // be an external framework nesting boundary
4012 // (`View$OnClickListener` → `.OnClickListener`).
4013 || !pool.get(internal).is_some();
4014 // The `$` may only become a nesting dot when the tail
4015 // segment STARTS a Java identifier: R8's desugared-
4016 // library names carry `$` inside PACKAGE paths
4017 // (`j$/util/...` dotted into `j..util`) and suffixes
4018 // like `Collection$-EL` or anonymous `RequestId$1`
4019 // cannot be dotted under any reading.
4020 let tail_ok = rest[i + 1..]
4021 .chars()
4022 .next()
4023 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
4024 out.push_str(&seg.replace('/', "."));
4025 out.push_str(if known && tail_ok { "." } else { "$" });
4026 off += i + 1;
4027 }
4028 None => {
4029 out.push_str(&rest.replace('/', "."));
4030 return sanitize_ref(&out);
4031 }
4032 }
4033 }
4034}
4035
4036/// The top-level EMISSION UNIT ancestor of a pool class: walking the
4037/// `$`-chain upward (outer_of), the first ancestor that is itself
4038/// emitted as its own compilation unit — a digit-tail rest marks a
4039/// flat boundary (see top_level_classes). Clean members render inline
4040/// in their outer, so only the unit's own name may carry `$`.
4041fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
4042 let mut cur = internal.to_string();
4043 loop {
4044 let outer = pool.outer_of(&cur)?;
4045 let rest = cur
4046 .strip_prefix(outer)
4047 .and_then(|t| t.strip_prefix('$'))
4048 .unwrap_or("");
4049 if !crate::clean_member_tail(rest) {
4050 return Some(cur);
4051 }
4052 cur = outer.to_string();
4053 }
4054}
4055
4056/// Class-file names may contain characters Java source identifiers
4057/// cannot (`Collection$-EL`); the deterministic mapping matches the
4058/// declaration sites (java_ident).
4059/// Every `.`-segment of a fully-qualified name must start a Java
4060/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
4061///
4062/// PUBLIC on purpose: this is the single source of truth for the
4063/// declaration↔file-name mapping. The CLI writer used to keep its own
4064/// copy (`sanitize_file_seg`) which had drifted — it lacked the lone-`_`
4065/// escape below — so `l.֡` declared `class __` inside a file named
4066/// `_.java`. Callers reach it through `sanitize_seg`/`sanitize_internal`.
4067///
4068/// INJECTIVE on the characters, which is the property the writer needs.
4069/// The old mapping folded every non-identifier character to `_`, so
4070/// `l.᩻ܶ`, `l.᩻ۡ` and `l.֫᩷` all became `l.__`: on bin.mt.plus 22,636
4071/// distinct classes in package `l` collapsed onto three file names, the
4072/// writer's EEXIST branch overwrote them, and 22,633 sources were
4073/// destroyed with exit code 0. `_u<hex>` keeps them distinct, is
4074/// self-describing (it encodes the original code point) and costs the
4075/// decompiler nothing — the alternative, minting 22,636 registry
4076/// renames, makes every type reference allocate and took this sample
4077/// from 5.2 s to 26.8 s.
4078///
4079/// The only residual collisions are *lookalikes*: a literal class named
4080/// `_u1a7b` beside the class `᩻` (same code point), or the pre-existing
4081/// keyword form `_do` beside `do`. Those are detected and repaired
4082/// deterministically by `lossy_sanitize_renames`, which is why that pass
4083/// still exists.
4084pub fn sanitize_fq(dotted: &str) -> String {
4085 dotted
4086 .split('.')
4087 .map(sanitize_fq_seg)
4088 .collect::<Vec<_>>()
4089 .join(".")
4090}
4091
4092/// One `.`-segment: escape, then repair identifier-position problems.
4093fn sanitize_fq_seg(seg: &str) -> String {
4094 // Ident-safe ASCII passes through unchanged (the overwhelmingly
4095 // common case, and the only one on the borrow-fast path); every
4096 // other character — non-ASCII, and ASCII punctuation such as the
4097 // `-` in `Collection$-EL` — becomes `_u<hex>`.
4098 let plain = seg
4099 .chars()
4100 .all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
4101 let mut out = if plain {
4102 seg.to_string()
4103 } else {
4104 use std::fmt::Write as _;
4105 let mut s = String::with_capacity(seg.len() + 8);
4106 for c in seg.chars() {
4107 if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
4108 s.push(c);
4109 } else {
4110 s.push_str("_u");
4111 let _ = write!(s, "{:x}", c as u32);
4112 }
4113 }
4114 s
4115 };
4116 // `out` is pure ASCII from here: the identifier-position repairs.
4117 if is_java_keyword_name(&out)
4118 || is_restricted_type_name(&out)
4119 || out.chars().next().is_some_and(|c| c.is_ascii_digit())
4120 {
4121 out.insert(0, '_');
4122 }
4123 // A lone `_` is a reserved IDENTIFIER since Java 9.
4124 if out == "_" {
4125 out = "__".to_string();
4126 }
4127 out
4128}
4129
4130/// Restricted contextual TYPE names — legal as member/local names
4131/// (rt.jar compiles `var` locals), illegal in class declarations and
4132/// type references. Consulted only on CLASS-name paths.
4133pub(crate) fn is_restricted_type_name(s: &str) -> bool {
4134 matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
4135}
4136
4137fn is_java_keyword_name(s: &str) -> bool {
4138 matches!(
4139 s,
4140 "abstract"
4141 | "assert"
4142 | "boolean"
4143 | "break"
4144 | "byte"
4145 | "case"
4146 | "catch"
4147 | "char"
4148 | "class"
4149 | "const"
4150 | "continue"
4151 | "default"
4152 | "do"
4153 | "double"
4154 | "else"
4155 | "enum"
4156 | "extends"
4157 | "final"
4158 | "finally"
4159 | "float"
4160 | "for"
4161 | "goto"
4162 | "if"
4163 | "implements"
4164 | "import"
4165 | "instanceof"
4166 | "int"
4167 | "interface"
4168 | "long"
4169 | "native"
4170 | "new"
4171 | "package"
4172 | "private"
4173 | "protected"
4174 | "public"
4175 | "return"
4176 | "short"
4177 | "static"
4178 | "strictfp"
4179 | "super"
4180 | "switch"
4181 | "synchronized"
4182 | "this"
4183 | "throw"
4184 | "throws"
4185 | "transient"
4186 | "try"
4187 | "void"
4188 | "volatile"
4189 | "while"
4190 | "true"
4191 | "false"
4192 | "null"
4193 // `_` is a reserved identifier since Java 9 (Alipay's
4194 // instant-run fields are named `_`) — the `_<name>` mapping
4195 // turns it into `__`, matching jdc-core's call sites.
4196 | "_"
4197 )
4198}
4199
4200fn sanitize_ref(name: &str) -> String {
4201 sanitize_fq(name)
4202}
4203
4204/// Dotted source form of an internal name.
4205pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
4206 print_class_name(pool, internal)
4207}
4208
4209/// Unused import silencer.
4210#[allow(dead_code)]
4211fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
4212 let _ = java_type_to_generic;
4213}
4214
4215#[cfg(test)]
4216mod sanitize_tests {
4217 use super::sanitize_fq;
4218
4219 /// The property the file writer depends on: distinct class names must
4220 /// reach distinct output paths. The lossy predecessor of this mapping
4221 /// folded every non-identifier character to `_`, so 22,636 classes of
4222 /// `bin.mt.plus` (names built from Thai/Yi/Syriac code points) became
4223 /// three (`l.__`, `l.___`, `l._`) and all but three were overwritten.
4224 #[test]
4225 fn distinct_obfuscated_names_stay_distinct() {
4226 // The three names that collapsed onto `l/__.java` in the field.
4227 let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
4228 let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
4229 let before = mapped.len();
4230 mapped.sort();
4231 mapped.dedup();
4232 assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
4233 assert!(
4234 mapped.iter().all(|m| m.is_ascii()),
4235 "sanitizer must stay ASCII: {mapped:?}"
4236 );
4237 // Self-describing: the escape carries the original code point.
4238 assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
4239 }
4240
4241 /// Every output must be a legal Java identifier segment: no leading
4242 /// digit, no keyword, not the lone `_` reserved since Java 9.
4243 #[test]
4244 fn output_is_a_legal_identifier() {
4245 for (input, want) in [
4246 ("do", "_do"),
4247 ("_", "__"),
4248 ("0Xx", "_0Xx"),
4249 ("Collection$-EL", "Collection$_u2dEL"),
4250 ("᩻", "_u1a7b"),
4251 ("a", "a"),
4252 ("A$B", "A$B"),
4253 ("x_y", "x_y"),
4254 ] {
4255 assert_eq!(sanitize_fq(input), want, "input {input:?}");
4256 }
4257 }
4258
4259 /// Documented residual: ident-safe ASCII passes through unchanged, so a
4260 /// literal `_u1a7b` collides with the single character U+1A7B. The
4261 /// collision is real — which is why `lossy_sanitize_renames` still owns
4262 /// a repair pass — and this test pins the shape the guard must catch.
4263 #[test]
4264 fn lookalike_collision_is_known_and_bounded() {
4265 assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
4266 }
4267}