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 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
58 let _ = std::thread::Builder::new()
59 .stack_size(64 * 1024 * 1024)
60 .spawn(move || {
61 let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
62 let _ = tx.send(r);
63 });
64 pending.lock().unwrap().push((rx, name, deadline));
65 return Err(anyhow::anyhow!(
66 "deferred to monitored thread (pathological-CFG guard)"
67 ));
68 }
69 decompile_class_impl(pool, class, opts)
70}
71
72/// Any method in the exponential-walk danger zone → run under the
73/// deadline. Calibrated between real workloads: routine R8 classes top out
74/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
75/// (weibo's gson TypeAdapters) sit at 24k+.
76fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
77 // Header peek only — decoding every body here would double the APK's
78 // total decode work.
79 for m in class.all_methods() {
80 if m.code_off == 0 {
81 continue;
82 }
83 if let Some(dex) = pool.dex(m.dex_idx) {
84 if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
85 if insns > 16_000 {
86 return true;
87 }
88 }
89 }
90 }
91 false
92}
93
94fn decompile_class_impl(
95 pool: &DexPool,
96 class: &PoolClass,
97 opts: &ClassOptions,
98) -> anyhow::Result<String> {
99 let ctx = DexCtx::new(pool, class);
100 // Size the class buffer up front: corpus-average classes render to
101 // ~1KB per method — without the reserve the String doubles through
102 // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
103 // Cap the reserve: weixin's monster classes (hundreds of methods)
104 // would reserve megabytes of untouched capacity per class — large
105 // blocks take mimalloc's commit/purge path (madvise churn showed up
106 // in profiles). 256KB covers the corpus-average class dozens of
107 // times over; bigger classes pay a few extra doublings.
108 let mut out = String::with_capacity(
109 (class.all_methods().count() * 1024).min(256 * 1024) + 512,
110 );
111 if opts.provenance {
112 static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
113 out.push_str(BANNER.get_or_init(|| {
114 format!(
115 "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
116 env!("CARGO_PKG_VERSION")
117 )
118 }));
119 // Provenance: which input image this class was lifted from (jadx's
120 // `loaded from: classes.dex` pattern). Byte-stable across runs —
121 // deliberately NO timestamp so outputs diff cleanly. Direct
122 // push_str: the per-class format! temporaries were 3 allocations
123 // × every class in the corpus.
124 if let Some(dex) = pool.dex(class.dex_idx) {
125 if let Some(label) = pool.dex_labels.get(class.dex_idx) {
126 out.push_str("// From: ");
127 out.push_str(label);
128 out.push_str(" (DEX ");
129 out.push_str(&dex.version);
130 out.push_str(")\n");
131 }
132 }
133 }
134 if let Some(src) = &class.source_file {
135 out.push_str("// Source file: ");
136 out.push_str(src);
137 out.push('\n');
138 }
139 if class.is_synthetic() {
140 out.push_str("// synthetic\n");
141 }
142 let (pkg, _) = split_name(&class.name);
143 // Two-pass import assembly: the body renders FIRST (into a temp
144 // buffer) with the obscured render state installed — expression
145 // positions DISCOVER additional obscured refs on the fly — then the
146 // file assembles as provenance + package + imports + body. The one
147 // extra body copy is the price of correct import placement.
148 let mut obscured_map = compute_obscured_renders(pool, class);
149 // Same-package simple-name collisions: an import shadows every
150 // same-package use of that simple in this file — drop those from
151 // the map (their refs stay qualified, erroring honestly).
152 let blocked: jdc_core::FxHashSet<String> = pool
153 .package_simples()
154 .get(&pkg)
155 .cloned()
156 .unwrap_or_default();
157 obscured_map.retain(|_internal, simple| !blocked.contains(simple));
158 set_obscured_state(class.name.clone(), obscured_map.clone(), blocked.clone());
159 let mut body_buf = String::with_capacity(out.capacity() / 2);
160 let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
161 let recorded = take_recorded_and_clear();
162 body_res?;
163 if !pkg.is_empty() {
164 out.push('\n');
165 out.push_str("package ");
166 let dotted = pkg.replace('/', ".");
167 out.push_str(&sanitize_fq(&dotted));
168 out.push_str(";\n");
169 // Imports: the metadata map plus the expression-level
170 // discoveries, pool-known only (an import of an unknown class
171 // is a hard error), display-renamed, sorted for determinism.
172 let mut import_set: jdc_core::FxHashSet<String> =
173 obscured_map.keys().cloned().collect();
174 for r in recorded {
175 if pool.get(&r).is_some() {
176 let simple = r.rsplit('/').next().unwrap_or("");
177 if !blocked.contains(simple) {
178 import_set.insert(r);
179 }
180 }
181 }
182 let mut imports: Vec<String> = import_set
183 .iter()
184 .map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
185 .collect();
186 imports.sort();
187 for display in imports {
188 out.push_str(&format!("import {};\n", display));
189 }
190 }
191 out.push('\n');
192 out.push_str(&body_buf);
193 Ok(out)
194}
195
196/// Render the class header, fields, methods and nested member classes into
197/// `out` (used at top level and recursively for nested members).
198// `opts` is only consumed by the nested-member recursion below — that
199// is its purpose (propagating emission options into inline children).
200/// One enum constant: the source identifier plus constructor
201/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
202use jdc_core::ir::expr::{ConstVal, Expr};
203use jdc_core::ir::stmt::Stmt;
204struct EnumConst {
205 #[allow(dead_code)]
206 field: String,
207 name: String,
208 extra_args: Vec<Expr>,
209}
210
211/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
212/// static field must be initialized in `<clinit>` by
213/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
214/// always emit. Returns None (caller falls back to the desugared
215/// `/* enum */ class` form) when anything is missing: R8 variance,
216/// constant-specific bodies (the field holds an anonymous subclass), or
217/// a <clinit> that failed to decompile.
218fn collect_enum_constants(
219 pool: &DexPool,
220 class: &PoolClass,
221 ctx: &DexCtx<'_>,
222) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
223 let _ = ctx;
224 let const_fields: Vec<&PoolField> = class
225 .static_fields
226 .iter()
227 .filter(|f| f.access & crate::access::ACC_ENUM != 0)
228 .collect();
229 if const_fields.is_empty() { return None; }
230 let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
231 let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
232
233 // R8/d8 split the constant build across an intermediate local:
234 // Self v0 = new Self("NAME", i, ...);
235 // a = v0; // sput to the ACC_ENUM field
236 // Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
237 // Pass 1 registers each intermediate (or direct) new; pass 2 binds
238 // them to the ACC_ENUM fields; references to the locals are then
239 // rewritten into constant identifiers so the $VALUES build keeps
240 // compiling once the definitions drop.
241 use std::collections::HashMap;
242 let mut const_name: Vec<String> = Vec::new();
243 let mut const_field: Vec<String> = Vec::new();
244 let mut const_extra: Vec<Vec<Expr>> = Vec::new();
245 // Declared ordinal per constant (the ctor's int arg) — the merge at
246 // the end orders field-bound and synthetic constants by it and
247 // demands a dense 0..n run (Java derives ordinals from declaration
248 // position).
249 let mut const_ord: Vec<i64> = Vec::new();
250 let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
251 let mut drop_stmts: Vec<usize> = Vec::new();
252
253 // Collect (immutable borrows) first; the mutable passes come after.
254 if !matches!(&body.body, Stmt::Block(_)) { return None; }
255
256 // Pass 1: definitions. (immutable borrow; rewrite comes later)
257 // Rolling reaching-defs of clinit locals for resolving enum-ctor
258 // extra args (resolve_enum_arg); `tainted` flips at the first
259 // non-straight-line statement — past it, linear-scan defs are no
260 // longer provable and Local args reject the enum mode.
261 let mut defs: HashMap<u32, &Expr> = HashMap::default();
262 let mut tainted = false;
263 let self_name: std::sync::Arc<str> = class.name.as_str().into();
264 let self_ty =
265 jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
266 // A REUSED intermediate local: `v = new Self(..); A = v; v = new
267 // Self(..); B = v;` — the first def is a LocalDef, the rest are
268 // Assigns to the same var. Register every def site; pass 2 resolves
269 // a local sput to the def site CURRENT at that statement (rolling),
270 // not to the var (Kotlin EnumEntries enums, RegexOption: 8→7 count
271 // mismatch used to abort the whole promotion).
272 let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
273 for (i, st) in match &body.body {
274 Stmt::Block(v) => v.iter().enumerate(),
275 _ => return None,
276 } {
277 // (var, New) for both def shapes: LocalDef and Assign-to-local.
278 let def_shape: Option<(u32, &Expr)> = match st {
279 Stmt::LocalDef {
280 var,
281 init: Some(e),
282 ..
283 } => Some((*var, e)),
284 Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
285 match (&**target, &**value) {
286 (Expr::Local { var, .. }, e) => Some((*var, e)),
287 _ => None,
288 }
289 }
290 _ => None,
291 };
292 if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
293 if ncls.as_ref() == class.name && args.len() >= 2 {
294 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
295 (&args[0], &args[1])
296 {
297 if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
298 var_of.insert(var, const_name.len());
299 def_site.insert(i, (var, const_name.len()));
300 const_ord.push(*ord0 as i64);
301 const_name.push(n.to_string());
302 const_field.push(String::new());
303 const_extra.push(resolve_enum_extras(
304 &args[2..],
305 &defs,
306 tainted,
307 &var_of,
308 &const_name,
309 &self_name,
310 &self_ty,
311 )?);
312 drop_stmts.push(i);
313 }
314 }
315 }
316 track_def(st, &mut defs, &mut tainted);
317 }
318
319 // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
320 // Local sputs resolve through the ROLLING def map (updated at each
321 // def-site statement) — a reused `v` must bind to the constant
322 // defined most recently, not to the var's first registration.
323 let mut defs2: HashMap<u32, &Expr> = HashMap::default();
324 let mut tainted2 = false;
325 let mut cur_def: HashMap<u32, usize> = HashMap::default();
326 for (i, st) in match &body.body {
327 Stmt::Block(v) => v.iter().enumerate(),
328 _ => return None,
329 } {
330 if let Some((var, idx)) = def_site.get(&i) {
331 cur_def.insert(*var, *idx);
332 }
333 if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
334 if let Expr::Field {
335 cls,
336 name: fname,
337 is_static: true,
338 ..
339 } = &**target
340 {
341 if cls.as_ref() != class.name {
342 continue;
343 }
344 if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
345 continue;
346 }
347 if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { { return None; } // duplicate assignment
348 }
349 let idx = match &**value {
350 Expr::Local { var, .. } => cur_def.get(var).copied()?,
351 Expr::New { cls: ncls, args, .. }
352 if ncls.as_ref() == class.name && args.len() >= 2 =>
353 {
354 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
355 (&args[0], &args[1])
356 {
357 if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
358 let idx = const_name.len();
359 const_ord.push(*ord0 as i64);
360 const_name.push(n.to_string());
361 const_field.push(String::new());
362 const_extra.push(resolve_enum_extras(
363 &args[2..],
364 &defs2,
365 tainted2,
366 &var_of,
367 &const_name,
368 &self_name,
369 &self_ty,
370 )?);
371 idx
372 } else {
373 { return None; }
374 }
375 }
376 _ => return None,
377 };
378 const_field[idx] = fname.to_string();
379 drop_stmts.push(i);
380 }
381 }
382 track_def(st, &mut defs2, &mut tainted2);
383 }
384 // Own the reaching-def values: defs2 borrows body.body, and the
385 // synthetic-constant scan below mutates the $VALUES array in place.
386 let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
387 .iter()
388 .map(|(k, v)| (*k, (*v).clone()))
389 .collect();
390 drop(defs2);
391 let defs2: std::collections::HashMap<u32, &Expr> =
392 defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
393
394 // Every ACC_ENUM field bound, every intermediate matched.
395 // Every ACC_ENUM FIELD must be bound to a constant. The converse
396 // need not hold: R8 drops the FIELD of a constant nothing reads
397 // externally while the $VALUES array keeps it (weixin lite/api/n's
398 // ON_DESTROY — a pass-1 local with no pass-2 sput). Field-less
399 // constants keep their ctor-string source name.
400 if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
401 return None;
402 }
403 // Obfuscated enums rename the ACC_ENUM FIELD (d/e/f) while the ctor's
404 // name STRING keeps the source identifier — the promoted constant
405 // must be declared under the FIELD name: every reference in the
406 // pool resolves through it. Using the name string broke all
407 // cross-file references (weixin +2.9k when newly-promoted enums
408 // swapped d/e/f for TEXT_ENTER_EDITING/…).
409 for i in 0..const_name.len() {
410 let field = &const_field[i];
411 if !field.is_empty() && field != &const_name[i] {
412 let id = java_ident(field);
413 if id.is_empty() || id.as_ref() != field.as_str() {
414 { return None; }
415 }
416 const_name[i] = field.clone();
417 }
418 }
419 {
420 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
421 if !const_name.iter().all(|c| seen.insert(c.as_str())) { return None; }
422 }
423
424 // Synthetic constants: entries built INLINE inside the $VALUES
425 // array (`new Self("NAME", ord, ..)` with no ACC_ENUM field — R8
426 // drops the field when nothing outside reads it; weixin u2/i's
427 // "CONSTANT"). Promote them so the header declares the constant:
428 // the raw `new` in the array is "无法实例化枚举类", and every
429 // constant after the gap carries a SHIFTED ordinal (e was 2 in the
430 // dex, renders as position 1).
431 let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
432 {
433 let self_cls: &str = class.name.as_str();
434 if let Stmt::Block(vs) = &mut body.body {
435 for st in vs.iter_mut() {
436 crate::passes::walk_stmt_exprs(st, &mut |e| {
437 crate::passes::deep_rewrite(e, &mut |x| {
438 let Expr::NewArray { elem, init: Some(list), .. } = x else {
439 return;
440 };
441 if elem.erased() != JavaType::Object(self_cls.into()) {
442 return;
443 }
444 for slot in list.iter_mut() {
445 let (n, ord, rest) = match slot {
446 Expr::New { cls, args, raw: false, .. }
447 if cls.as_ref() == self_cls && args.len() >= 2 =>
448 {
449 match (&args[0], &args[1]) {
450 (
451 Expr::Const(ConstVal::Str(sv)),
452 Expr::Const(ConstVal::Int(o)),
453 ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
454 _ => continue,
455 }
456 }
457 _ => continue,
458 };
459 if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
460 continue;
461 }
462 if const_name.contains(&n)
463 || synth.iter().any(|(_, s2, _)| *s2 == n)
464 {
465 continue;
466 }
467 let Some(extras) = resolve_enum_extras(
468 &rest,
469 &defs2,
470 tainted2,
471 &var_of,
472 &const_name,
473 &self_name,
474 &self_ty,
475 ) else {
476 continue;
477 };
478 *slot = Expr::Field {
479 owner: None,
480 cls: self_name.clone(),
481 name: std::sync::Arc::from(n.as_str()),
482 ty: self_ty.clone(),
483 is_static: true,
484 };
485 synth.push((ord, n, extras));
486 }
487 });
488 });
489 }
490 }
491 }
492
493 // Pass 3: rewrite references to the intermediate locals into the
494 // constant identifiers (static-field reads on Self). POSITION-AWARE:
495 // a REUSED local must read as the constant defined most recently at
496 // that statement — a var-level map made every `arr[k] = v` in the
497 // $VALUES build reference the LAST constant (weixin +2.9k when this
498 // regressed values() contents).
499 {
500 let mut cur: HashMap<u32, usize> = HashMap::default();
501 if let Stmt::Block(vs) = &mut body.body {
502 for (i, st) in vs.iter_mut().enumerate() {
503 if let Some((var, idx)) = def_site.get(&i) {
504 cur.insert(*var, *idx);
505 }
506 crate::passes::walk_stmt_exprs(st, &mut |e| {
507 crate::passes::deep_rewrite(e, &mut |x| {
508 if let Expr::Local { var, .. } = x {
509 if let Some(&idx) = cur.get(var) {
510 let name: std::sync::Arc<str> =
511 std::sync::Arc::from(const_name[idx].as_str());
512 *x = Expr::Field {
513 owner: None,
514 cls: self_name.clone(),
515 name,
516 ty: self_ty.clone(),
517 is_static: true,
518 };
519 }
520 }
521 });
522 });
523 }
524 }
525 }
526
527 // Pass 4: drop the definitions and their sputs.
528 if let Stmt::Block(vs) = &mut body.body {
529 let drop_set: std::collections::HashSet<usize> =
530 drop_stmts.iter().copied().collect();
531 vs.retain(|_| true);
532 let mut idx = 0usize;
533 vs.retain(|_| {
534 let keep = !drop_set.contains(&idx);
535 idx += 1;
536 keep
537 });
538 }
539 // The resolved args consumed the clinit locals' readers; prune the
540 // now-dead defs from the remnant (impure inits survive as bare
541 // expression statements — drop_dead_locals' standard contract).
542 crate::passes::drop_dead_locals(&mut body.body);
543
544 let mut merged: Vec<(i64, EnumConst)> = const_ord
545 .into_iter()
546 .zip(
547 const_field
548 .into_iter()
549 .zip(const_name)
550 .zip(const_extra)
551 .map(|((field, name), extra_args)| EnumConst {
552 field,
553 name,
554 extra_args,
555 }),
556 )
557 .collect();
558 for (ord, name, extra_args) in synth {
559 merged.push((
560 ord,
561 EnumConst {
562 field: name.clone(),
563 name,
564 extra_args,
565 },
566 ));
567 }
568 merged.sort_by_key(|(o, _)| *o);
569 // Ordinals must be UNIQUE and ASCENDING; gaps are legal — R8 drops
570 // unused constants wholesale (weixin lite/api/n starts at ordinal
571 // 1). Java derives ordinal() from declaration position, so a gap
572 // shifts every later constant. Pad each gap with a synthetic
573 // constant (`_r<k>`) to keep positions faithful — only when no
574 // constant carries extra ctor args (a pad cannot invent them).
575 // The synthetic values()/valueOf() the true-enum render suppresses
576 // then see the pad (values() drift vs the dex $VALUES array — the
577 // compilable-and-ordinal-faithful side of the trade).
578 let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
579 let mut member_taken: jdc_core::FxHashSet<String> = merged
580 .iter()
581 .map(|(_, c)| c.name.clone())
582 .chain(
583 class
584 .static_fields
585 .iter()
586 .chain(class.instance_fields.iter())
587 .map(|f| f.name.to_string()),
588 )
589 .chain(class.all_methods().map(|m| m.name.to_string()))
590 .collect();
591 let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
592 let mut expect: i64 = 0;
593 let mut pad_k = 0u32;
594 for (ord, c) in merged {
595 if ord < expect {
596 { return None; } // duplicate/colliding ordinals — unfaithful
597 }
598 if ord > expect {
599 if has_extras {
600 { return None; } // cannot synthesize the missing ctor args
601 }
602 while expect < ord {
603 let pname = loop {
604 let cand = format!("_r{pad_k}");
605 pad_k += 1;
606 if !member_taken.contains(&cand) {
607 member_taken.insert(cand.clone());
608 break cand;
609 }
610 };
611 padded.push(EnumConst {
612 field: pname.clone(),
613 name: pname,
614 extra_args: Vec::new(),
615 });
616 expect += 1;
617 }
618 }
619 expect = ord + 1;
620 padded.push(c);
621 }
622 let out = padded;
623 Some((out, body))
624}
625
626/// Update the rolling reaching-def map for enum-arg resolution. Only
627/// flat single-target definitions keep the tracking sound; any control
628/// flow taints it (a linear scan can no longer prove WHICH definition
629/// reaches later capture sites).
630fn track_def<'e>(
631 st: &'e Stmt,
632 defs: &mut std::collections::HashMap<u32, &'e Expr>,
633 tainted: &mut bool,
634) {
635 match st {
636 Stmt::LocalDef { var, init, .. } => match init {
637 Some(e) => {
638 defs.insert(*var, e);
639 }
640 None => {
641 defs.remove(var);
642 }
643 },
644 Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
645 if let Expr::Local { var, .. } = &**target {
646 if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
647 defs.insert(*var, value);
648 } else {
649 defs.remove(var);
650 }
651 }
652 }
653 // Plain expression statements (calls) don't define locals.
654 Stmt::ExprStmt(_) => {}
655 _ => *tainted = true,
656 }
657}
658
659/// Resolve every enum-ctor extra arg to a self-contained expression, or
660/// reject the enum mode (None). R8 reuses ONE register across all
661/// constant constructions — revenuecat's LogIntent builds 11 of 12
662/// emoji lists through the same `list` local, reassigned between the
663/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
664/// clinit where no local is in scope: an unresolved `Local` used to
665/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
666/// cannot-find). Each Local is replaced by its reaching pure definition
667/// (recursively) or, when it names an earlier constant's intermediate,
668/// by a static-field reference to that constant. Any expression shape
669/// WITHOUT locals passes through untouched (enum args may be arbitrary
670/// expressions — BinOp/Cast/Method/New — each renders exactly once per
671/// constant, so no duplication concern applies). Only an unresolvable
672/// Local (missing/tainted def, depth > 4) rejects the whole enum
673/// detection; the class then falls back to plain-field rendering, which
674/// always compiles.
675fn resolve_enum_extras(
676 extras: &[Expr],
677 defs: &std::collections::HashMap<u32, &Expr>,
678 tainted: bool,
679 var_of: &std::collections::HashMap<u32, usize>,
680 const_name: &[String],
681 self_name: &std::sync::Arc<str>,
682 self_ty: &jdc_core::ir::expr::TypeRef,
683) -> Option<Vec<Expr>> {
684 extras
685 .iter()
686 .map(|e| {
687 let mut out = e.clone();
688 let mut fail = false;
689 resolve_locals_in(
690 &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
691 &mut fail,
692 );
693 if fail {
694 None
695 } else {
696 Some(out)
697 }
698 })
699 .collect()
700}
701
702#[allow(clippy::too_many_arguments)]
703fn resolve_locals_in(
704 e: &mut Expr,
705 defs: &std::collections::HashMap<u32, &Expr>,
706 tainted: bool,
707 var_of: &std::collections::HashMap<u32, usize>,
708 const_name: &[String],
709 self_name: &std::sync::Arc<str>,
710 self_ty: &jdc_core::ir::expr::TypeRef,
711 depth: u32,
712 fail: &mut bool,
713) {
714 crate::passes::deep_rewrite(e, &mut |x| {
715 if let Expr::Local { var, .. } = x {
716 // An earlier constant's intermediate: a static-field read of
717 // that constant (declared above — backward reference, legal
718 // in enum ctor args).
719 if let Some(&ci) = var_of.get(var) {
720 if let Some(nm) = const_name.get(ci) {
721 *x = Expr::Field {
722 owner: None,
723 cls: self_name.clone(),
724 name: std::sync::Arc::from(nm.as_str()),
725 ty: self_ty.clone(),
726 is_static: true,
727 };
728 return;
729 }
730 }
731 if tainted || depth > 4 {
732 *fail = true;
733 return;
734 }
735 let Some(d) = defs.get(var).copied() else {
736 *fail = true;
737 return;
738 };
739 let mut sub = d.clone();
740 resolve_locals_in(
741 &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
742 depth + 1, fail,
743 );
744 if *fail {
745 return;
746 }
747 *x = sub;
748 }
749 });
750}
751
752/// Render enum-constant constructor arguments via the shared expression
753/// emitter (the VarTable is irrelevant for argument printing — no local
754/// names appear — but the API requires one).
755fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
756 let dummy_vt = jdc_core::var::VarTable::default();
757 let mut p = Printer::new(ctx, &dummy_vt);
758 for (i, a) in args.iter().enumerate() {
759 if i > 0 {
760 out.push_str(", ");
761 }
762 p.expr(a, 0, out);
763 }
764 let _ = pool;
765}
766
767#[allow(clippy::only_used_in_recursion)]
768fn emit_class_body(
769 pool: &DexPool,
770 class: &PoolClass,
771 ctx: &DexCtx<'_>,
772 opts: &ClassOptions,
773 out: &mut String,
774 depth: usize,
775) -> anyhow::Result<()> {
776 let ind = indent(depth);
777 // The class's own header must use the RENAMED display name — the
778 // ctor path (is_init) already renames; a header declaring `class a`
779 // while the file/ctor say `a_2` leaves the ctor looking like a
780 // method with no return type (MinisApp's Y2.a vs y2.a package
781 // case-collision: 2,518 javac parse errors).
782 let cname = crate::apply_class_rename(&class.name);
783 let (_, simple) = split_name(&cname);
784 // A class emitted as its OWN top-level file (depth 0) must declare a
785 // flat `$` name — `class Outer.Inner` is not declarable at file
786 // scope. An INLINE nested member (depth > 0) declares its own
787 // segment inside the parent's body.
788 let simple = if depth == 0 {
789 simple.to_string()
790 } else {
791 // R8 names can END in `$`: an empty last `$`-segment would
792 // render `class {` — keep the whole simple name.
793 let seg = simple.rsplit('$').next().unwrap_or(&simple);
794 if seg.is_empty() {
795 simple.to_string()
796 } else {
797 seg.to_string()
798 }
799 };
800 // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
801 // declared — restricted contextual type names escape here and in
802 // the ctor, file name, and every type reference.
803 let simple = if is_restricted_type_name(&simple) {
804 format!("_{simple}")
805 } else {
806 simple
807 };
808 let is_iface = class.is_interface();
809 let is_enum = class.is_enum();
810
811 let mut head = String::new();
812 let a = class.access;
813 if a & ACC_PUBLIC != 0 {
814 head.push_str("public ");
815 }
816 // Inline nested members need their `static` (interfaces/annotations
817 // are implicitly static; a missing `static` on a member class makes
818 // every `new Report(...)` site an "outer instance required" error).
819 if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
820 head.push_str("static ");
821 }
822 if a & ACC_FINAL != 0 && !is_enum {
823 head.push_str("final ");
824 }
825 if a & ACC_ABSTRACT != 0 && !is_iface {
826 head.push_str("abstract ");
827 }
828 if a & ACC_ANNOTATION != 0 {
829 head.push('@');
830 }
831 let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
832 collect_enum_constants(pool, class, ctx)
833 } else {
834 None
835 };
836 if let Some(ecs) = &enum_consts {
837 // True `enum` declaration: constants render in the header, the
838 // desugared boilerplate (const fields, their <clinit> inits —
839 // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
840 // disappears. R8-renamed values()/valueOf() stay (they do not
841 // collide with the compiler-generated ones); javac-named ones
842 // are skipped at the method loop below.
843 head.push_str("enum ");
844 head.push_str(&java_ident(&simple));
845 let _ = ecs;
846 } else if is_enum {
847 // An enum with constant-specific bodies carries ACC_ABSTRACT —
848 // `abstract final` is an illegal modifier combination; abstract
849 // (already pushed above) suppresses the hardcoded final.
850 let abstract_ = a & ACC_ABSTRACT != 0;
851 head.push_str(if abstract_ {
852 "/* enum */ class "
853 } else {
854 "/* enum */ final class "
855 });
856 head.push_str(&java_ident(&simple));
857 } else if is_iface {
858 head.push_str("interface ");
859 head.push_str(&java_ident(&simple));
860 } else {
861 head.push_str("class ");
862 head.push_str(&java_ident(&simple));
863 }
864 // The obscured-super import was emitted at the package line; the
865 // clause must render the SIMPLE name (the qualified form binds to
866 // the class itself even with the import present).
867 let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
868 if is_iface {
869 // A @interface cannot declare extends at all (JLS 9.6): the dex
870 // interface table lists java/lang/annotation/Annotation for
871 // every annotation type — rendering it produced "对于
872 // @interfaces, 不允许 'extends'" (weibo 1100).
873 let is_annot = a & ACC_ANNOTATION != 0;
874 if !class.interfaces.is_empty() && !is_annot {
875 head.push_str(" extends ");
876 head.push_str(&join_dotted(pool, &class.interfaces));
877 }
878 } else {
879 if let Some(sup) = &class.super_name {
880 if sup != "java/lang/Object" && !is_enum {
881 head.push_str(" extends ");
882 head.push_str(&render_super(sup));
883 }
884 }
885 if !class.interfaces.is_empty() {
886 head.push_str(if is_iface {
887 " extends "
888 } else {
889 " implements "
890 });
891 head.push_str(&join_dotted(pool, &class.interfaces));
892 }
893 }
894
895 out.push_str(&ind);
896 out.push_str(&head);
897 out.push_str(" {\n");
898
899 // True-enum constant list: the constants lead the body, ahead of
900 // any remaining fields.
901 if let Some((ecs, _)) = &enum_consts {
902 for (i, ec) in ecs.iter().enumerate() {
903 out.push_str(&indent(depth + 1));
904 out.push_str(&java_ident(&ec.name));
905 if !ec.extra_args.is_empty() {
906 let mut line = String::from("(");
907 render_enum_args(ctx, pool, &ec.extra_args, &mut line);
908 line.push(')');
909 out.push_str(&line);
910 }
911 if i + 1 < ecs.len() {
912 out.push_str(",\n");
913 } else {
914 out.push_str(";\n");
915 }
916 }
917 out.push('\n');
918 }
919
920 // Fields.
921 // Kotlin `object`/lazy singletons: the dex static value for INSTANCE
922 // is null while the clinit assigns it — rendering `= null` as the
923 // field initializer made the clinit assignment illegal ("无法为
924 // static final 变量 INSTANCE 分配值", okio SegmentPool family). A
925 // static final whose static value is null AND which the clinit
926 // sputs renders as a blank final. Scan the clinit's raw SPuts once.
927 let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
928 // Gate: only classes with a null-valued final static can produce
929 // a blank final — skip the clinit decode otherwise (decoding it
930 // for every class cost seconds on 98k-class corpora).
931 (|| {
932 let has_null_final = class.static_fields.iter().enumerate().any(|(i, f)| {
933 f.access & crate::access::ACC_FINAL != 0
934 && matches!(class.static_values.get(i), Some(StaticValue::Null))
935 });
936 if !has_null_final {
937 return None;
938 }
939 let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
940 let dex = pool.dex(m.dex_idx)?;
941 let ci = dex.code_at(m.code_off)?;
942 let mut set: jdc_core::FxHashSet<(
943 std::sync::Arc<str>,
944 std::sync::Arc<str>,
945 )> = jdc_core::FxHashSet::default();
946 for ins in &ci.insns {
947 if let InsnKind::SPut { field_idx, .. } = ins.kind {
948 let fid = dex.field(field_idx);
949 // clinit only writes own-class fields here; match
950 // (name, type) against the field list below.
951 set.insert((
952 std::sync::Arc::from(dex.string(fid.name_idx)),
953 std::sync::Arc::from(dex.type_name(fid.type_idx)),
954 ));
955 }
956 }
957 Some(set)
958 })();
959 let mut field_emitted = false;
960 for (i, f) in class.static_fields.iter().enumerate() {
961 // Enum constant fields became the header list above.
962 if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
963 continue;
964 }
965 if field_emitted || !class.instance_fields.is_empty() {
966 out.push('\n');
967 }
968 field_emitted = true;
969 // A blank-final: static final, null static value, clinit-assigned.
970 let blank_final = f.access & crate::access::ACC_FINAL != 0
971 && matches!(class.static_values.get(i), Some(StaticValue::Null))
972 && clinit_sputs
973 .as_ref()
974 .is_some_and(|s| {
975 s.contains(&(
976 std::sync::Arc::from(f.name.as_str()),
977 std::sync::Arc::from(f.desc.as_str()),
978 ))
979 });
980 emit_field(
981 pool,
982 f,
983 if blank_final {
984 None
985 } else {
986 class.static_values.get(i)
987 },
988 &class.name,
989 out,
990 depth + 1,
991 true,
992 class.is_interface(),
993 );
994 }
995 if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
996 out.push('\n');
997 }
998 for (i, f) in class.instance_fields.iter().enumerate() {
999 if i > 0 {
1000 out.push('\n');
1001 }
1002 emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1003 }
1004
1005 // Methods.
1006 // Duplicated (name, parameter-types) pairs in one class are javac
1007 // "method is already defined" errors (weibo: 11.5k hits). Source
1008 // cannot declare them, so exactly one survives: covariant bridges
1009 // (`Object get(int)` beside `ByteString get(int)` — the compiler
1010 // GENERATES bridges), plus R8 output that lost the bridge flag
1011 // (okio Buffer.clone, interface getView re-declarations). A
1012 // non-bridge method outranks a bridge for the same signature
1013 // (bridge bodies are delegation stubs); first occurrence otherwise.
1014 // Never drop a signature outright — the last method standing for a
1015 // key is always rendered.
1016 fn sig_key(m: &PoolMethod) -> (&str, &str) {
1017 let d: &str = &m.desc;
1018 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1019 let hi = d.find(')').unwrap_or(d.len());
1020 (&*m.name, &d[lo..hi])
1021 }
1022 let methods: Vec<&PoolMethod> = class.all_methods().collect();
1023 let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1024 for (i, m) in methods.iter().enumerate() {
1025 let key = sig_key(m);
1026 match claim.get(&key) {
1027 Some(&j)
1028 if m.access & crate::access::ACC_BRIDGE == 0
1029 && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1030 {
1031 claim.insert(key, i);
1032 }
1033 None => {
1034 claim.insert(key, i);
1035 }
1036 _ => {}
1037 }
1038 }
1039 // NOTE: a same-signature bridge is already retired by the claim map
1040 // above. An ERASURE-shaped bridge (SAM/variance: params are the
1041 // Object-erased types of a sibling's) must NOT be skipped: with raw
1042 // (non-generic) interface rendering the bridge is exactly what
1043 // satisfies the interface — skipping it turned every Kotlin lambda
1044 // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
1045 // +17), while the "对invoke的引用不明确" it appeared to fix was a
1046 // missing-kotlin-classpath cascade, not a ddc bug.
1047 let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
1048 for (i, m) in methods.iter().enumerate() {
1049 if &*m.name == "<clinit>" {
1050 continue; // rendered after the fields
1051 }
1052 if claim.get(&sig_key(m)) != Some(&i) {
1053 continue;
1054 }
1055 // True-enum rendering: javac auto-generates values()/valueOf()
1056 // — the original-named ones must not re-declare (R8-renamed
1057 // copies stay, they do not collide).
1058 if let Some((ecs, _)) = &enum_consts {
1059 let d = m.parsed_desc();
1060 let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
1061 let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
1062 let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
1063 let one_str = d
1064 .as_ref()
1065 .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
1066 .unwrap_or(false);
1067 let static_ = m.is_static();
1068 if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
1069 continue;
1070 }
1071 if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
1072 continue;
1073 }
1074 // Enum constructors must be private in source form.
1075 let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
1076 let mut c = (*m).clone();
1077 c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
1078 Some(c)
1079 } else {
1080 None
1081 };
1082 let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
1083 let mark = out.len();
1084 if emitted_any {
1085 out.push('\n');
1086 }
1087 if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
1088 emitted_any = true;
1089 } else {
1090 out.truncate(mark);
1091 }
1092 let _ = ecs;
1093 continue;
1094 }
1095 let mark = out.len();
1096 if emitted_any {
1097 out.push('\n');
1098 }
1099 if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
1100 emitted_any = true;
1101 } else {
1102 out.truncate(mark);
1103 }
1104 }
1105 // Inherited-ctor bridges: dex method refs resolve through the
1106 // hierarchy, so `new C(args)` against a class C with NO declared
1107 // <init> legally targets the SUPERCLASS ctor (weixin tenpay
1108 // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
1109 // has no inherited constructors: without a bridge, C's implicit
1110 // default ctor is the only one and every arg-carrying construction
1111 // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
1112 // Mirror the nearest ctor-declaring ancestor's public/protected
1113 // ctors as thin `super(..)` delegations.
1114 if !class.is_interface()
1115 && enum_consts.is_none()
1116 && !class.all_methods().any(|m| &*m.name == "<init>")
1117 {
1118 if let Some(mut sup) = class.super_name.clone() {
1119 loop {
1120 if sup == "java/lang/Object" {
1121 break;
1122 }
1123 let Some(sc) = pool.get(&sup) else { break };
1124 let ctors: Vec<&PoolMethod> = sc
1125 .all_methods()
1126 .filter(|m| &*m.name == "<init>")
1127 .collect();
1128 if !ctors.is_empty() {
1129 for sm in ctors {
1130 if sm.access & crate::access::ACC_PRIVATE != 0
1131 || (sm.access
1132 & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
1133 == 0)
1134 {
1135 continue; // private/package-private: no legal bridge
1136 }
1137 let Some(d) = sm.parsed_desc() else { continue };
1138 let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
1139 "public "
1140 } else {
1141 "protected "
1142 };
1143 if emitted_any {
1144 out.push('\n');
1145 }
1146 out.push_str(&format!(" {}", " ".repeat(depth)));
1147 out.push_str(mods);
1148 out.push_str(&java_ident(&simple));
1149 out.push('(');
1150 let mut names = Vec::with_capacity(d.args.len());
1151 for (i, a) in d.args.iter().enumerate() {
1152 if i > 0 {
1153 out.push_str(", ");
1154 }
1155 out.push_str(&type_name(pool, a));
1156 let nm = format!("p{}", i + 1);
1157 out.push(' ');
1158 out.push_str(&nm);
1159 names.push(nm);
1160 }
1161 out.push_str(") {\n");
1162 out.push_str(&format!(" {} super({});\n", " ".repeat(depth), names.join(", ")));
1163 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
1164 emitted_any = true;
1165 }
1166 break;
1167 }
1168 match &sc.super_name {
1169 Some(n) => sup = n.clone(),
1170 None => break,
1171 }
1172 }
1173 }
1174 }
1175
1176 // Static initializer. INTERFACES cannot carry a `static { }` block in
1177 // Java — their clinit only assigns constants, which static_values (or
1178 // the `= null` default) already render as field initializers; skip
1179 // the block entirely.
1180 let skip_clinit = class.is_interface();
1181 if let Some((_, clinit_body)) = enum_consts.take() {
1182 // True-enum <clinit>: the constant assignments are already gone;
1183 // render the remainder (the $VALUES array build) directly.
1184 // Skip an empty remainder (all statements were constant inits).
1185 let empty = match &clinit_body.body {
1186 Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
1187 _ => false,
1188 };
1189 if !empty {
1190 if emitted_any {
1191 out.push('\n');
1192 }
1193 out.push_str(&indent(depth + 1));
1194 out.push_str("static {\n");
1195 let p = Printer::new(ctx, &clinit_body.vt);
1196 let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
1197 for line in text.lines() {
1198 if line.trim().is_empty() {
1199 continue;
1200 }
1201 out.push_str(line);
1202 out.push('\n');
1203 }
1204 out.push_str(&indent(depth + 1));
1205 out.push_str("}\n");
1206 emitted_any = true;
1207 }
1208 } else if let Some(clinit) = (!skip_clinit)
1209 .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
1210 .flatten()
1211 {
1212 let mark = out.len();
1213 if emitted_any {
1214 out.push('\n');
1215 }
1216 if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
1217 emitted_any = true;
1218 } else {
1219 out.truncate(mark);
1220 }
1221 }
1222
1223 // Nested member classes (clean `$` tails only; anonymous/local/lambda
1224 // classes are emitted as their own top-level files by the driver).
1225 for nested in nested_members(pool, class, ctx) {
1226 if emitted_any {
1227 out.push('\n');
1228 }
1229 let nested_ctx = DexCtx::new(pool, nested);
1230 out.push('\n');
1231 emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
1232 emitted_any = true;
1233 }
1234
1235 out.push_str(&ind);
1236 out.push_str("}\n");
1237 Ok(())
1238}
1239
1240/// Member classes of `class` (direct children by the `$` chain / member
1241/// annotations), excluding anonymous / local / lambda shapes.
1242fn nested_members<'a>(
1243 pool: &'a DexPool,
1244 class: &'a PoolClass,
1245 ctx: &DexCtx<'_>,
1246) -> Vec<&'a PoolClass> {
1247 let mut out = Vec::new();
1248 for name in pool.children_of(&class.name) {
1249 let Some(pc) = pool.get(name) else { continue };
1250 // Only a REAL `outer$tail` name is an inline member: the
1251 // children index also carries annotation-derived outers
1252 // (EnclosingClass) with no naming relationship to the child
1253 // (obfuscated apps pair a 1-char name with a long enclosing
1254 // descriptor) — the blind slice panicked there (alipay
1255 // `a.a.a.a.c`, exposed once children_of actually returned data).
1256 let Some(rest) = name
1257 .strip_prefix(class.name.as_str())
1258 .and_then(|t| t.strip_prefix('$'))
1259 else {
1260 continue;
1261 };
1262 if rest.is_empty() || rest.starts_with('-') {
1263 continue;
1264 }
1265 if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
1266 continue;
1267 }
1268 let tail = rest.rsplit('$').next().unwrap_or(rest);
1269 if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
1270 continue; // anonymous
1271 }
1272 if tail.starts_with(|c: char| c.is_ascii_digit()) {
1273 continue; // local
1274 }
1275 out.push(pc);
1276 }
1277 out
1278}
1279
1280// Eight parameters are all load-bearing (pool, field, static value,
1281// owner class, output, depth, staticness, interface-init requirement);
1282// bundling them into a struct would obscure each call site.
1283#[allow(clippy::too_many_arguments)]
1284fn emit_field(
1285 pool: &DexPool,
1286 f: &crate::PoolField,
1287 init: Option<&StaticValue>,
1288 f_class: &str,
1289 out: &mut String,
1290 depth: usize,
1291 is_static: bool,
1292 require_init: bool,
1293) {
1294 let ind = indent(depth);
1295 let mut line = String::new();
1296 let a = f.access;
1297 if a & ACC_PUBLIC != 0 {
1298 line.push_str("public ");
1299 } else if a & ACC_PRIVATE != 0 {
1300 line.push_str("private ");
1301 } else if a & ACC_PROTECTED != 0 {
1302 line.push_str("protected ");
1303 }
1304 if is_static {
1305 line.push_str("static ");
1306 }
1307 if a & ACC_FINAL != 0 {
1308 line.push_str("final ");
1309 }
1310 if a & ACC_SYNTHETIC != 0 {
1311 line.push_str("/* synthetic */ ");
1312 }
1313 if a & ACC_TRANSIENT_HINT != 0 {
1314 line.push_str("transient ");
1315 }
1316 if a & ACC_VOLATILE_HINT != 0 {
1317 line.push_str("volatile ");
1318 }
1319 let ty = type_name(pool, &desc_type(&f.desc));
1320 line.push_str(&ty);
1321 line.push(' ');
1322 let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
1323 line.push_str(&java_ident(fname));
1324 let mut rendered = None;
1325 if let Some(v) = init {
1326 rendered = render_static_value(pool, v, f_class);
1327 }
1328 if rendered.is_none() && require_init {
1329 // Interface fields MUST have an initializer in Java; the dex may
1330 // not carry a static_values entry for a compile-time-constant the
1331 // compiler folded away. Keep it compilable.
1332 let default = match desc_type(&f.desc) {
1333 JavaType::Boolean => "false",
1334 JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
1335 JavaType::Long => "0L",
1336 JavaType::Float => "0.0F",
1337 JavaType::Double => "0.0",
1338 _ => "null",
1339 };
1340 rendered = Some(default.to_string());
1341 }
1342 if let Some(text) = rendered {
1343 // A long field holding an `Int(i64)` static value needs the `L`
1344 // suffix: without it the literal is an int and overflows
1345 // (`long d = -6343169151696340687` failed javac).
1346 let text = if matches!(desc_type(&f.desc), JavaType::Long)
1347 && text.chars().all(|c| c.is_ascii_digit() || c == '-')
1348 {
1349 format!("{text}L")
1350 } else {
1351 text
1352 };
1353 line.push_str(" = ");
1354 line.push_str(&text);
1355 }
1356 line.push(';');
1357 out.push_str(&ind);
1358 out.push_str(&line);
1359 out.push('\n');
1360}
1361
1362// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
1363// volatile(0x40)/transient(0x80) for fields.
1364pub const ACC_VOLATILE_HINT: u32 = 0x40;
1365pub const ACC_TRANSIENT_HINT: u32 = 0x80;
1366
1367fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
1368 Some(match v {
1369 StaticValue::Int(i) => i.to_string(),
1370 StaticValue::Float(f) => format_float(*f as f64, true),
1371 StaticValue::Double(d) => format_float(*d, false),
1372 StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
1373 StaticValue::Type(t) => format!("{}.class", dotted(t)),
1374 StaticValue::Boolean(b) => b.to_string(),
1375 StaticValue::Null => "null".into(),
1376 StaticValue::Field(cls, name) => {
1377 let n = java_ident(name);
1378 if cls == owner {
1379 n.into_owned()
1380 } else {
1381 format!("{}.{}", print_class_name(pool, cls), n)
1382 }
1383 }
1384 StaticValue::Other => return None,
1385 })
1386}
1387
1388/// Render one method straight into the class buffer `out`. Returns
1389/// whether anything was written (skips: no descriptor, deferred to a
1390/// monitored thread). The old shape returned a per-method String that
1391/// the caller copied in — one extra full copy of every method body.
1392/// `enum_promoted`: the class rendered as a true `enum` declaration
1393/// (constants in the header), which changes what a ctor signature may
1394/// declare.
1395fn emit_method(
1396 pool: &DexPool,
1397 class: &PoolClass,
1398 ctx: &DexCtx<'_>,
1399 m: &PoolMethod,
1400 depth: usize,
1401 enum_promoted: bool,
1402 out: &mut String,
1403) -> anyhow::Result<bool> {
1404 let ind = indent(depth);
1405 let desc = m.parsed_desc();
1406
1407 // Signature.
1408 let mut sig = String::with_capacity(192);
1409 let a = m.access;
1410 if a & ACC_PUBLIC != 0 {
1411 sig.push_str("public ");
1412 } else if a & ACC_PRIVATE != 0 {
1413 sig.push_str("private ");
1414 } else if a & ACC_PROTECTED != 0 {
1415 sig.push_str("protected ");
1416 }
1417 let is_clinit = &*m.name == "<clinit>";
1418 let is_init = &*m.name == "<init>";
1419 if a & ACC_STATIC != 0 || is_clinit {
1420 sig.push_str("static ");
1421 }
1422 if a & ACC_FINAL != 0 {
1423 sig.push_str("final ");
1424 }
1425 // `abstract synchronized` is an illegal combination — obfuscated
1426 // builds mark abstract bridges synchronized (WhatsApp
1427 // SQLiteOpenHelper).
1428 if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
1429 sig.push_str("synchronized ");
1430 }
1431 if a & ACC_NATIVE != 0 {
1432 sig.push_str("native ");
1433 }
1434 if a & ACC_ABSTRACT != 0 {
1435 sig.push_str("abstract ");
1436 }
1437 if a & ACC_SYNTHETIC != 0 {
1438 sig.push_str("/* synthetic */ ");
1439 }
1440
1441 // Body (needed for parameter names even for abstract methods).
1442 let mut body = decompile_method(pool, class, m).ok().flatten();
1443 // An interface method WITH a body is a `default` method (JLS 9.4.3)
1444 // unless static/private — dex carries no `default` flag, so the
1445 // plain form rendered an abstract signature with a body and javac
1446 // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
1447 // default REQUIRES a body: only push it once the body is confirmed
1448 // (a failed decompile renders a body-less declaration).
1449 if class.is_interface()
1450 && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
1451 && body.is_some()
1452 {
1453 sig.insert_str(0, "default ");
1454 }
1455 let param_names: Vec<String> = body
1456 .as_ref()
1457 .map(|b| {
1458 let mut ps: Vec<(u16, String)> =
1459 b.vt.vars
1460 .iter()
1461 .filter(|v| v.is_param && v.name != "this")
1462 .map(|v| (v.slot, v.name.clone()))
1463 .collect();
1464 ps.sort_by_key(|(s, _)| *s);
1465 ps.into_iter().map(|(_, n)| n).collect()
1466 })
1467 .unwrap_or_else(|| {
1468 (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
1469 .map(|i| format!("p{}", i + 1))
1470 .collect()
1471 });
1472
1473 // Non-static member-inner ctor: the synthetic outer instance rides
1474 // as args[0] (typed as the direct enclosing class). Every emission
1475 // site already passes it implicitly — qualified `outer.new Inner(..)`,
1476 // `this.new Inner(..)` from inside the outer, `super(..)` after
1477 // skip_outer_arg — so the signature drops the param and the body's
1478 // references become `Outer.this`. Without this, every construction
1479 // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
1480 // guava androidx inner-class families, d8 capture lambdas).
1481 let mut inner_arg0 = 0usize;
1482 if is_init {
1483 let tail = class.name.rsplit('$').next().unwrap_or("");
1484 let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
1485 if let Some(d) = desc.as_ref() {
1486 if let Some(JavaType::Object(outer)) = d.args.first() {
1487 // The direct enclosing class: nesting annotation when
1488 // present, else the `$`-chain parent (find_outer_name —
1489 // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
1490 // a plain rsplit leaves a trailing `$`). It must agree
1491 // with the this$0 type for the rewrite to fire.
1492 let enclosing: Option<String> = class
1493 .nesting
1494 .enclosing_class
1495 .clone()
1496 .or_else(|| crate::find_outer_name(pool, &class.name));
1497 let direct = enclosing.as_deref() == Some(outer.as_ref());
1498 if direct && !digit_simple && ctx.class_has_this0(&class.name) {
1499 let param0 = body.as_ref().and_then(|b| {
1500 b.vt.vars
1501 .iter()
1502 .find(|v| v.is_param && v.name != "this")
1503 .map(|v| v.id)
1504 });
1505 let written = match (&body, param0) {
1506 (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
1507 _ => false,
1508 };
1509 if let (Some(p0), false) = (param0, written) {
1510 if let Some(b) = body.as_mut() {
1511 // this()-delegations to the SAME class drop the
1512 // outer arg (all of its ctors share the strip);
1513 // a super target joins when it is an inner of
1514 // the same enclosing family.
1515 let mut eligible: Vec<String> = vec![class.name.clone()];
1516 if let Some(sup) = &class.super_name {
1517 let sup_enclosing =
1518 crate::find_outer_name(pool, sup);
1519 if sup_enclosing.as_deref() == Some(outer.as_ref())
1520 && ctx.class_has_this0(sup)
1521 {
1522 eligible.push(sup.clone());
1523 }
1524 }
1525 let renamed = crate::apply_class_rename(outer);
1526 let display = dotted(renamed.as_ref());
1527 let ty =
1528 jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
1529 crate::passes::rewrite_inner_ctor_outer_param(
1530 &mut b.body,
1531 p0,
1532 &display,
1533 &ty,
1534 &eligible,
1535 );
1536 inner_arg0 = 1;
1537 }
1538 }
1539 }
1540 }
1541 }
1542 }
1543
1544 if is_clinit {
1545 // `static { ... }` — the caller strips the method name/params.
1546 } else {
1547 let Some(d) = &desc else { return Ok(false) };
1548 if is_init {
1549 // The ctor name must equal the DECLARED class name of its
1550 // file: flat `$` at depth 0 (own file), own segment when
1551 // inlined in the parent at depth > 0. Case-renamed classes
1552 // use their display name.
1553 let dname = crate::apply_class_rename(&class.name);
1554 let (_, simple) = split_name(&dname);
1555 // emit_method's depth is the METHOD indent = class depth + 1:
1556 // own-file classes (depth 0 header → method depth 1) need the
1557 // flat `$` ctor name; inline nested members use their segment.
1558 let base = if depth <= 1 {
1559 simple.to_string()
1560 } else {
1561 // R8 names can END in `$` (`ThreadMsg$$$`): the last
1562 // `$`-segment is empty — keep the whole simple name.
1563 let seg = simple.rsplit('$').next().unwrap_or(&simple);
1564 if seg.is_empty() {
1565 simple.to_string()
1566 } else {
1567 seg.to_string()
1568 }
1569 };
1570 let name = if is_restricted_type_name(&base) {
1571 format!("_{base}")
1572 } else {
1573 base
1574 };
1575 sig.push_str(&java_ident(&name));
1576 } else {
1577 sig.push_str(&type_name(pool, &d.ret));
1578 sig.push(' ');
1579 let mname =
1580 jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
1581 sig.push_str(&java_ident(mname));
1582 }
1583 sig.push('(');
1584 // A promoted enum ctor's dex descriptor carries the compiler-
1585 // synthesized `(String name, int ordinal)` prefix — JLS forbids
1586 // declaring those (they are implicit in the `A(args)` constant
1587 // declarations the promotion emits, and strip_enum_ctor_super
1588 // already removed the `super(name, ordinal, ..)` delegation).
1589 // Skip the pair in the signature, or every constant declaration
1590 // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
1591 // when the body never reads the two params — code that genuinely
1592 // uses them keeps the declared form.
1593 let mut arg0 = 0;
1594 if enum_promoted
1595 && is_init
1596 && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
1597 && matches!(d.args.get(1), Some(JavaType::Int))
1598 {
1599 // The Kotlin default-arg bridge ctor reads name/ordinal in
1600 // its `this(str, p2, ..)` delegation only — the LEADING pair
1601 // of a this()-delegation drops together with the params, so
1602 // those reads do not block the strip (the constants' extra
1603 // args match the bridge's user params, not the 2-param user
1604 // ctor).
1605 let synthetic: Vec<u32> = body
1606 .as_ref()
1607 .map(|b| {
1608 b.vt
1609 .vars
1610 .iter()
1611 .filter(|v| v.is_param && v.name != "this")
1612 .take(2)
1613 .map(|v| v.id)
1614 .collect()
1615 })
1616 .unwrap_or_default();
1617 // The delegation-leading extension only applies to the
1618 // Kotlin default-arg BRIDGE: its descriptor ends with
1619 // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
1620 // ctor whose first two params are (String, int) and merely
1621 // forwards them must keep its signature (weixin +2.9k when
1622 // ungated).
1623 let is_bridge = matches!(
1624 d.args.last(),
1625 Some(JavaType::Object(m)) if m.as_ref() == "kotlin/jvm/internal/DefaultConstructorMarker"
1626 );
1627 let mut lead = [0usize, 0usize];
1628 let refs_ok = body.as_ref().is_some_and(|b| {
1629 let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
1630 let mut c = b.body.clone();
1631 let cls_name = class.name.as_str();
1632 crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
1633 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
1634 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
1635 for (k, a) in args.iter().enumerate() {
1636 if let Expr::Local { var: v, .. } = a {
1637 if let Some(pi) = synthetic.iter().position(|p| p == v) {
1638 if k == pi && k < 2 {
1639 lead[pi] += 1;
1640 }
1641 }
1642 }
1643 }
1644 }
1645 }
1646 });
1647 synthetic.iter().enumerate().all(|(pi, id)| {
1648 let total = uses.get(id).copied().unwrap_or(0);
1649 if is_bridge {
1650 total == lead[pi]
1651 } else {
1652 total == 0
1653 }
1654 })
1655 });
1656 if refs_ok {
1657 arg0 = 2;
1658 // Drop the leading (name, ordinal) args of the this()
1659 // delegations.
1660 if let (Some(b), true) = (body.as_mut(), is_bridge) {
1661 let cls_name = class.name.as_str();
1662 let syn = synthetic;
1663 crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
1664 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
1665 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
1666 let mut drop_n = 0;
1667 for a in args.iter().take(2) {
1668 if let Expr::Local { var: v, .. } = a {
1669 if syn.contains(v) && drop_n == v - syn[0] {
1670 drop_n += 1;
1671 continue;
1672 }
1673 }
1674 break;
1675 }
1676 for _ in 0..drop_n {
1677 args.remove(0);
1678 }
1679 }
1680 }
1681 });
1682 }
1683 }
1684 }
1685 let n = d.args.len();
1686 let varargs = a & ACC_VARARGS != 0 && n > 0;
1687 let arg0 = arg0 + inner_arg0;
1688 for (i, arg) in d.args.iter().enumerate().skip(arg0) {
1689 if i > arg0 {
1690 sig.push_str(", ");
1691 }
1692 let name = param_names
1693 .get(i)
1694 .cloned()
1695 .unwrap_or_else(|| format!("p{}", i));
1696 if varargs && i + 1 == n {
1697 if let JavaType::Array(inner) = arg {
1698 sig.push_str(&type_name(pool, inner));
1699 sig.push_str("...");
1700 } else {
1701 sig.push_str(&type_name(pool, arg));
1702 }
1703 } else {
1704 sig.push_str(&type_name(pool, arg));
1705 }
1706 sig.push(' ');
1707 // Param names come from dex debug info — obfuscated apps
1708 // name them `_` (reserved since Java 9) or after keywords.
1709 sig.push_str(&java_ident(&name));
1710 }
1711 sig.push(')');
1712 }
1713
1714 if is_clinit {
1715 let Some(b) = body else { return Ok(false) };
1716 // Direct render: the printer starts at the method's ABSOLUTE
1717 // indent and appends into the same buffer that carries the
1718 // header — no intermediate body string, no per-line re-indent
1719 // pass (two full copies of every method body saved).
1720 out.push_str(&ind);
1721 out.push_str("static {\n");
1722 let hdr = out.len();
1723 let printer = Printer::new(ctx, &b.vt)
1724 .with_indent(depth + 1)
1725 .with_output(std::mem::take(out));
1726 let t_print = std::time::Instant::now();
1727 let mut rendered = printer.into_string(&b.body);
1728 crate::method::phase_hit(3, t_print);
1729 if rendered[hdr..].trim().is_empty() {
1730 rendered.truncate(hdr);
1731 }
1732 rendered.push_str(&ind);
1733 rendered.push_str("}\n");
1734 *out = rendered;
1735 return Ok(true);
1736 }
1737 if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
1738 out.push_str(&ind);
1739 out.push_str(&sig);
1740 out.push_str(";\n");
1741 return Ok(true);
1742 }
1743 let Some(b) = body else { return Ok(false) };
1744
1745 // Direct render at the absolute indent level (see the clinit path).
1746 let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
1747 match &b.desc.ret {
1748 JavaType::Boolean => {
1749 printer = printer.with_ret_bool(true);
1750 }
1751 JavaType::Char => {
1752 printer = printer.with_ret_char(true);
1753 }
1754 JavaType::Byte => {
1755 printer = printer.with_ret_narrow(true, false);
1756 }
1757 JavaType::Short => {
1758 printer = printer.with_ret_narrow(false, true);
1759 }
1760 _ => {}
1761 }
1762 out.push_str(&ind);
1763 out.push_str(&sig);
1764 out.push_str(" {\n");
1765 let hdr = out.len();
1766 let printer = printer.with_output(std::mem::take(out));
1767 let t_print = std::time::Instant::now();
1768 let mut rendered = printer.into_string(&b.body);
1769 crate::method::phase_hit(3, t_print);
1770 if rendered[hdr..].trim().is_empty() {
1771 rendered.truncate(hdr);
1772 }
1773 rendered.push_str(&ind);
1774 rendered.push_str("}\n");
1775 *out = rendered;
1776 Ok(true)
1777}
1778
1779// ---------------------------------------------------------------------------
1780// Naming helpers
1781// ---------------------------------------------------------------------------
1782
1783fn indent(depth: usize) -> String {
1784 " ".repeat(depth)
1785}
1786
1787/// `$`-separated nesting rendered with dots — but ONLY when every
1788/// segment is a clean Java identifier (a genuine member class chain).
1789/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
1790/// `...$$inlined$collect$1`) and local-class tails are NOT member
1791/// classes Java can name; the whole name stays flat with `$` (ddc emits
1792/// them as their own top-level files).
1793/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
1794/// any other non-identifier character) is not legal Java. Deterministic
1795/// mapping, applied identically at declaration and call sites.
1796pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1797 // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
1798 // (millions per APK), where it profiled as __NSGetEnviron.
1799 static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1800 if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
1801 eprintln!("[ident] {name:?}");
1802 }
1803 let clean = name
1804 .chars()
1805 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
1806 // Kotlin names fields/methods `default` (Companion.default) — no Java
1807 // program can declare or reference a keyword; the identical mapping
1808 // lives in jdc-core's call-site sanitizer.
1809 let keyword = matches!(
1810 name,
1811 "abstract"
1812 | "assert"
1813 | "boolean"
1814 | "break"
1815 | "byte"
1816 | "case"
1817 | "catch"
1818 | "char"
1819 | "class"
1820 | "const"
1821 | "continue"
1822 | "default"
1823 | "do"
1824 | "double"
1825 | "else"
1826 | "enum"
1827 | "extends"
1828 | "final"
1829 | "finally"
1830 | "float"
1831 | "for"
1832 | "goto"
1833 | "if"
1834 | "implements"
1835 | "import"
1836 | "instanceof"
1837 | "int"
1838 | "interface"
1839 | "long"
1840 | "native"
1841 | "new"
1842 | "package"
1843 | "private"
1844 | "protected"
1845 | "public"
1846 | "return"
1847 | "short"
1848 | "static"
1849 | "strictfp"
1850 | "super"
1851 | "switch"
1852 | "synchronized"
1853 | "this"
1854 | "throw"
1855 | "throws"
1856 | "transient"
1857 | "try"
1858 | "void"
1859 | "volatile"
1860 | "while"
1861 | "true"
1862 | "false"
1863 | "null"
1864 // `_` is a reserved identifier since Java 9 (Alipay's
1865 // instant-run fields are named `_`) — the `_<name>` mapping
1866 // turns it into `__`, matching jdc-core's call sites.
1867 | "_"
1868 );
1869 // A simple name may not START with a digit either (WhatsApp nests
1870 // `X/0Xx`): the declaration site and every reference (jdc-core's
1871 // sanitize_source_name) prefix the same underscore.
1872 let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
1873 if clean && !keyword && !digit_start {
1874 std::borrow::Cow::Borrowed(name)
1875 } else if keyword || digit_start {
1876 std::borrow::Cow::Owned(format!("_{name}"))
1877 } else {
1878 // Non-ASCII single chars (Alipay names a field `支`) map to a
1879 // lone `_` — itself reserved since Java 9. Escape it.
1880 let mapped: String = name
1881 .chars()
1882 .map(|c| {
1883 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1884 c
1885 } else {
1886 '_'
1887 }
1888 })
1889 .collect();
1890 if mapped == "_" {
1891 std::borrow::Cow::Owned("__".to_string())
1892 } else {
1893 std::borrow::Cow::Owned(mapped)
1894 }
1895 }
1896}
1897
1898fn split_name(internal: &str) -> (String, String) {
1899 match internal.rfind('/') {
1900 Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
1901 None => (String::new(), internal.to_string()),
1902 }
1903}
1904
1905/// Dotted source form of an internal name.
1906pub fn dotted(internal: &str) -> String {
1907 let mut out = internal.replace('/', ".");
1908 // `$` → `.` only when the following segment can start a Java
1909 // identifier (anonymous/synthetic tails stay `$`).
1910 let mut i = 0;
1911 while let Some(p) = out[i..].find('$') {
1912 let at = i + p;
1913 // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
1914 // source name — dotting it produced a leading `.Gson.Types`.
1915 let head_ok = at > 0
1916 && out[..at]
1917 .chars()
1918 .next_back()
1919 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
1920 let tail_ok = out[at + 1..]
1921 .chars()
1922 .next()
1923 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1924 if tail_ok && head_ok {
1925 out.replace_range(at..at + 1, ".");
1926 i = at + 1;
1927 } else {
1928 i = at + 1;
1929 }
1930 }
1931 // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
1932 // reached `extends` with the digit start unmapped.
1933 sanitize_fq(&out)
1934}
1935
1936fn join_dotted(pool: &DexPool, names: &[String]) -> String {
1937 names
1938 .iter()
1939 .map(|n| print_class_name(pool, n))
1940 .collect::<Vec<_>>()
1941 .join(", ")
1942}
1943
1944/// A printable type name (arrays render with `[]` suffixes). Nested class
1945/// names dot their `$` when the outer chain is known to the pool.
1946pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
1947 match t {
1948 JavaType::Void => "void".into(),
1949 JavaType::Object(n) => print_class_name(pool, n),
1950 JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
1951 other => other.to_java(false),
1952 }
1953}
1954
1955/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
1956/// its left side names a known class (literal `$` top-level names survive).
1957// ---------------------------------------------------------------------------
1958// The per-class import map (JLS 6.4.2 obscuring repair).
1959//
1960// A referenced FQN whose FIRST segment equals an in-scope class simple
1961// name binds the qualifier to the CLASS, not the package — `class j3
1962// implements j3.g` is cyclic inheritance and collapses javac's
1963// attribution for the whole package (the Object-cascade root, weixin
1964// 84 files + contagion). The repair: emit `import j3.g;` and render
1965// the SIMPLE name in every type position. The set is populated per
1966// class from a metadata pre-scan (supertypes, field types, method
1967// signature descriptors) and read by print_class_name and the
1968// DexCtx::obscured_simple implementation (which jdc-core's shorten /
1969// java_type_name consult).
1970struct ObscureState {
1971 /// The ROOT class being rendered (its simple name is the obscuring
1972 /// in-scope name).
1973 class: String,
1974 /// Metadata pre-scan results: internal → simple render.
1975 map: jdc_core::FxHashMap<String, String>,
1976 /// Expression-level obscured refs DISCOVERED during the body render
1977 /// (their imports emit at assembly time).
1978 recorded: jdc_core::FxHashSet<String>,
1979 /// Simple names of the CURRENT PACKAGE's own classes: importing a
1980 /// name that collides would SHADOW every same-package use of it in
1981 /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
1982 /// erroring) rather than corrupt.
1983 blocked: jdc_core::FxHashSet<String>,
1984}
1985
1986thread_local! {
1987 static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
1988 const { std::cell::RefCell::new(None) };
1989}
1990
1991/// Install the per-class render state (call at class render entry).
1992pub(crate) fn set_obscured_state(
1993 class: String,
1994 map: jdc_core::FxHashMap<String, String>,
1995 blocked: jdc_core::FxHashSet<String>,
1996) {
1997 OBSCURE.with(|m| {
1998 *m.borrow_mut() = Some(ObscureState {
1999 class,
2000 map,
2001 recorded: jdc_core::FxHashSet::default(),
2002 blocked,
2003 });
2004 });
2005}
2006
2007/// Take the recorded expression-level refs and clear the state.
2008pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
2009 OBSCURE.with(|m| {
2010 m.borrow_mut()
2011 .take()
2012 .map(|st| st.recorded)
2013 .unwrap_or_default()
2014 })
2015}
2016
2017/// Render-time lookup: the metadata map first; otherwise an
2018/// expression-level ref whose first segment equals the root class's
2019/// simple name — record it (its import emits at assembly) and render
2020/// the simple name NOW.
2021pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
2022 OBSCURE.with(|m| {
2023 let mut st = m.borrow_mut();
2024 let st = st.as_mut()?;
2025 if let Some(simple) = st.map.get(internal) {
2026 return Some(simple.clone());
2027 }
2028 let first = internal.split('/').next().unwrap_or("");
2029 let own = st.class.rsplit('/').next().unwrap_or("");
2030 if first == own && internal.split('/').count() >= 2 {
2031 // A NESTED internal's in-scope simple name is its `$` tail
2032 // (`x/a$b` imports/renders as `b`) — the slash-tail left the
2033 // `$` in the render (`t2$a` flat against a nested emission).
2034 let simple = internal
2035 .rsplit(['/', '$'])
2036 .next()
2037 .unwrap_or("")
2038 .to_string();
2039 if !simple.is_empty() && !st.blocked.contains(&simple) {
2040 st.recorded.insert(internal.to_string());
2041 return Some(simple);
2042 }
2043 }
2044 None
2045 })
2046}
2047
2048/// The pre-scan: internal names referenced by the class's metadata
2049/// (supertypes, field types, method descriptors) whose first segment
2050/// equals the class's own simple name and which exist in the pool —
2051/// these get imports and simple-name renders.
2052fn compute_obscured_renders(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashMap<String, String> {
2053 let own_simple = class.name.rsplit('/').next().unwrap_or("");
2054 if own_simple.is_empty() {
2055 return jdc_core::FxHashMap::default();
2056 }
2057 let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2058 for sup in class.interfaces.iter() {
2059 refs.insert(sup.clone());
2060 }
2061 if let Some(sup) = &class.super_name {
2062 refs.insert(sup.clone());
2063 }
2064 for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
2065 if let JavaType::Object(n) = crate::desc_type(&f.desc) {
2066 refs.insert(n.to_string());
2067 }
2068 }
2069 for m in class.all_methods() {
2070 if let Some(d) = m.parsed_desc() {
2071 for a in d.args.iter() {
2072 if let JavaType::Object(n) = a {
2073 refs.insert(n.to_string());
2074 }
2075 }
2076 if let JavaType::Object(n) = &d.ret {
2077 refs.insert(n.to_string());
2078 }
2079 }
2080 }
2081 let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
2082 for r in refs {
2083 // First segment == the class's own simple name: the qualified
2084 // render would be obscured. The name must exist in the pool for
2085 // the import to resolve. The KEY stays internal (renders look
2086 // refs up by internal name), but the import line and the
2087 // simple name use the RENAMED display — a collision-renamed
2088 // class renders under its display name and an import of the
2089 // internal name does not resolve.
2090 let first = r.split('/').next().unwrap_or("");
2091 if first == own_simple && r.split('/').count() >= 2 && pool.get(&r).is_some() {
2092 // Own-family refs (the class itself, its nested members)
2093 // render through member scope — an import would be redundant
2094 // (self-import) or shadow a same-package sibling named like
2095 // the tail (`import t.t2.a` hijacks every bare `a` that
2096 // meant sibling class t.a — lark t/t2).
2097 if r == class.name || r.starts_with(&format!("{}$", class.name)) {
2098 continue;
2099 }
2100 let display = crate::apply_class_rename(&r);
2101 let simple = display
2102 .rsplit(['/', '$'])
2103 .next()
2104 .unwrap_or("")
2105 .to_string();
2106 if !simple.is_empty() {
2107 out.insert(r.clone(), simple);
2108 }
2109 }
2110 }
2111 out
2112}
2113
2114pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
2115 if let Some(simple) = obscured_render_pub(internal) {
2116 return sanitize_ref(&simple);
2117 }
2118 let cow = crate::apply_class_rename(internal);
2119 let internal: &str = &cow;
2120 // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
2121 // shape, `Outer$lruCache$1`) is emitted as its own top-level file
2122 // whose simple name keeps every `$` — the `$` boundaries inside
2123 // that unit are part of the NAME, not nesting. References must use
2124 // the flat unit name; the generic loop below dotted the first
2125 // boundary (`LruCacheKt.lruCache$1`) against the declaration
2126 // `class LruCacheKt$lruCache$1` — every use of the type failed and
2127 // javac's attribution for the whole file collapsed (3627 pure-
2128 // cascade files on weibo).
2129 if internal.contains('$') && pool.get(internal).is_some() {
2130 if let Some(root) = emission_root(pool, internal) {
2131 if root.contains('$') {
2132 let below = &internal[root.len()..];
2133 if below.is_empty() {
2134 // The unit itself: the flat name IS the reference.
2135 return sanitize_ref(&internal.replace('/', "."));
2136 }
2137 let segs: Vec<&str> = below[1..].split('$').collect();
2138 if segs.iter().any(|s| s.is_empty()) {
2139 // R8 tails like `ThreadMsg$$$` cannot be dotted at
2140 // all — keep the whole name flat.
2141 return sanitize_ref(&internal.replace('/', "."));
2142 }
2143 let mut out = root.replace('/', ".");
2144 for s in segs {
2145 out.push('.');
2146 out.push_str(s);
2147 }
2148 return sanitize_ref(&out);
2149 }
2150 }
2151 }
2152 let mut out = String::new();
2153 // `known` must test the ACCUMULATED internal prefix, not the bare
2154 // inter-`$` segment: the per-segment shape checked `pool.get("a")`
2155 // for the second level of `s5/o$a$b`, missed, and rendered the
2156 // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
2157 // nested type.
2158 let mut off = 0usize;
2159 loop {
2160 let rest = &internal[off..];
2161 match rest.find('$') {
2162 Some(i) => {
2163 let seg = &rest[..i];
2164 let prefix = &internal[..off + i];
2165 let known = pool.get(prefix).is_some()
2166 || jdc_core::rename::is_renamed_display(prefix)
2167 // The FULL name is not a pool class: this `$` cannot
2168 // be a literal name (pool literal classes — an app's
2169 // own `View$OnUnhandledKeyEventListener` — keep their
2170 // `$` here AND at their declaration), so it can only
2171 // be an external framework nesting boundary
2172 // (`View$OnClickListener` → `.OnClickListener`).
2173 || !pool.get(internal).is_some();
2174 // The `$` may only become a nesting dot when the tail
2175 // segment STARTS a Java identifier: R8's desugared-
2176 // library names carry `$` inside PACKAGE paths
2177 // (`j$/util/...` dotted into `j..util`) and suffixes
2178 // like `Collection$-EL` or anonymous `RequestId$1`
2179 // cannot be dotted under any reading.
2180 let tail_ok = rest[i + 1..]
2181 .chars()
2182 .next()
2183 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
2184 out.push_str(&seg.replace('/', "."));
2185 out.push_str(if known && tail_ok { "." } else { "$" });
2186 off += i + 1;
2187 }
2188 None => {
2189 out.push_str(&rest.replace('/', "."));
2190 return sanitize_ref(&out);
2191 }
2192 }
2193 }
2194}
2195
2196/// The top-level EMISSION UNIT ancestor of a pool class: walking the
2197/// `$`-chain upward (outer_of), the first ancestor that is itself
2198/// emitted as its own compilation unit — a digit-tail rest marks a
2199/// flat boundary (see top_level_classes). Clean members render inline
2200/// in their outer, so only the unit's own name may carry `$`.
2201fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
2202 let mut cur = internal.to_string();
2203 loop {
2204 let outer = pool.outer_of(&cur)?;
2205 let rest = cur
2206 .strip_prefix(outer)
2207 .and_then(|t| t.strip_prefix('$'))
2208 .unwrap_or("");
2209 if !crate::clean_member_tail(rest) {
2210 return Some(cur);
2211 }
2212 cur = outer.to_string();
2213 }
2214}
2215
2216/// Class-file names may contain characters Java source identifiers
2217/// cannot (`Collection$-EL`); the deterministic mapping matches the
2218/// declaration sites (java_ident).
2219/// Every `.`-segment of a fully-qualified name must start a Java
2220/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
2221pub(crate) fn sanitize_fq(dotted: &str) -> String {
2222 dotted
2223 .split('.')
2224 .map(|seg| {
2225 if is_java_keyword_name(seg)
2226 || is_restricted_type_name(seg)
2227 || seg.chars().next().is_some_and(|c| c.is_ascii_digit())
2228 {
2229 format!("_{seg}")
2230 } else if seg
2231 .chars()
2232 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
2233 {
2234 seg.to_string()
2235 } else {
2236 // Non-ASCII single chars (rimet nests classes named `ˆ`
2237 // / `ァ`) map to a lone `_` — reserved since Java 9.
2238 let mapped: String = seg
2239 .chars()
2240 .map(|c| {
2241 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
2242 c
2243 } else {
2244 '_'
2245 }
2246 })
2247 .collect();
2248 if mapped == "_" {
2249 "__".to_string()
2250 } else {
2251 mapped
2252 }
2253 }
2254 })
2255 .collect::<Vec<_>>()
2256 .join(".")
2257}
2258
2259/// Restricted contextual TYPE names — legal as member/local names
2260/// (rt.jar compiles `var` locals), illegal in class declarations and
2261/// type references. Consulted only on CLASS-name paths.
2262pub(crate) fn is_restricted_type_name(s: &str) -> bool {
2263 matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
2264}
2265
2266fn is_java_keyword_name(s: &str) -> bool {
2267 matches!(
2268 s,
2269 "abstract"
2270 | "assert"
2271 | "boolean"
2272 | "break"
2273 | "byte"
2274 | "case"
2275 | "catch"
2276 | "char"
2277 | "class"
2278 | "const"
2279 | "continue"
2280 | "default"
2281 | "do"
2282 | "double"
2283 | "else"
2284 | "enum"
2285 | "extends"
2286 | "final"
2287 | "finally"
2288 | "float"
2289 | "for"
2290 | "goto"
2291 | "if"
2292 | "implements"
2293 | "import"
2294 | "instanceof"
2295 | "int"
2296 | "interface"
2297 | "long"
2298 | "native"
2299 | "new"
2300 | "package"
2301 | "private"
2302 | "protected"
2303 | "public"
2304 | "return"
2305 | "short"
2306 | "static"
2307 | "strictfp"
2308 | "super"
2309 | "switch"
2310 | "synchronized"
2311 | "this"
2312 | "throw"
2313 | "throws"
2314 | "transient"
2315 | "try"
2316 | "void"
2317 | "volatile"
2318 | "while"
2319 | "true"
2320 | "false"
2321 | "null"
2322 // `_` is a reserved identifier since Java 9 (Alipay's
2323 // instant-run fields are named `_`) — the `_<name>` mapping
2324 // turns it into `__`, matching jdc-core's call sites.
2325 | "_"
2326 )
2327}
2328
2329fn sanitize_ref(name: &str) -> String {
2330 sanitize_fq(name)
2331}
2332
2333/// Dotted source form of an internal name.
2334pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
2335 print_class_name(pool, internal)
2336}
2337
2338/// Unused import silencer.
2339#[allow(dead_code)]
2340fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
2341 let _ = java_type_to_generic;
2342}