1use 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 pub provenance: bool,
18}
19
20impl Default for ClassOptions {
21 fn default() -> Self {
22 ClassOptions { provenance: true }
23 }
24}
25
26pub 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 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
71fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
76 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 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 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_class_body(pool, class, &ctx, opts, &mut out, 0)?;
154 Ok(out)
155}
156
157use 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
172fn 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 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(); let mut drop_stmts: Vec<usize> = Vec::new();
210
211 if !matches!(&body.body, Stmt::Block(_)) {
213 return None;
214 }
215
216 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 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; }
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 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 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 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 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
387fn 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 Stmt::ExprStmt(_) => {}
416 _ => *tainted = true,
417 }
418}
419
420fn 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 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
513fn 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 let cname = crate::apply_class_rename(&class.name);
544 let (_, simple) = split_name(&cname);
545 let simple = if depth == 0 {
550 simple.to_string()
551 } else {
552 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 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 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 head.push_str("enum ");
605 head.push_str(&java_ident(&simple));
606 let _ = ecs;
607 } else if is_enum {
608 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 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 let mut field_emitted = false;
674 for (i, f) in class.static_fields.iter().enumerate() {
675 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 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; }
743 if claim.get(&sig_key(m)) != Some(&i) {
744 continue;
745 }
746 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 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 let skip_clinit = class.is_interface();
801 if let Some((_, clinit_body)) = enum_consts.take() {
802 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 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
860fn 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 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; }
892 if tail.starts_with(|c: char| c.is_ascii_digit()) {
893 continue; }
895 out.push(pc);
896 }
897 out
898}
899
900#[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 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 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
982pub 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
1008fn 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 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 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 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 } else {
1080 let Some(d) = &desc else { return Ok(false) };
1081 if is_init {
1082 let dname = crate::apply_class_rename(&class.name);
1087 let (_, simple) = split_name(&dname);
1088 let base = if depth <= 1 {
1092 simple.to_string()
1093 } else {
1094 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 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 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 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
1210fn indent(depth: usize) -> String {
1215 " ".repeat(depth)
1216}
1217
1218pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1228 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 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 | "_"
1299 );
1300 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 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
1336pub fn dotted(internal: &str) -> String {
1338 let mut out = internal.replace('/', ".");
1339 let mut i = 0;
1342 while let Some(p) = out[i..].find('$') {
1343 let at = i + p;
1344 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 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
1375pub 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
1386pub 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 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 || !pool.get(internal).is_some();
1413 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
1435pub(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 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
1478pub(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 | "_"
1545 )
1546}
1547
1548fn sanitize_ref(name: &str) -> String {
1549 sanitize_fq(name)
1550}
1551
1552pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
1554 print_class_name(pool, internal)
1555}
1556
1557#[allow(dead_code)]
1559fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
1560 let _ = java_type_to_generic;
1561}