Skip to main content

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};
13
14#[derive(Debug, Clone)]
15pub struct ClassOptions {
16    /// Prefix each file with a provenance comment.
17    pub provenance: bool,
18}
19
20impl Default for ClassOptions {
21    fn default() -> Self {
22        ClassOptions { provenance: true }
23    }
24}
25
26/// Decompile one class (plus nested member classes) to a Java source file.
27///
28/// Classes with large method bodies run in a monitored thread with a
29/// deadline: pathological CFGs can drive the shared structurer's walk into
30/// an exponential exploration that never returns. On timeout the class is
31/// reported failed and the thread is abandoned (reaped at process exit).
32/// One registered monitored decompile: the receiver the worker polls at
33/// the tail, the class name (for diagnostics), and the deadline counted
34/// from the spawn.
35pub type PendingMonitor = (
36    std::sync::mpsc::Receiver<Result<String, String>>,
37    String,
38    std::time::Instant,
39);
40
41pub fn decompile_class(
42    pool: &std::sync::Arc<DexPool>,
43    class: &PoolClass,
44    opts: &ClassOptions,
45    pending: &std::sync::Mutex<Vec<PendingMonitor>>,
46) -> anyhow::Result<String> {
47    if class_is_risky(pool, class) {
48        // Detached monitored thread: the CALLER registers the receiver and
49        // moves on (awaiting happens at the end of the run) — a spinning
50        // pathological method no longer stalls its worker.
51        let pool2 = pool.clone();
52        let cls = class.clone();
53        let opts2 = opts.clone();
54        let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
55        let name = class.name.clone();
56        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
57        let _ = std::thread::Builder::new()
58            .stack_size(64 * 1024 * 1024)
59            .spawn(move || {
60                let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
61                let _ = tx.send(r);
62            });
63        pending.lock().unwrap().push((rx, name, deadline));
64        return Err(anyhow::anyhow!(
65            "deferred to monitored thread (pathological-CFG guard)"
66        ));
67    }
68    decompile_class_impl(pool, class, opts)
69}
70
71/// Any method in the exponential-walk danger zone → run under the
72/// deadline. Calibrated between real workloads: routine R8 classes top out
73/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
74/// (weibo's gson TypeAdapters) sit at 24k+.
75fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
76    // Header peek only — decoding every body here would double the APK's
77    // total decode work.
78    for m in class.all_methods() {
79        if m.code_off == 0 {
80            continue;
81        }
82        if let Some(dex) = pool.dex(m.dex_idx) {
83            if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
84                if insns > 16_000 {
85                    return true;
86                }
87            }
88        }
89    }
90    false
91}
92
93fn decompile_class_impl(
94    pool: &DexPool,
95    class: &PoolClass,
96    opts: &ClassOptions,
97) -> anyhow::Result<String> {
98    let ctx = DexCtx::new(pool, class);
99    // Size the class buffer up front: corpus-average classes render to
100    // ~1KB per method — without the reserve the String doubles through
101    // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
102    // Cap the reserve: weixin's monster classes (hundreds of methods)
103    // would reserve megabytes of untouched capacity per class — large
104    // blocks take mimalloc's commit/purge path (madvise churn showed up
105    // in profiles). 256KB covers the corpus-average class dozens of
106    // times over; bigger classes pay a few extra doublings.
107    let mut out = String::with_capacity(
108        (class.all_methods().count() * 1024).min(256 * 1024) + 512,
109    );
110    if opts.provenance {
111        static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
112        out.push_str(BANNER.get_or_init(|| {
113            format!(
114                "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
115                env!("CARGO_PKG_VERSION")
116            )
117        }));
118        // Provenance: which input image this class was lifted from (jadx's
119        // `loaded from: classes.dex` pattern). Byte-stable across runs —
120        // deliberately NO timestamp so outputs diff cleanly. Direct
121        // push_str: the per-class format! temporaries were 3 allocations
122        // × every class in the corpus.
123        if let Some(dex) = pool.dex(class.dex_idx) {
124            if let Some(label) = pool.dex_labels.get(class.dex_idx) {
125                out.push_str("// From: ");
126                out.push_str(label);
127                out.push_str(" (DEX ");
128                out.push_str(&dex.version);
129                out.push_str(")\n");
130            }
131        }
132    }
133    if let Some(src) = &class.source_file {
134        out.push_str("// Source file: ");
135        out.push_str(src);
136        out.push('\n');
137    }
138    if class.is_synthetic() {
139        out.push_str("// synthetic\n");
140    }
141    let (pkg, _) = split_name(&class.name);
142    if !pkg.is_empty() {
143        out.push('\n');
144        out.push_str("package ");
145        let dotted = pkg.replace('/', ".");
146        out.push_str(&sanitize_fq(&dotted));
147        out.push_str(";\n");
148    }
149    out.push('\n');
150    // Emit straight into `out`: the intermediate body buffer copied the
151    // whole rendered class (corpus-average ~10KB) a second time — pure
152    // memmove traffic; the error path discards `out` either way.
153    emit_class_body(pool, class, &ctx, opts, &mut out, 0)?;
154    Ok(out)
155}
156
157/// Render the class header, fields, methods and nested member classes into
158/// `out` (used at top level and recursively for nested members).
159// `opts` is only consumed by the nested-member recursion below — that
160// is its purpose (propagating emission options into inline children).
161/// One enum constant: the source identifier plus constructor
162/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
163use jdc_core::ir::expr::{ConstVal, Expr};
164use jdc_core::ir::stmt::Stmt;
165struct EnumConst {
166    #[allow(dead_code)]
167    field: String,
168    name: String,
169    extra_args: Vec<Expr>,
170}
171
172/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
173/// static field must be initialized in `<clinit>` by
174/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
175/// always emit. Returns None (caller falls back to the desugared
176/// `/* enum */ class` form) when anything is missing: R8 variance,
177/// constant-specific bodies (the field holds an anonymous subclass), or
178/// a <clinit> that failed to decompile.
179fn collect_enum_constants(
180    pool: &DexPool,
181    class: &PoolClass,
182    ctx: &DexCtx<'_>,
183) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
184    let _ = ctx;
185    let const_fields: Vec<&PoolField> = class
186        .static_fields
187        .iter()
188        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
189        .collect();
190    if const_fields.is_empty() {
191        return None;
192    }
193    let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
194    let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
195
196    // R8/d8 split the constant build across an intermediate local:
197    //   Self v0 = new Self("NAME", i, ...);
198    //   a = v0;                       // sput to the ACC_ENUM field
199    //   Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
200    // Pass 1 registers each intermediate (or direct) new; pass 2 binds
201    // them to the ACC_ENUM fields; references to the locals are then
202    // rewritten into constant identifiers so the $VALUES build keeps
203    // compiling once the definitions drop.
204    use std::collections::HashMap;
205    let mut const_name: Vec<String> = Vec::new();
206    let mut const_field: Vec<String> = Vec::new();
207    let mut const_extra: Vec<Vec<Expr>> = Vec::new();
208    let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
209    let mut drop_stmts: Vec<usize> = Vec::new();
210
211    // Collect (immutable borrows) first; the mutable passes come after.
212    if !matches!(&body.body, Stmt::Block(_)) {
213        return None;
214    }
215
216    // Pass 1: definitions. (immutable borrow; rewrite comes later)
217    // Rolling reaching-defs of clinit locals for resolving enum-ctor
218    // extra args (resolve_enum_arg); `tainted` flips at the first
219    // non-straight-line statement — past it, linear-scan defs are no
220    // longer provable and Local args reject the enum mode.
221    let mut defs: HashMap<u32, &Expr> = HashMap::default();
222    let mut tainted = false;
223    let self_name: std::sync::Arc<str> = class.name.as_str().into();
224    let self_ty =
225        jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
226    for (i, st) in match &body.body {
227        Stmt::Block(v) => v.iter().enumerate(),
228        _ => return None,
229    } {
230        if let Stmt::LocalDef {
231            var,
232            init: Some(Expr::New { cls: ncls, args, .. }),
233            ..
234        } = st
235        {
236            if ncls.as_ref() == class.name && args.len() >= 2 {
237                if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(_))) =
238                    (&args[0], &args[1])
239                {
240                    if java_ident(n).as_ref() != &**n || n.is_empty() {
241                        return None;
242                    }
243                    var_of.insert(*var, const_name.len());
244                    const_name.push(n.to_string());
245                    const_field.push(String::new());
246                    const_extra.push(resolve_enum_extras(
247                        &args[2..],
248                        &defs,
249                        tainted,
250                        &var_of,
251                        &const_name,
252                        &self_name,
253                        &self_ty,
254                    )?);
255                    drop_stmts.push(i);
256                }
257            }
258        }
259        track_def(st, &mut defs, &mut tainted);
260    }
261
262    // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
263    let mut defs2: HashMap<u32, &Expr> = HashMap::default();
264    let mut tainted2 = false;
265    for (i, st) in match &body.body {
266        Stmt::Block(v) => v.iter().enumerate(),
267        _ => return None,
268    } {
269        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
270            if let Expr::Field {
271                cls,
272                name: fname,
273                is_static: true,
274                ..
275            } = &**target
276            {
277                if cls.as_ref() != class.name {
278                    continue;
279                }
280                if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
281                    continue;
282                }
283                if const_field.iter().any(|f| !f.is_empty() && f == &**fname) {
284                    return None; // duplicate assignment
285                }
286                let idx = match &**value {
287                    Expr::Local { var, .. } => var_of.get(var).copied()?,
288                    Expr::New { cls: ncls, args, .. }
289                        if ncls.as_ref() == class.name && args.len() >= 2 =>
290                    {
291                        if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(_))) =
292                            (&args[0], &args[1])
293                        {
294                            if java_ident(n).as_ref() != &**n || n.is_empty() {
295                                return None;
296                            }
297                            let idx = const_name.len();
298                            const_name.push(n.to_string());
299                            const_field.push(String::new());
300                            const_extra.push(resolve_enum_extras(
301                                &args[2..],
302                                &defs2,
303                                tainted2,
304                                &var_of,
305                                &const_name,
306                                &self_name,
307                                &self_ty,
308                            )?);
309                            idx
310                        } else {
311                            return None;
312                        }
313                    }
314                    _ => return None,
315                };
316                const_field[idx] = fname.to_string();
317                drop_stmts.push(i);
318            }
319        }
320        track_def(st, &mut defs2, &mut tainted2);
321    }
322
323    // Every ACC_ENUM field bound, every intermediate matched.
324    if const_field.len() != const_fields.len() || const_field.iter().any(|f| f.is_empty()) {
325        return None;
326    }
327    {
328        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
329        if !const_name.iter().all(|c| seen.insert(c.as_str())) {
330            return None;
331        }
332    }
333
334    // Pass 3: rewrite references to the intermediate locals into the
335    // constant identifiers (static-field reads on Self).
336    let var2name: jdc_core::FxHashMap<u32, &str> = var_of
337        .iter()
338        .map(|(v, &i)| (*v, const_name[i].as_str()))
339        .collect();
340    crate::passes::rewrite_exprs(&mut body.body, &mut |e| {
341        crate::passes::deep_rewrite(e, &mut |x| {
342            if let Expr::Local { var, .. } = x {
343                if let Some(n) = var2name.get(var) {
344                    let name: std::sync::Arc<str> = std::sync::Arc::from(*n);
345                    *x = Expr::Field {
346                        owner: None,
347                        cls: self_name.clone(),
348                        name,
349                        ty: self_ty.clone(),
350                        is_static: true,
351                    };
352                }
353            }
354        });
355    });
356
357    // Pass 4: drop the definitions and their sputs.
358    if let Stmt::Block(vs) = &mut body.body {
359        let drop_set: std::collections::HashSet<usize> =
360            drop_stmts.iter().copied().collect();
361        vs.retain(|_| true);
362        let mut idx = 0usize;
363        vs.retain(|_| {
364            let keep = !drop_set.contains(&idx);
365            idx += 1;
366            keep
367        });
368    }
369    // The resolved args consumed the clinit locals' readers; prune the
370    // now-dead defs from the remnant (impure inits survive as bare
371    // expression statements — drop_dead_locals' standard contract).
372    crate::passes::drop_dead_locals(&mut body.body);
373
374    let out: Vec<EnumConst> = const_field
375        .into_iter()
376        .zip(const_name)
377        .zip(const_extra)
378        .map(|((field, name), extra_args)| EnumConst {
379            field,
380            name,
381            extra_args,
382        })
383        .collect();
384    Some((out, body))
385}
386
387/// Update the rolling reaching-def map for enum-arg resolution. Only
388/// flat single-target definitions keep the tracking sound; any control
389/// flow taints it (a linear scan can no longer prove WHICH definition
390/// reaches later capture sites).
391fn track_def<'e>(
392    st: &'e Stmt,
393    defs: &mut std::collections::HashMap<u32, &'e Expr>,
394    tainted: &mut bool,
395) {
396    match st {
397        Stmt::LocalDef { var, init, .. } => match init {
398            Some(e) => {
399                defs.insert(*var, e);
400            }
401            None => {
402                defs.remove(var);
403            }
404        },
405        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
406            if let Expr::Local { var, .. } = &**target {
407                if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
408                    defs.insert(*var, value);
409                } else {
410                    defs.remove(var);
411                }
412            }
413        }
414        // Plain expression statements (calls) don't define locals.
415        Stmt::ExprStmt(_) => {}
416        _ => *tainted = true,
417    }
418}
419
420/// Resolve every enum-ctor extra arg to a self-contained expression, or
421/// reject the enum mode (None). R8 reuses ONE register across all
422/// constant constructions — revenuecat's LogIntent builds 11 of 12
423/// emoji lists through the same `list` local, reassigned between the
424/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
425/// clinit where no local is in scope: an unresolved `Local` used to
426/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
427/// cannot-find). Each Local is replaced by its reaching pure definition
428/// (recursively) or, when it names an earlier constant's intermediate,
429/// by a static-field reference to that constant. Any expression shape
430/// WITHOUT locals passes through untouched (enum args may be arbitrary
431/// expressions — BinOp/Cast/Method/New — each renders exactly once per
432/// constant, so no duplication concern applies). Only an unresolvable
433/// Local (missing/tainted def, depth > 4) rejects the whole enum
434/// detection; the class then falls back to plain-field rendering, which
435/// always compiles.
436fn resolve_enum_extras(
437    extras: &[Expr],
438    defs: &std::collections::HashMap<u32, &Expr>,
439    tainted: bool,
440    var_of: &std::collections::HashMap<u32, usize>,
441    const_name: &[String],
442    self_name: &std::sync::Arc<str>,
443    self_ty: &jdc_core::ir::expr::TypeRef,
444) -> Option<Vec<Expr>> {
445    extras
446        .iter()
447        .map(|e| {
448            let mut out = e.clone();
449            let mut fail = false;
450            resolve_locals_in(
451                &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
452                &mut fail,
453            );
454            if fail {
455                None
456            } else {
457                Some(out)
458            }
459        })
460        .collect()
461}
462
463#[allow(clippy::too_many_arguments)]
464fn resolve_locals_in(
465    e: &mut Expr,
466    defs: &std::collections::HashMap<u32, &Expr>,
467    tainted: bool,
468    var_of: &std::collections::HashMap<u32, usize>,
469    const_name: &[String],
470    self_name: &std::sync::Arc<str>,
471    self_ty: &jdc_core::ir::expr::TypeRef,
472    depth: u32,
473    fail: &mut bool,
474) {
475    crate::passes::deep_rewrite(e, &mut |x| {
476        if let Expr::Local { var, .. } = x {
477            // An earlier constant's intermediate: a static-field read of
478            // that constant (declared above — backward reference, legal
479            // in enum ctor args).
480            if let Some(&ci) = var_of.get(var) {
481                if let Some(nm) = const_name.get(ci) {
482                    *x = Expr::Field {
483                        owner: None,
484                        cls: self_name.clone(),
485                        name: std::sync::Arc::from(nm.as_str()),
486                        ty: self_ty.clone(),
487                        is_static: true,
488                    };
489                    return;
490                }
491            }
492            if tainted || depth > 4 {
493                *fail = true;
494                return;
495            }
496            let Some(d) = defs.get(var).copied() else {
497                *fail = true;
498                return;
499            };
500            let mut sub = d.clone();
501            resolve_locals_in(
502                &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
503                depth + 1, fail,
504            );
505            if *fail {
506                return;
507            }
508            *x = sub;
509        }
510    });
511}
512
513/// Render enum-constant constructor arguments via the shared expression
514/// emitter (the VarTable is irrelevant for argument printing — no local
515/// names appear — but the API requires one).
516fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
517    let dummy_vt = jdc_core::var::VarTable::default();
518    let mut p = Printer::new(ctx, &dummy_vt);
519    for (i, a) in args.iter().enumerate() {
520        if i > 0 {
521            out.push_str(", ");
522        }
523        p.expr(a, 0, out);
524    }
525    let _ = pool;
526}
527
528#[allow(clippy::only_used_in_recursion)]
529fn emit_class_body(
530    pool: &DexPool,
531    class: &PoolClass,
532    ctx: &DexCtx<'_>,
533    opts: &ClassOptions,
534    out: &mut String,
535    depth: usize,
536) -> anyhow::Result<()> {
537    let ind = indent(depth);
538    // The class's own header must use the RENAMED display name — the
539    // ctor path (is_init) already renames; a header declaring `class a`
540    // while the file/ctor say `a_2` leaves the ctor looking like a
541    // method with no return type (MinisApp's Y2.a vs y2.a package
542    // case-collision: 2,518 javac parse errors).
543    let cname = crate::apply_class_rename(&class.name);
544    let (_, simple) = split_name(&cname);
545    // A class emitted as its OWN top-level file (depth 0) must declare a
546    // flat `$` name — `class Outer.Inner` is not declarable at file
547    // scope. An INLINE nested member (depth > 0) declares its own
548    // segment inside the parent's body.
549    let simple = if depth == 0 {
550        simple.to_string()
551    } else {
552        // R8 names can END in `$`: an empty last `$`-segment would
553        // render `class  {` — keep the whole simple name.
554        let seg = simple.rsplit('$').next().unwrap_or(&simple);
555        if seg.is_empty() {
556            simple.to_string()
557        } else {
558            seg.to_string()
559        }
560    };
561    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
562    // declared — restricted contextual type names escape here and in
563    // the ctor, file name, and every type reference.
564    let simple = if is_restricted_type_name(&simple) {
565        format!("_{simple}")
566    } else {
567        simple
568    };
569    let is_iface = class.is_interface();
570    let is_enum = class.is_enum();
571
572    let mut head = String::new();
573    let a = class.access;
574    if a & ACC_PUBLIC != 0 {
575        head.push_str("public ");
576    }
577    // Inline nested members need their `static` (interfaces/annotations
578    // are implicitly static; a missing `static` on a member class makes
579    // every `new Report(...)` site an "outer instance required" error).
580    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
581        head.push_str("static ");
582    }
583    if a & ACC_FINAL != 0 && !is_enum {
584        head.push_str("final ");
585    }
586    if a & ACC_ABSTRACT != 0 && !is_iface {
587        head.push_str("abstract ");
588    }
589    if a & ACC_ANNOTATION != 0 {
590        head.push('@');
591    }
592    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
593        collect_enum_constants(pool, class, ctx)
594    } else {
595        None
596    };
597    if let Some(ecs) = &enum_consts {
598        // True `enum` declaration: constants render in the header, the
599        // desugared boilerplate (const fields, their <clinit> inits —
600        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
601        // disappears. R8-renamed values()/valueOf() stay (they do not
602        // collide with the compiler-generated ones); javac-named ones
603        // are skipped at the method loop below.
604        head.push_str("enum ");
605        head.push_str(&java_ident(&simple));
606        let _ = ecs;
607    } else if is_enum {
608        // An enum with constant-specific bodies carries ACC_ABSTRACT —
609        // `abstract final` is an illegal modifier combination; abstract
610        // (already pushed above) suppresses the hardcoded final.
611        let abstract_ = a & ACC_ABSTRACT != 0;
612        head.push_str(if abstract_ {
613            "/* enum */ class "
614        } else {
615            "/* enum */ final class "
616        });
617        head.push_str(&java_ident(&simple));
618    } else if is_iface {
619        head.push_str("interface ");
620        head.push_str(&java_ident(&simple));
621    } else {
622        head.push_str("class ");
623        head.push_str(&java_ident(&simple));
624    }
625    if is_iface {
626        if !class.interfaces.is_empty() {
627            head.push_str(" extends ");
628            head.push_str(&join_dotted(pool, &class.interfaces));
629        }
630    } else {
631        if let Some(sup) = &class.super_name {
632            if sup != "java/lang/Object" && !is_enum {
633                head.push_str(" extends ");
634                head.push_str(&print_class_name(pool, sup));
635            }
636        }
637        if !class.interfaces.is_empty() {
638            head.push_str(if is_iface {
639                " extends "
640            } else {
641                " implements "
642            });
643            head.push_str(&join_dotted(pool, &class.interfaces));
644        }
645    }
646
647    out.push_str(&ind);
648    out.push_str(&head);
649    out.push_str(" {\n");
650
651    // True-enum constant list: the constants lead the body, ahead of
652    // any remaining fields.
653    if let Some((ecs, _)) = &enum_consts {
654        for (i, ec) in ecs.iter().enumerate() {
655            out.push_str(&indent(depth + 1));
656            out.push_str(&java_ident(&ec.name));
657            if !ec.extra_args.is_empty() {
658                let mut line = String::from("(");
659                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
660                line.push(')');
661                out.push_str(&line);
662            }
663            if i + 1 < ecs.len() {
664                out.push_str(",\n");
665            } else {
666                out.push_str(";\n");
667            }
668        }
669        out.push('\n');
670    }
671
672    // Fields.
673    let mut field_emitted = false;
674    for (i, f) in class.static_fields.iter().enumerate() {
675        // Enum constant fields became the header list above.
676        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
677            continue;
678        }
679        if field_emitted || !class.instance_fields.is_empty() {
680            out.push('\n');
681        }
682        field_emitted = true;
683        emit_field(
684            pool,
685            f,
686            class.static_values.get(i),
687            &class.name,
688            out,
689            depth + 1,
690            true,
691            class.is_interface(),
692        );
693    }
694    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
695        out.push('\n');
696    }
697    for (i, f) in class.instance_fields.iter().enumerate() {
698        if i > 0 {
699            out.push('\n');
700        }
701        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
702    }
703
704    // Methods.
705    // Duplicated (name, parameter-types) pairs in one class are javac
706    // "method is already defined" errors (weibo: 11.5k hits). Source
707    // cannot declare them, so exactly one survives: covariant bridges
708    // (`Object get(int)` beside `ByteString get(int)` — the compiler
709    // GENERATES bridges), plus R8 output that lost the bridge flag
710    // (okio Buffer.clone, interface getView re-declarations). A
711    // non-bridge method outranks a bridge for the same signature
712    // (bridge bodies are delegation stubs); first occurrence otherwise.
713    // Never drop a signature outright — the last method standing for a
714    // key is always rendered.
715    fn sig_key(m: &PoolMethod) -> (&str, &str) {
716        let d: &str = &m.desc;
717        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
718        let hi = d.find(')').unwrap_or(d.len());
719        (&*m.name, &d[lo..hi])
720    }
721    let methods: Vec<&PoolMethod> = class.all_methods().collect();
722    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
723    for (i, m) in methods.iter().enumerate() {
724        let key = sig_key(m);
725        match claim.get(&key) {
726            Some(&j)
727                if m.access & crate::access::ACC_BRIDGE == 0
728                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
729            {
730                claim.insert(key, i);
731            }
732            None => {
733                claim.insert(key, i);
734            }
735            _ => {}
736        }
737    }
738    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
739    for (i, m) in methods.iter().enumerate() {
740        if &*m.name == "<clinit>" {
741            continue; // rendered after the fields
742        }
743        if claim.get(&sig_key(m)) != Some(&i) {
744            continue;
745        }
746        // True-enum rendering: javac auto-generates values()/valueOf()
747        // — the original-named ones must not re-declare (R8-renamed
748        // copies stay, they do not collide).
749        if let Some((ecs, _)) = &enum_consts {
750            let d = m.parsed_desc();
751            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
752            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
753            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
754            let one_str = d
755                .as_ref()
756                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
757                .unwrap_or(false);
758            let static_ = m.is_static();
759            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
760                continue;
761            }
762            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
763                continue;
764            }
765            // Enum constructors must be private in source form.
766            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
767                let mut c = (*m).clone();
768                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
769                Some(c)
770            } else {
771                None
772            };
773            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
774            let mark = out.len();
775            if emitted_any {
776                out.push('\n');
777            }
778            if emit_method(pool, class, ctx, m_ref, depth + 1, out)? {
779                emitted_any = true;
780            } else {
781                out.truncate(mark);
782            }
783            let _ = ecs;
784            continue;
785        }
786        let mark = out.len();
787        if emitted_any {
788            out.push('\n');
789        }
790        if emit_method(pool, class, ctx, m, depth + 1, out)? {
791            emitted_any = true;
792        } else {
793            out.truncate(mark);
794        }
795    }
796    // Static initializer. INTERFACES cannot carry a `static { }` block in
797    // Java — their clinit only assigns constants, which static_values (or
798    // the `= null` default) already render as field initializers; skip
799    // the block entirely.
800    let skip_clinit = class.is_interface();
801    if let Some((_, clinit_body)) = enum_consts.take() {
802        // True-enum <clinit>: the constant assignments are already gone;
803        // render the remainder (the $VALUES array build) directly.
804        // Skip an empty remainder (all statements were constant inits).
805        let empty = match &clinit_body.body {
806            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
807            _ => false,
808        };
809        if !empty {
810            if emitted_any {
811                out.push('\n');
812            }
813            out.push_str(&indent(depth + 1));
814            out.push_str("static {\n");
815            let p = Printer::new(ctx, &clinit_body.vt);
816            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
817            for line in text.lines() {
818                if line.trim().is_empty() {
819                    continue;
820                }
821                out.push_str(line);
822                out.push('\n');
823            }
824            out.push_str(&indent(depth + 1));
825            out.push_str("}\n");
826            emitted_any = true;
827        }
828    } else if let Some(clinit) = (!skip_clinit)
829        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
830        .flatten()
831    {
832        let mark = out.len();
833        if emitted_any {
834            out.push('\n');
835        }
836        if emit_method(pool, class, ctx, clinit, depth + 1, out)? {
837            emitted_any = true;
838        } else {
839            out.truncate(mark);
840        }
841    }
842
843    // Nested member classes (clean `$` tails only; anonymous/local/lambda
844    // classes are emitted as their own top-level files by the driver).
845    for nested in nested_members(pool, class, ctx) {
846        if emitted_any {
847            out.push('\n');
848        }
849        let nested_ctx = DexCtx::new(pool, nested);
850        out.push('\n');
851        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
852        emitted_any = true;
853    }
854
855    out.push_str(&ind);
856    out.push_str("}\n");
857    Ok(())
858}
859
860/// Member classes of `class` (direct children by the `$` chain / member
861/// annotations), excluding anonymous / local / lambda shapes.
862fn nested_members<'a>(
863    pool: &'a DexPool,
864    class: &'a PoolClass,
865    ctx: &DexCtx<'_>,
866) -> Vec<&'a PoolClass> {
867    let mut out = Vec::new();
868    for name in pool.children_of(&class.name) {
869        let Some(pc) = pool.get(name) else { continue };
870        // Only a REAL `outer$tail` name is an inline member: the
871        // children index also carries annotation-derived outers
872        // (EnclosingClass) with no naming relationship to the child
873        // (obfuscated apps pair a 1-char name with a long enclosing
874        // descriptor) — the blind slice panicked there (alipay
875        // `a.a.a.a.c`, exposed once children_of actually returned data).
876        let Some(rest) = name
877            .strip_prefix(class.name.as_str())
878            .and_then(|t| t.strip_prefix('$'))
879        else {
880            continue;
881        };
882        if rest.is_empty() || rest.starts_with('-') {
883            continue;
884        }
885        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
886            continue;
887        }
888        let tail = rest.rsplit('$').next().unwrap_or(rest);
889        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
890            continue; // anonymous
891        }
892        if tail.starts_with(|c: char| c.is_ascii_digit()) {
893            continue; // local
894        }
895        out.push(pc);
896    }
897    out
898}
899
900// Eight parameters are all load-bearing (pool, field, static value,
901// owner class, output, depth, staticness, interface-init requirement);
902// bundling them into a struct would obscure each call site.
903#[allow(clippy::too_many_arguments)]
904fn emit_field(
905    pool: &DexPool,
906    f: &crate::PoolField,
907    init: Option<&StaticValue>,
908    f_class: &str,
909    out: &mut String,
910    depth: usize,
911    is_static: bool,
912    require_init: bool,
913) {
914    let ind = indent(depth);
915    let mut line = String::new();
916    let a = f.access;
917    if a & ACC_PUBLIC != 0 {
918        line.push_str("public ");
919    } else if a & ACC_PRIVATE != 0 {
920        line.push_str("private ");
921    } else if a & ACC_PROTECTED != 0 {
922        line.push_str("protected ");
923    }
924    if is_static {
925        line.push_str("static ");
926    }
927    if a & ACC_FINAL != 0 {
928        line.push_str("final ");
929    }
930    if a & ACC_SYNTHETIC != 0 {
931        line.push_str("/* synthetic */ ");
932    }
933    if a & ACC_TRANSIENT_HINT != 0 {
934        line.push_str("transient ");
935    }
936    if a & ACC_VOLATILE_HINT != 0 {
937        line.push_str("volatile ");
938    }
939    let ty = type_name(pool, &desc_type(&f.desc));
940    line.push_str(&ty);
941    line.push(' ');
942    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
943    line.push_str(&java_ident(fname));
944    let mut rendered = None;
945    if let Some(v) = init {
946        rendered = render_static_value(pool, v, f_class);
947    }
948    if rendered.is_none() && require_init {
949        // Interface fields MUST have an initializer in Java; the dex may
950        // not carry a static_values entry for a compile-time-constant the
951        // compiler folded away. Keep it compilable.
952        let default = match desc_type(&f.desc) {
953            JavaType::Boolean => "false",
954            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
955            JavaType::Long => "0L",
956            JavaType::Float => "0.0F",
957            JavaType::Double => "0.0",
958            _ => "null",
959        };
960        rendered = Some(default.to_string());
961    }
962    if let Some(text) = rendered {
963        // A long field holding an `Int(i64)` static value needs the `L`
964        // suffix: without it the literal is an int and overflows
965        // (`long d = -6343169151696340687` failed javac).
966        let text = if matches!(desc_type(&f.desc), JavaType::Long)
967            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
968        {
969            format!("{text}L")
970        } else {
971            text
972        };
973        line.push_str(" = ");
974        line.push_str(&text);
975    }
976    line.push(';');
977    out.push_str(&ind);
978    out.push_str(&line);
979    out.push('\n');
980}
981
982// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
983// volatile(0x40)/transient(0x80) for fields.
984pub const ACC_VOLATILE_HINT: u32 = 0x40;
985pub const ACC_TRANSIENT_HINT: u32 = 0x80;
986
987fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
988    Some(match v {
989        StaticValue::Int(i) => i.to_string(),
990        StaticValue::Float(f) => format_float(*f as f64, true),
991        StaticValue::Double(d) => format_float(*d, false),
992        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
993        StaticValue::Type(t) => format!("{}.class", dotted(t)),
994        StaticValue::Boolean(b) => b.to_string(),
995        StaticValue::Null => "null".into(),
996        StaticValue::Field(cls, name) => {
997            let n = java_ident(name);
998            if cls == owner {
999                n.into_owned()
1000            } else {
1001                format!("{}.{}", print_class_name(pool, cls), n)
1002            }
1003        }
1004        StaticValue::Other => return None,
1005    })
1006}
1007
1008/// Render one method straight into the class buffer `out`. Returns
1009/// whether anything was written (skips: no descriptor, deferred to a
1010/// monitored thread). The old shape returned a per-method String that
1011/// the caller copied in — one extra full copy of every method body.
1012fn emit_method(
1013    pool: &DexPool,
1014    class: &PoolClass,
1015    ctx: &DexCtx<'_>,
1016    m: &PoolMethod,
1017    depth: usize,
1018    out: &mut String,
1019) -> anyhow::Result<bool> {
1020    let ind = indent(depth);
1021    let desc = m.parsed_desc();
1022
1023    // Signature.
1024    let mut sig = String::with_capacity(192);
1025    let a = m.access;
1026    if a & ACC_PUBLIC != 0 {
1027        sig.push_str("public ");
1028    } else if a & ACC_PRIVATE != 0 {
1029        sig.push_str("private ");
1030    } else if a & ACC_PROTECTED != 0 {
1031        sig.push_str("protected ");
1032    }
1033    let is_clinit = &*m.name == "<clinit>";
1034    let is_init = &*m.name == "<init>";
1035    if a & ACC_STATIC != 0 || is_clinit {
1036        sig.push_str("static ");
1037    }
1038    if a & ACC_FINAL != 0 {
1039        sig.push_str("final ");
1040    }
1041    // `abstract synchronized` is an illegal combination — obfuscated
1042    // builds mark abstract bridges synchronized (WhatsApp
1043    // SQLiteOpenHelper).
1044    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
1045        sig.push_str("synchronized ");
1046    }
1047    if a & ACC_NATIVE != 0 {
1048        sig.push_str("native ");
1049    }
1050    if a & ACC_ABSTRACT != 0 {
1051        sig.push_str("abstract ");
1052    }
1053    if a & ACC_SYNTHETIC != 0 {
1054        sig.push_str("/* synthetic */ ");
1055    }
1056
1057    // Body (needed for parameter names even for abstract methods).
1058    let body = decompile_method(pool, class, m).ok().flatten();
1059    let param_names: Vec<String> = body
1060        .as_ref()
1061        .map(|b| {
1062            let mut ps: Vec<(u16, String)> =
1063                b.vt.vars
1064                    .iter()
1065                    .filter(|v| v.is_param && v.name != "this")
1066                    .map(|v| (v.slot, v.name.clone()))
1067                    .collect();
1068            ps.sort_by_key(|(s, _)| *s);
1069            ps.into_iter().map(|(_, n)| n).collect()
1070        })
1071        .unwrap_or_else(|| {
1072            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
1073                .map(|i| format!("p{}", i + 1))
1074                .collect()
1075        });
1076
1077    if is_clinit {
1078        // `static { ... }` — the caller strips the method name/params.
1079    } else {
1080        let Some(d) = &desc else { return Ok(false) };
1081        if is_init {
1082            // The ctor name must equal the DECLARED class name of its
1083            // file: flat `$` at depth 0 (own file), own segment when
1084            // inlined in the parent at depth > 0. Case-renamed classes
1085            // use their display name.
1086            let dname = crate::apply_class_rename(&class.name);
1087            let (_, simple) = split_name(&dname);
1088            // emit_method's depth is the METHOD indent = class depth + 1:
1089            // own-file classes (depth 0 header → method depth 1) need the
1090            // flat `$` ctor name; inline nested members use their segment.
1091            let base = if depth <= 1 {
1092                simple.to_string()
1093            } else {
1094                // R8 names can END in `$` (`ThreadMsg$$$`): the last
1095                // `$`-segment is empty — keep the whole simple name.
1096                let seg = simple.rsplit('$').next().unwrap_or(&simple);
1097                if seg.is_empty() {
1098                    simple.to_string()
1099                } else {
1100                    seg.to_string()
1101                }
1102            };
1103            let name = if is_restricted_type_name(&base) {
1104                format!("_{base}")
1105            } else {
1106                base
1107            };
1108            sig.push_str(&java_ident(&name));
1109        } else {
1110            sig.push_str(&type_name(pool, &d.ret));
1111            sig.push(' ');
1112            let mname =
1113                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
1114            sig.push_str(&java_ident(mname));
1115        }
1116        sig.push('(');
1117        let n = d.args.len();
1118        let varargs = a & ACC_VARARGS != 0 && n > 0;
1119        for (i, arg) in d.args.iter().enumerate() {
1120            if i > 0 {
1121                sig.push_str(", ");
1122            }
1123            let name = param_names
1124                .get(i)
1125                .cloned()
1126                .unwrap_or_else(|| format!("p{}", i));
1127            if varargs && i + 1 == n {
1128                if let JavaType::Array(inner) = arg {
1129                    sig.push_str(&type_name(pool, inner));
1130                    sig.push_str("...");
1131                } else {
1132                    sig.push_str(&type_name(pool, arg));
1133                }
1134            } else {
1135                sig.push_str(&type_name(pool, arg));
1136            }
1137            sig.push(' ');
1138            // Param names come from dex debug info — obfuscated apps
1139            // name them `_` (reserved since Java 9) or after keywords.
1140            sig.push_str(&java_ident(&name));
1141        }
1142        sig.push(')');
1143    }
1144
1145    if is_clinit {
1146        let Some(b) = body else { return Ok(false) };
1147        // Direct render: the printer starts at the method's ABSOLUTE
1148        // indent and appends into the same buffer that carries the
1149        // header — no intermediate body string, no per-line re-indent
1150        // pass (two full copies of every method body saved).
1151        out.push_str(&ind);
1152        out.push_str("static {\n");
1153        let hdr = out.len();
1154        let printer = Printer::new(ctx, &b.vt)
1155            .with_indent(depth + 1)
1156            .with_output(std::mem::take(out));
1157        let t_print = std::time::Instant::now();
1158        let mut rendered = printer.into_string(&b.body);
1159        crate::method::phase_hit(3, t_print);
1160        if rendered[hdr..].trim().is_empty() {
1161            rendered.truncate(hdr);
1162        }
1163        rendered.push_str(&ind);
1164        rendered.push_str("}\n");
1165        *out = rendered;
1166        return Ok(true);
1167    }
1168    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
1169        out.push_str(&ind);
1170        out.push_str(&sig);
1171        out.push_str(";\n");
1172        return Ok(true);
1173    }
1174    let Some(b) = body else { return Ok(false) };
1175
1176    // Direct render at the absolute indent level (see the clinit path).
1177    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
1178    match &b.desc.ret {
1179        JavaType::Boolean => {
1180            printer = printer.with_ret_bool(true);
1181        }
1182        JavaType::Char => {
1183            printer = printer.with_ret_char(true);
1184        }
1185        JavaType::Byte => {
1186            printer = printer.with_ret_narrow(true, false);
1187        }
1188        JavaType::Short => {
1189            printer = printer.with_ret_narrow(false, true);
1190        }
1191        _ => {}
1192    }
1193    out.push_str(&ind);
1194    out.push_str(&sig);
1195    out.push_str(" {\n");
1196    let hdr = out.len();
1197    let printer = printer.with_output(std::mem::take(out));
1198    let t_print = std::time::Instant::now();
1199    let mut rendered = printer.into_string(&b.body);
1200    crate::method::phase_hit(3, t_print);
1201    if rendered[hdr..].trim().is_empty() {
1202        rendered.truncate(hdr);
1203    }
1204    rendered.push_str(&ind);
1205    rendered.push_str("}\n");
1206    *out = rendered;
1207    Ok(true)
1208}
1209
1210// ---------------------------------------------------------------------------
1211// Naming helpers
1212// ---------------------------------------------------------------------------
1213
1214fn indent(depth: usize) -> String {
1215    "    ".repeat(depth)
1216}
1217
1218/// `$`-separated nesting rendered with dots — but ONLY when every
1219/// segment is a clean Java identifier (a genuine member class chain).
1220/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
1221/// `...$$inlined$collect$1`) and local-class tails are NOT member
1222/// classes Java can name; the whole name stays flat with `$` (ddc emits
1223/// them as their own top-level files).
1224/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
1225/// any other non-identifier character) is not legal Java. Deterministic
1226/// mapping, applied identically at declaration and call sites.
1227pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1228    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
1229    // (millions per APK), where it profiled as __NSGetEnviron.
1230    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1231    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
1232        eprintln!("[ident] {name:?}");
1233    }
1234    let clean = name
1235        .chars()
1236        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
1237    // Kotlin names fields/methods `default` (Companion.default) — no Java
1238    // program can declare or reference a keyword; the identical mapping
1239    // lives in jdc-core's call-site sanitizer.
1240    let keyword = matches!(
1241        name,
1242        "abstract"
1243            | "assert"
1244            | "boolean"
1245            | "break"
1246            | "byte"
1247            | "case"
1248            | "catch"
1249            | "char"
1250            | "class"
1251            | "const"
1252            | "continue"
1253            | "default"
1254            | "do"
1255            | "double"
1256            | "else"
1257            | "enum"
1258            | "extends"
1259            | "final"
1260            | "finally"
1261            | "float"
1262            | "for"
1263            | "goto"
1264            | "if"
1265            | "implements"
1266            | "import"
1267            | "instanceof"
1268            | "int"
1269            | "interface"
1270            | "long"
1271            | "native"
1272            | "new"
1273            | "package"
1274            | "private"
1275            | "protected"
1276            | "public"
1277            | "return"
1278            | "short"
1279            | "static"
1280            | "strictfp"
1281            | "super"
1282            | "switch"
1283            | "synchronized"
1284            | "this"
1285            | "throw"
1286            | "throws"
1287            | "transient"
1288            | "try"
1289            | "void"
1290            | "volatile"
1291            | "while"
1292            | "true"
1293            | "false"
1294            | "null"
1295            // `_` is a reserved identifier since Java 9 (Alipay's
1296            // instant-run fields are named `_`) — the `_<name>` mapping
1297            // turns it into `__`, matching jdc-core's call sites.
1298            | "_"
1299    );
1300    // A simple name may not START with a digit either (WhatsApp nests
1301    // `X/0Xx`): the declaration site and every reference (jdc-core's
1302    // sanitize_source_name) prefix the same underscore.
1303    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
1304    if clean && !keyword && !digit_start {
1305        std::borrow::Cow::Borrowed(name)
1306    } else if keyword || digit_start {
1307        std::borrow::Cow::Owned(format!("_{name}"))
1308    } else {
1309        // Non-ASCII single chars (Alipay names a field `支`) map to a
1310        // lone `_` — itself reserved since Java 9. Escape it.
1311        let mapped: String = name
1312            .chars()
1313            .map(|c| {
1314                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1315                    c
1316                } else {
1317                    '_'
1318                }
1319            })
1320            .collect();
1321        if mapped == "_" {
1322            std::borrow::Cow::Owned("__".to_string())
1323        } else {
1324            std::borrow::Cow::Owned(mapped)
1325        }
1326    }
1327}
1328
1329fn split_name(internal: &str) -> (String, String) {
1330    match internal.rfind('/') {
1331        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
1332        None => (String::new(), internal.to_string()),
1333    }
1334}
1335
1336/// Dotted source form of an internal name.
1337pub fn dotted(internal: &str) -> String {
1338    let mut out = internal.replace('/', ".");
1339    // `$` → `.` only when the following segment can start a Java
1340    // identifier (anonymous/synthetic tails stay `$`).
1341    let mut i = 0;
1342    while let Some(p) = out[i..].find('$') {
1343        let at = i + p;
1344        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
1345        // source name — dotting it produced a leading `.Gson.Types`.
1346        let head_ok = at > 0
1347            && out[..at]
1348                .chars()
1349                .next_back()
1350                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
1351        let tail_ok = out[at + 1..]
1352            .chars()
1353            .next()
1354            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1355        if tail_ok && head_ok {
1356            out.replace_range(at..at + 1, ".");
1357            i = at + 1;
1358        } else {
1359            i = at + 1;
1360        }
1361    }
1362    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
1363    // reached `extends` with the digit start unmapped.
1364    sanitize_fq(&out)
1365}
1366
1367fn join_dotted(pool: &DexPool, names: &[String]) -> String {
1368    names
1369        .iter()
1370        .map(|n| print_class_name(pool, n))
1371        .collect::<Vec<_>>()
1372        .join(", ")
1373}
1374
1375/// A printable type name (arrays render with `[]` suffixes). Nested class
1376/// names dot their `$` when the outer chain is known to the pool.
1377pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
1378    match t {
1379        JavaType::Void => "void".into(),
1380        JavaType::Object(n) => print_class_name(pool, n),
1381        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
1382        other => other.to_java(false),
1383    }
1384}
1385
1386/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
1387/// its left side names a known class (literal `$` top-level names survive).
1388pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
1389    let cow = crate::apply_class_rename(internal);
1390    let internal: &str = &cow;
1391    let mut out = String::new();
1392    // `known` must test the ACCUMULATED internal prefix, not the bare
1393    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
1394    // for the second level of `s5/o$a$b`, missed, and rendered the
1395    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
1396    // nested type.
1397    let mut off = 0usize;
1398    loop {
1399        let rest = &internal[off..];
1400        match rest.find('$') {
1401            Some(i) => {
1402                let seg = &rest[..i];
1403                let prefix = &internal[..off + i];
1404                let known = pool.get(prefix).is_some()
1405                    || jdc_core::rename::is_renamed_display(prefix)
1406                    // The FULL name is not a pool class: this `$` cannot
1407                    // be a literal name (pool literal classes — an app's
1408                    // own `View$OnUnhandledKeyEventListener` — keep their
1409                    // `$` here AND at their declaration), so it can only
1410                    // be an external framework nesting boundary
1411                    // (`View$OnClickListener` → `.OnClickListener`).
1412                    || !pool.get(internal).is_some();
1413                // The `$` may only become a nesting dot when the tail
1414                // segment STARTS a Java identifier: R8's desugared-
1415                // library names carry `$` inside PACKAGE paths
1416                // (`j$/util/...` dotted into `j..util`) and suffixes
1417                // like `Collection$-EL` or anonymous `RequestId$1`
1418                // cannot be dotted under any reading.
1419                let tail_ok = rest[i + 1..]
1420                    .chars()
1421                    .next()
1422                    .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1423                out.push_str(&seg.replace('/', "."));
1424                out.push_str(if known && tail_ok { "." } else { "$" });
1425                off += i + 1;
1426            }
1427            None => {
1428                out.push_str(&rest.replace('/', "."));
1429                return sanitize_ref(&out);
1430            }
1431        }
1432    }
1433}
1434
1435/// Class-file names may contain characters Java source identifiers
1436/// cannot (`Collection$-EL`); the deterministic mapping matches the
1437/// declaration sites (java_ident).
1438/// Every `.`-segment of a fully-qualified name must start a Java
1439/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
1440pub(crate) fn sanitize_fq(dotted: &str) -> String {
1441    dotted
1442        .split('.')
1443        .map(|seg| {
1444            if is_java_keyword_name(seg)
1445                || is_restricted_type_name(seg)
1446                || seg.chars().next().is_some_and(|c| c.is_ascii_digit())
1447            {
1448                format!("_{seg}")
1449            } else if seg
1450                .chars()
1451                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
1452            {
1453                seg.to_string()
1454            } else {
1455                // Non-ASCII single chars (rimet nests classes named `ˆ`
1456                // / `ァ`) map to a lone `_` — reserved since Java 9.
1457                let mapped: String = seg
1458                    .chars()
1459                    .map(|c| {
1460                        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1461                            c
1462                        } else {
1463                            '_'
1464                        }
1465                    })
1466                    .collect();
1467                if mapped == "_" {
1468                    "__".to_string()
1469                } else {
1470                    mapped
1471                }
1472            }
1473        })
1474        .collect::<Vec<_>>()
1475        .join(".")
1476}
1477
1478/// Restricted contextual TYPE names — legal as member/local names
1479/// (rt.jar compiles `var` locals), illegal in class declarations and
1480/// type references. Consulted only on CLASS-name paths.
1481pub(crate) fn is_restricted_type_name(s: &str) -> bool {
1482    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
1483}
1484
1485fn is_java_keyword_name(s: &str) -> bool {
1486    matches!(
1487        s,
1488        "abstract"
1489            | "assert"
1490            | "boolean"
1491            | "break"
1492            | "byte"
1493            | "case"
1494            | "catch"
1495            | "char"
1496            | "class"
1497            | "const"
1498            | "continue"
1499            | "default"
1500            | "do"
1501            | "double"
1502            | "else"
1503            | "enum"
1504            | "extends"
1505            | "final"
1506            | "finally"
1507            | "float"
1508            | "for"
1509            | "goto"
1510            | "if"
1511            | "implements"
1512            | "import"
1513            | "instanceof"
1514            | "int"
1515            | "interface"
1516            | "long"
1517            | "native"
1518            | "new"
1519            | "package"
1520            | "private"
1521            | "protected"
1522            | "public"
1523            | "return"
1524            | "short"
1525            | "static"
1526            | "strictfp"
1527            | "super"
1528            | "switch"
1529            | "synchronized"
1530            | "this"
1531            | "throw"
1532            | "throws"
1533            | "transient"
1534            | "try"
1535            | "void"
1536            | "volatile"
1537            | "while"
1538            | "true"
1539            | "false"
1540            | "null"
1541            // `_` is a reserved identifier since Java 9 (Alipay's
1542            // instant-run fields are named `_`) — the `_<name>` mapping
1543            // turns it into `__`, matching jdc-core's call sites.
1544            | "_"
1545    )
1546}
1547
1548fn sanitize_ref(name: &str) -> String {
1549    sanitize_fq(name)
1550}
1551
1552/// Dotted source form of an internal name.
1553pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
1554    print_class_name(pool, internal)
1555}
1556
1557/// Unused import silencer.
1558#[allow(dead_code)]
1559fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
1560    let _ = java_type_to_generic;
1561}