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::ctx::{java_type_to_generic, DexCtx};
10use crate::method::decompile_method;
11use crate::{desc_type, DexPool, PoolClass, PoolMethod, StaticValue};
12
13#[derive(Debug, Clone)]
14pub struct ClassOptions {
15    /// Prefix each file with a provenance comment.
16    pub provenance: bool,
17}
18
19impl Default for ClassOptions {
20    fn default() -> Self {
21        ClassOptions { provenance: true }
22    }
23}
24
25/// Decompile one class (plus nested member classes) to a Java source file.
26///
27/// Classes with large method bodies run in a monitored thread with a
28/// deadline: pathological CFGs can drive the shared structurer's walk into
29/// an exponential exploration that never returns. On timeout the class is
30/// reported failed and the thread is abandoned (reaped at process exit).
31pub fn decompile_class(
32    pool: &std::sync::Arc<DexPool>,
33    class: &PoolClass,
34    opts: &ClassOptions,
35    pending: &std::sync::Mutex<
36        Vec<(
37            std::sync::mpsc::Receiver<Result<String, String>>,
38            String,
39            std::time::Instant,
40        )>,
41    >,
42) -> anyhow::Result<String> {
43    if class_is_risky(pool, class) {
44        // Detached monitored thread: the CALLER registers the receiver and
45        // moves on (awaiting happens at the end of the run) — a spinning
46        // pathological method no longer stalls its worker.
47        let pool2 = pool.clone();
48        let cls = class.clone();
49        let opts2 = opts.clone();
50        let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
51        let name = class.name.clone();
52        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
53        let _ = std::thread::Builder::new()
54            .stack_size(64 * 1024 * 1024)
55            .spawn(move || {
56                let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
57                let _ = tx.send(r);
58            });
59        pending.lock().unwrap().push((rx, name, deadline));
60        return Err(anyhow::anyhow!(
61            "deferred to monitored thread (pathological-CFG guard)"
62        ));
63    }
64    decompile_class_impl(pool, class, opts)
65}
66
67/// Any method in the exponential-walk danger zone → run under the
68/// deadline. Calibrated between real workloads: routine R8 classes top out
69/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
70/// (weibo's gson TypeAdapters) sit at 24k+.
71fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
72    // Header peek only — decoding every body here would double the APK's
73    // total decode work.
74    for m in class.all_methods() {
75        if m.code_off == 0 {
76            continue;
77        }
78        if let Some(dex) = pool.dex(m.dex_idx) {
79            if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
80                if insns > 16_000 {
81                    return true;
82                }
83            }
84        }
85    }
86    false
87}
88
89fn decompile_class_impl(
90    pool: &DexPool,
91    class: &PoolClass,
92    opts: &ClassOptions,
93) -> anyhow::Result<String> {
94    let ctx = DexCtx::new(pool, class);
95    let mut out = String::new();
96    if opts.provenance {
97        out.push_str(&format!(
98            "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
99            env!("CARGO_PKG_VERSION")
100        ));
101        // Provenance: which input image this class was lifted from (jadx's
102        // `loaded from: classes.dex` pattern). Byte-stable across runs —
103        // deliberately NO timestamp so outputs diff cleanly.
104        let label = pool
105            .dex_labels
106            .get(class.dex_idx)
107            .cloned()
108            .unwrap_or_default();
109        if let Some(dex) = pool.dex(class.dex_idx) {
110            out.push_str(&format!("// From: {} (DEX {})\n", label, dex.version));
111        }
112    }
113    if let Some(src) = &class.source_file {
114        out.push_str(&format!("// Source file: {}\n", src));
115    }
116    if class.is_synthetic() {
117        out.push_str("// synthetic\n");
118    }
119    let (pkg, _) = split_name(&class.name);
120    if !pkg.is_empty() {
121        out.push('\n');
122        out.push_str(&format!("package {};\n", pkg.replace('/', ".")));
123    }
124    out.push('\n');
125    let mut body = String::new();
126    emit_class_body(pool, class, &ctx, opts, &mut body, 0)?;
127    out.push_str(&body);
128    Ok(out)
129}
130
131/// Render the class header, fields, methods and nested member classes into
132/// `out` (used at top level and recursively for nested members).
133fn emit_class_body(
134    pool: &DexPool,
135    class: &PoolClass,
136    ctx: &DexCtx<'_>,
137    opts: &ClassOptions,
138    out: &mut String,
139    depth: usize,
140) -> anyhow::Result<()> {
141    let ind = indent(depth);
142    let (_, simple) = split_name(&class.name);
143    let simple = simple.replace('$', ".");
144    let is_iface = class.is_interface();
145    let is_enum = class.is_enum();
146
147    let mut head = String::new();
148    let a = class.access;
149    if a & ACC_PUBLIC != 0 {
150        head.push_str("public ");
151    }
152    if a & ACC_FINAL != 0 && !is_enum {
153        head.push_str("final ");
154    }
155    if a & ACC_ABSTRACT != 0 && !is_iface {
156        head.push_str("abstract ");
157    }
158    if a & ACC_ANNOTATION != 0 {
159        head.push_str("@");
160    }
161    if is_enum {
162        head.push_str("/* enum */ final class ");
163        head.push_str(&simple);
164    } else if is_iface {
165        head.push_str("interface ");
166        head.push_str(&simple);
167    } else {
168        head.push_str("class ");
169        head.push_str(&simple);
170    }
171    if is_iface {
172        if !class.interfaces.is_empty() {
173            head.push_str(" extends ");
174            head.push_str(&join_dotted(&class.interfaces));
175        }
176    } else {
177        if let Some(sup) = &class.super_name {
178            if sup != "java/lang/Object" && !is_enum {
179                head.push_str(" extends ");
180                head.push_str(&dotted(sup));
181            }
182        }
183        if !class.interfaces.is_empty() {
184            head.push_str(if is_iface {
185                " extends "
186            } else {
187                " implements "
188            });
189            head.push_str(&join_dotted(&class.interfaces));
190        }
191    }
192
193    out.push_str(&ind);
194    out.push_str(&head);
195    out.push_str(" {\n");
196
197    // Fields.
198    for (i, f) in class.static_fields.iter().enumerate() {
199        if i > 0 || !class.instance_fields.is_empty() {
200            out.push('\n');
201        }
202        emit_field(
203            pool,
204            f,
205            class.static_values.get(i),
206            &class.name,
207            out,
208            depth + 1,
209            true,
210        );
211    }
212    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
213        out.push('\n');
214    }
215    for (i, f) in class.instance_fields.iter().enumerate() {
216        if i > 0 {
217            out.push('\n');
218        }
219        emit_field(pool, f, None, &class.name, out, depth + 1, false);
220    }
221
222    // Methods.
223    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
224    for m in class.all_methods() {
225        if m.name == "<clinit>" {
226            continue; // rendered after the fields
227        }
228        let text = emit_method(pool, class, ctx, m, depth + 1)?;
229        if let Some(text) = text {
230            if emitted_any {
231                out.push('\n');
232            }
233            out.push_str(&text);
234            emitted_any = true;
235        }
236    }
237    // Static initializer.
238    if let Some(clinit) = class.all_methods().find(|m| m.name == "<clinit>") {
239        if let Some(text) = emit_method(pool, class, ctx, clinit, depth + 1)? {
240            if emitted_any {
241                out.push('\n');
242            }
243            out.push_str(&text);
244            emitted_any = true;
245        }
246    }
247
248    // Nested member classes (clean `$` tails only; anonymous/local/lambda
249    // classes are emitted as their own top-level files by the driver).
250    for nested in nested_members(pool, class, ctx) {
251        if emitted_any {
252            out.push('\n');
253        }
254        let nested_ctx = DexCtx::new(pool, nested);
255        out.push('\n');
256        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
257        emitted_any = true;
258    }
259
260    out.push_str(&ind);
261    out.push_str("}\n");
262    Ok(())
263}
264
265/// Member classes of `class` (direct children by the `$` chain / member
266/// annotations), excluding anonymous / local / lambda shapes.
267fn nested_members<'a>(
268    pool: &'a DexPool,
269    class: &'a PoolClass,
270    ctx: &DexCtx<'_>,
271) -> Vec<&'a PoolClass> {
272    let mut out = Vec::new();
273    for name in pool.children_of(&class.name) {
274        let Some(pc) = pool.get(&name) else { continue };
275        let rest = &name[class.name.len() + 1..];
276        if rest.is_empty() || rest.starts_with('-') {
277            continue;
278        }
279        if ctx.find_outer(&name).as_deref() != Some(class.name.as_str()) {
280            continue;
281        }
282        let tail = rest.rsplit('$').next().unwrap_or(rest);
283        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
284            continue; // anonymous
285        }
286        if tail.starts_with(|c: char| c.is_ascii_digit()) {
287            continue; // local
288        }
289        out.push(pc);
290    }
291    out
292}
293
294fn emit_field(
295    pool: &DexPool,
296    f: &crate::PoolField,
297    init: Option<&StaticValue>,
298    f_class: &str,
299    out: &mut String,
300    depth: usize,
301    is_static: bool,
302) {
303    let ind = indent(depth);
304    let mut line = String::new();
305    let a = f.access;
306    if a & ACC_PUBLIC != 0 {
307        line.push_str("public ");
308    } else if a & ACC_PRIVATE != 0 {
309        line.push_str("private ");
310    } else if a & ACC_PROTECTED != 0 {
311        line.push_str("protected ");
312    }
313    if is_static {
314        line.push_str("static ");
315    }
316    if a & ACC_FINAL != 0 {
317        line.push_str("final ");
318    }
319    if a & ACC_SYNTHETIC != 0 {
320        line.push_str("/* synthetic */ ");
321    }
322    if a & ACC_TRANSIENT_HINT != 0 {
323        line.push_str("transient ");
324    }
325    if a & ACC_VOLATILE_HINT != 0 {
326        line.push_str("volatile ");
327    }
328    let ty = type_name(pool, &desc_type(&f.desc));
329    line.push_str(&ty);
330    line.push(' ');
331    line.push_str(&f.name);
332    if let Some(v) = init {
333        if let Some(text) = render_static_value(v, &f_class) {
334            line.push_str(" = ");
335            line.push_str(&text);
336        }
337    }
338    line.push(';');
339    out.push_str(&ind);
340    out.push_str(&line);
341    out.push('\n');
342}
343
344// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
345// volatile(0x40)/transient(0x80) for fields.
346pub const ACC_VOLATILE_HINT: u32 = 0x40;
347pub const ACC_TRANSIENT_HINT: u32 = 0x80;
348
349fn render_static_value(v: &StaticValue, owner: &str) -> Option<String> {
350    Some(match v {
351        StaticValue::Int(i) => i.to_string(),
352        StaticValue::Float(f) => format_float(*f as f64, true),
353        StaticValue::Double(d) => format_float(*d, false),
354        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
355        StaticValue::Type(t) => format!("{}.class", dotted(t)),
356        StaticValue::Boolean(b) => b.to_string(),
357        StaticValue::Null => "null".into(),
358        StaticValue::Field(cls, name) => {
359            if cls == owner {
360                name.clone()
361            } else {
362                format!("{}.{}", dotted(cls), name)
363            }
364        }
365        StaticValue::Other => return None,
366    })
367}
368
369fn emit_method(
370    pool: &DexPool,
371    class: &PoolClass,
372    ctx: &DexCtx<'_>,
373    m: &PoolMethod,
374    depth: usize,
375) -> anyhow::Result<Option<String>> {
376    let ind = indent(depth);
377    let desc = m.parsed_desc();
378
379    // Signature.
380    let mut sig = String::new();
381    let a = m.access;
382    if a & ACC_PUBLIC != 0 {
383        sig.push_str("public ");
384    } else if a & ACC_PRIVATE != 0 {
385        sig.push_str("private ");
386    } else if a & ACC_PROTECTED != 0 {
387        sig.push_str("protected ");
388    }
389    let is_clinit = m.name == "<clinit>";
390    let is_init = m.name == "<init>";
391    if a & ACC_STATIC != 0 || is_clinit {
392        sig.push_str("static ");
393    }
394    if a & ACC_FINAL != 0 {
395        sig.push_str("final ");
396    }
397    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 {
398        sig.push_str("synchronized ");
399    }
400    if a & ACC_NATIVE != 0 {
401        sig.push_str("native ");
402    }
403    if a & ACC_ABSTRACT != 0 {
404        sig.push_str("abstract ");
405    }
406    if a & ACC_SYNTHETIC != 0 {
407        sig.push_str("/* synthetic */ ");
408    }
409
410    // Body (needed for parameter names even for abstract methods).
411    let body = decompile_method(pool, class, m).ok().flatten();
412    let param_names: Vec<String> = body
413        .as_ref()
414        .map(|b| {
415            let mut ps: Vec<(u16, String)> =
416                b.vt.vars
417                    .iter()
418                    .filter(|v| v.is_param && v.name != "this")
419                    .map(|v| (v.slot, v.name.clone()))
420                    .collect();
421            ps.sort_by_key(|(s, _)| *s);
422            ps.into_iter().map(|(_, n)| n).collect()
423        })
424        .unwrap_or_else(|| {
425            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
426                .map(|i| format!("p{}", i + 1))
427                .collect()
428        });
429
430    if is_clinit {
431        // `static { ... }` — the caller strips the method name/params.
432    } else {
433        let Some(d) = &desc else { return Ok(None) };
434        if is_init {
435            let (_, simple) = split_name(&class.name);
436            sig.push_str(&simple.replace('$', "."));
437        } else {
438            sig.push_str(&type_name(pool, &d.ret));
439            sig.push(' ');
440            sig.push_str(&m.name);
441        }
442        sig.push('(');
443        let n = d.args.len();
444        let varargs = a & ACC_VARARGS != 0 && n > 0;
445        for (i, arg) in d.args.iter().enumerate() {
446            if i > 0 {
447                sig.push_str(", ");
448            }
449            let name = param_names
450                .get(i)
451                .cloned()
452                .unwrap_or_else(|| format!("p{}", i));
453            if varargs && i + 1 == n {
454                if let JavaType::Array(inner) = arg {
455                    sig.push_str(&type_name(pool, inner));
456                    sig.push_str("...");
457                } else {
458                    sig.push_str(&type_name(pool, arg));
459                }
460            } else {
461                sig.push_str(&type_name(pool, arg));
462            }
463            sig.push(' ');
464            sig.push_str(&name);
465        }
466        sig.push(')');
467    }
468
469    if is_clinit {
470        let Some(b) = body else { return Ok(None) };
471        let printer = Printer::new(ctx, &b.vt);
472        let t_print = std::time::Instant::now();
473        let body_text = printer.into_string(&b.body);
474        crate::method::phase_hit(3, t_print);
475        let mut out = String::new();
476        out.push_str(&ind);
477        out.push_str("static {\n");
478        for line in body_text.lines() {
479            if line.is_empty() {
480                out.push('\n');
481            } else {
482                out.push_str(&ind);
483                out.push_str("    ");
484                out.push_str(line);
485                out.push('\n');
486            }
487        }
488        out.push_str(&ind);
489        out.push_str("}\n");
490        return Ok(Some(out));
491    }
492    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
493        return Ok(Some(format!("{}{};\n", ind, sig)));
494    }
495    let Some(b) = body else { return Ok(None) };
496
497    // The printer indents relative to 0; emit_method adds the absolute
498    // prefix (ind + one level) per line.
499    let mut printer = Printer::new(ctx, &b.vt);
500    match &b.desc.ret {
501        JavaType::Boolean => {
502            printer = printer.with_ret_bool(true);
503        }
504        JavaType::Char => {
505            printer = printer.with_ret_char(true);
506        }
507        JavaType::Byte => {
508            printer = printer.with_ret_narrow(true, false);
509        }
510        JavaType::Short => {
511            printer = printer.with_ret_narrow(false, true);
512        }
513        _ => {}
514    }
515    let t_print = std::time::Instant::now();
516    let body_text = printer.into_string(&b.body);
517    crate::method::phase_hit(3, t_print);
518
519    let mut out = String::new();
520    out.push_str(&ind);
521    out.push_str(&sig);
522    out.push_str(" {\n");
523    if !body_text.trim().is_empty() {
524        for line in body_text.lines() {
525            if line.is_empty() {
526                out.push('\n');
527            } else {
528                out.push_str(&ind);
529                out.push_str("    ");
530                out.push_str(line);
531                out.push('\n');
532            }
533        }
534    }
535    out.push_str(&ind);
536    out.push_str("}\n");
537    Ok(Some(out))
538}
539
540// ---------------------------------------------------------------------------
541// Naming helpers
542// ---------------------------------------------------------------------------
543
544fn indent(depth: usize) -> String {
545    "    ".repeat(depth)
546}
547
548fn split_name(internal: &str) -> (String, String) {
549    match internal.rfind('/') {
550        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
551        None => (String::new(), internal.to_string()),
552    }
553}
554
555/// Dotted source form of an internal name.
556pub fn dotted(internal: &str) -> String {
557    internal.replace('/', ".").replace('$', ".")
558}
559
560fn join_dotted(names: &[String]) -> String {
561    names
562        .iter()
563        .map(|n| dotted(n))
564        .collect::<Vec<_>>()
565        .join(", ")
566}
567
568/// A printable type name (arrays render with `[]` suffixes). Nested class
569/// names dot their `$` when the outer chain is known to the pool.
570pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
571    match t {
572        JavaType::Void => "void".into(),
573        JavaType::Object(n) => print_class_name(pool, n),
574        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
575        other => other.to_java(false),
576    }
577}
578
579/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
580/// its left side names a known class (literal `$` top-level names survive).
581pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
582    let mut out = String::new();
583    let mut rest: &str = internal;
584    loop {
585        match rest.find('$') {
586            Some(i) => {
587                let cand = &rest[..i];
588                let known = pool.get(cand).is_some() || cand == internal;
589                out.push_str(&cand.replace('/', "."));
590                out.push_str(if known { "." } else { "$" });
591                rest = &rest[i + 1..];
592            }
593            None => {
594                out.push_str(&rest.replace('/', "."));
595                return out;
596            }
597        }
598    }
599}
600
601/// Dotted source form of an internal name.
602pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
603    print_class_name(pool, internal)
604}
605
606/// Unused import silencer.
607#[allow(dead_code)]
608fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
609    let _ = java_type_to_generic;
610}