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