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