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 for (i, st) in match &body.body {
218 Stmt::Block(v) => v.iter().enumerate(),
219 _ => return None,
220 } {
221 if let Stmt::LocalDef {
222 var,
223 init: Some(Expr::New { cls: ncls, args, .. }),
224 ..
225 } = st
226 {
227 if ncls.as_ref() == class.name && args.len() >= 2 {
228 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(_))) =
229 (&args[0], &args[1])
230 {
231 if java_ident(n).as_ref() != &**n || n.is_empty() {
232 return None;
233 }
234 var_of.insert(*var, const_name.len());
235 const_name.push(n.to_string());
236 const_field.push(String::new());
237 const_extra.push(args[2..].to_vec());
238 drop_stmts.push(i);
239 }
240 }
241 }
242 }
243
244 for (i, st) in match &body.body {
246 Stmt::Block(v) => v.iter().enumerate(),
247 _ => return None,
248 } {
249 if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
250 if let Expr::Field {
251 cls,
252 name: fname,
253 is_static: true,
254 ..
255 } = &**target
256 {
257 if cls.as_ref() != class.name {
258 continue;
259 }
260 if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
261 continue;
262 }
263 if const_field.iter().any(|f| !f.is_empty() && f == &**fname) {
264 return None; }
266 let idx = match &**value {
267 Expr::Local { var, .. } => var_of.get(var).copied()?,
268 Expr::New { cls: ncls, args, .. }
269 if ncls.as_ref() == class.name && args.len() >= 2 =>
270 {
271 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(_))) =
272 (&args[0], &args[1])
273 {
274 if java_ident(n).as_ref() != &**n || n.is_empty() {
275 return None;
276 }
277 let idx = const_name.len();
278 const_name.push(n.to_string());
279 const_field.push(String::new());
280 const_extra.push(args[2..].to_vec());
281 idx
282 } else {
283 return None;
284 }
285 }
286 _ => return None,
287 };
288 const_field[idx] = fname.to_string();
289 drop_stmts.push(i);
290 }
291 }
292 }
293
294 if const_field.len() != const_fields.len() || const_field.iter().any(|f| f.is_empty()) {
296 return None;
297 }
298 {
299 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
300 if !const_name.iter().all(|c| seen.insert(c.as_str())) {
301 return None;
302 }
303 }
304
305 let var2name: jdc_core::FxHashMap<u32, &str> = var_of
308 .iter()
309 .map(|(v, &i)| (*v, const_name[i].as_str()))
310 .collect();
311 let self_name: std::sync::Arc<str> = class.name.as_str().into();
312 let self_ty =
313 jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
314 crate::passes::rewrite_exprs(&mut body.body, &mut |e| {
315 crate::passes::deep_rewrite(e, &mut |x| {
316 if let Expr::Local { var, .. } = x {
317 if let Some(n) = var2name.get(var) {
318 let name: std::sync::Arc<str> = std::sync::Arc::from(*n);
319 *x = Expr::Field {
320 owner: None,
321 cls: self_name.clone(),
322 name,
323 ty: self_ty.clone(),
324 is_static: true,
325 };
326 }
327 }
328 });
329 });
330
331 if let Stmt::Block(vs) = &mut body.body {
333 let drop_set: std::collections::HashSet<usize> =
334 drop_stmts.iter().copied().collect();
335 vs.retain(|_| true);
336 let mut idx = 0usize;
337 vs.retain(|_| {
338 let keep = !drop_set.contains(&idx);
339 idx += 1;
340 keep
341 });
342 }
343
344 let out: Vec<EnumConst> = const_field
345 .into_iter()
346 .zip(const_name)
347 .zip(const_extra)
348 .map(|((field, name), extra_args)| EnumConst {
349 field,
350 name,
351 extra_args,
352 })
353 .collect();
354 Some((out, body))
355}
356
357fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
361 let dummy_vt = jdc_core::var::VarTable::default();
362 let mut p = Printer::new(ctx, &dummy_vt);
363 for (i, a) in args.iter().enumerate() {
364 if i > 0 {
365 out.push_str(", ");
366 }
367 p.expr(a, 0, out);
368 }
369 let _ = pool;
370}
371
372#[allow(clippy::only_used_in_recursion)]
373fn emit_class_body(
374 pool: &DexPool,
375 class: &PoolClass,
376 ctx: &DexCtx<'_>,
377 opts: &ClassOptions,
378 out: &mut String,
379 depth: usize,
380) -> anyhow::Result<()> {
381 let ind = indent(depth);
382 let cname = crate::apply_class_rename(&class.name);
388 let (_, simple) = split_name(&cname);
389 let simple = if depth == 0 {
394 simple.to_string()
395 } else {
396 let seg = simple.rsplit('$').next().unwrap_or(&simple);
399 if seg.is_empty() {
400 simple.to_string()
401 } else {
402 seg.to_string()
403 }
404 };
405 let simple = if is_restricted_type_name(&simple) {
409 format!("_{simple}")
410 } else {
411 simple
412 };
413 let is_iface = class.is_interface();
414 let is_enum = class.is_enum();
415
416 let mut head = String::new();
417 let a = class.access;
418 if a & ACC_PUBLIC != 0 {
419 head.push_str("public ");
420 }
421 if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
425 head.push_str("static ");
426 }
427 if a & ACC_FINAL != 0 && !is_enum {
428 head.push_str("final ");
429 }
430 if a & ACC_ABSTRACT != 0 && !is_iface {
431 head.push_str("abstract ");
432 }
433 if a & ACC_ANNOTATION != 0 {
434 head.push('@');
435 }
436 let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
437 collect_enum_constants(pool, class, ctx)
438 } else {
439 None
440 };
441 if let Some(ecs) = &enum_consts {
442 head.push_str("enum ");
449 head.push_str(&java_ident(&simple));
450 let _ = ecs;
451 } else if is_enum {
452 let abstract_ = a & ACC_ABSTRACT != 0;
456 head.push_str(if abstract_ {
457 "/* enum */ class "
458 } else {
459 "/* enum */ final class "
460 });
461 head.push_str(&java_ident(&simple));
462 } else if is_iface {
463 head.push_str("interface ");
464 head.push_str(&java_ident(&simple));
465 } else {
466 head.push_str("class ");
467 head.push_str(&java_ident(&simple));
468 }
469 if is_iface {
470 if !class.interfaces.is_empty() {
471 head.push_str(" extends ");
472 head.push_str(&join_dotted(pool, &class.interfaces));
473 }
474 } else {
475 if let Some(sup) = &class.super_name {
476 if sup != "java/lang/Object" && !is_enum {
477 head.push_str(" extends ");
478 head.push_str(&print_class_name(pool, sup));
479 }
480 }
481 if !class.interfaces.is_empty() {
482 head.push_str(if is_iface {
483 " extends "
484 } else {
485 " implements "
486 });
487 head.push_str(&join_dotted(pool, &class.interfaces));
488 }
489 }
490
491 out.push_str(&ind);
492 out.push_str(&head);
493 out.push_str(" {\n");
494
495 if let Some((ecs, _)) = &enum_consts {
498 for (i, ec) in ecs.iter().enumerate() {
499 out.push_str(&indent(depth + 1));
500 out.push_str(&java_ident(&ec.name));
501 if !ec.extra_args.is_empty() {
502 let mut line = String::from("(");
503 render_enum_args(ctx, pool, &ec.extra_args, &mut line);
504 line.push(')');
505 out.push_str(&line);
506 }
507 if i + 1 < ecs.len() {
508 out.push_str(",\n");
509 } else {
510 out.push_str(";\n");
511 }
512 }
513 out.push('\n');
514 }
515
516 let mut field_emitted = false;
518 for (i, f) in class.static_fields.iter().enumerate() {
519 if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
521 continue;
522 }
523 if field_emitted || !class.instance_fields.is_empty() {
524 out.push('\n');
525 }
526 field_emitted = true;
527 emit_field(
528 pool,
529 f,
530 class.static_values.get(i),
531 &class.name,
532 out,
533 depth + 1,
534 true,
535 class.is_interface(),
536 );
537 }
538 if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
539 out.push('\n');
540 }
541 for (i, f) in class.instance_fields.iter().enumerate() {
542 if i > 0 {
543 out.push('\n');
544 }
545 emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
546 }
547
548 fn sig_key(m: &PoolMethod) -> (&str, &str) {
560 let d: &str = &m.desc;
561 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
562 let hi = d.find(')').unwrap_or(d.len());
563 (&*m.name, &d[lo..hi])
564 }
565 let methods: Vec<&PoolMethod> = class.all_methods().collect();
566 let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
567 for (i, m) in methods.iter().enumerate() {
568 let key = sig_key(m);
569 match claim.get(&key) {
570 Some(&j)
571 if m.access & crate::access::ACC_BRIDGE == 0
572 && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
573 {
574 claim.insert(key, i);
575 }
576 None => {
577 claim.insert(key, i);
578 }
579 _ => {}
580 }
581 }
582 let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
583 for (i, m) in methods.iter().enumerate() {
584 if &*m.name == "<clinit>" {
585 continue; }
587 if claim.get(&sig_key(m)) != Some(&i) {
588 continue;
589 }
590 if let Some((ecs, _)) = &enum_consts {
594 let d = m.parsed_desc();
595 let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
596 let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
597 let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
598 let one_str = d
599 .as_ref()
600 .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
601 .unwrap_or(false);
602 let static_ = m.is_static();
603 if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
604 continue;
605 }
606 if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
607 continue;
608 }
609 let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
611 let mut c = (*m).clone();
612 c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
613 Some(c)
614 } else {
615 None
616 };
617 let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
618 let mark = out.len();
619 if emitted_any {
620 out.push('\n');
621 }
622 if emit_method(pool, class, ctx, m_ref, depth + 1, out)? {
623 emitted_any = true;
624 } else {
625 out.truncate(mark);
626 }
627 let _ = ecs;
628 continue;
629 }
630 let mark = out.len();
631 if emitted_any {
632 out.push('\n');
633 }
634 if emit_method(pool, class, ctx, m, depth + 1, out)? {
635 emitted_any = true;
636 } else {
637 out.truncate(mark);
638 }
639 }
640 let skip_clinit = class.is_interface();
645 if let Some((_, clinit_body)) = enum_consts.take() {
646 let empty = match &clinit_body.body {
650 Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
651 _ => false,
652 };
653 if !empty {
654 if emitted_any {
655 out.push('\n');
656 }
657 out.push_str(&indent(depth + 1));
658 out.push_str("static {\n");
659 let p = Printer::new(ctx, &clinit_body.vt);
660 let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
661 for line in text.lines() {
662 if line.trim().is_empty() {
663 continue;
664 }
665 out.push_str(line);
666 out.push('\n');
667 }
668 out.push_str(&indent(depth + 1));
669 out.push_str("}\n");
670 emitted_any = true;
671 }
672 } else if let Some(clinit) = (!skip_clinit)
673 .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
674 .flatten()
675 {
676 let mark = out.len();
677 if emitted_any {
678 out.push('\n');
679 }
680 if emit_method(pool, class, ctx, clinit, depth + 1, out)? {
681 emitted_any = true;
682 } else {
683 out.truncate(mark);
684 }
685 }
686
687 for nested in nested_members(pool, class, ctx) {
690 if emitted_any {
691 out.push('\n');
692 }
693 let nested_ctx = DexCtx::new(pool, nested);
694 out.push('\n');
695 emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
696 emitted_any = true;
697 }
698
699 out.push_str(&ind);
700 out.push_str("}\n");
701 Ok(())
702}
703
704fn nested_members<'a>(
707 pool: &'a DexPool,
708 class: &'a PoolClass,
709 ctx: &DexCtx<'_>,
710) -> Vec<&'a PoolClass> {
711 let mut out = Vec::new();
712 for name in pool.children_of(&class.name) {
713 let Some(pc) = pool.get(name) else { continue };
714 let Some(rest) = name
721 .strip_prefix(class.name.as_str())
722 .and_then(|t| t.strip_prefix('$'))
723 else {
724 continue;
725 };
726 if rest.is_empty() || rest.starts_with('-') {
727 continue;
728 }
729 if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
730 continue;
731 }
732 let tail = rest.rsplit('$').next().unwrap_or(rest);
733 if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
734 continue; }
736 if tail.starts_with(|c: char| c.is_ascii_digit()) {
737 continue; }
739 out.push(pc);
740 }
741 out
742}
743
744#[allow(clippy::too_many_arguments)]
748fn emit_field(
749 pool: &DexPool,
750 f: &crate::PoolField,
751 init: Option<&StaticValue>,
752 f_class: &str,
753 out: &mut String,
754 depth: usize,
755 is_static: bool,
756 require_init: bool,
757) {
758 let ind = indent(depth);
759 let mut line = String::new();
760 let a = f.access;
761 if a & ACC_PUBLIC != 0 {
762 line.push_str("public ");
763 } else if a & ACC_PRIVATE != 0 {
764 line.push_str("private ");
765 } else if a & ACC_PROTECTED != 0 {
766 line.push_str("protected ");
767 }
768 if is_static {
769 line.push_str("static ");
770 }
771 if a & ACC_FINAL != 0 {
772 line.push_str("final ");
773 }
774 if a & ACC_SYNTHETIC != 0 {
775 line.push_str("/* synthetic */ ");
776 }
777 if a & ACC_TRANSIENT_HINT != 0 {
778 line.push_str("transient ");
779 }
780 if a & ACC_VOLATILE_HINT != 0 {
781 line.push_str("volatile ");
782 }
783 let ty = type_name(pool, &desc_type(&f.desc));
784 line.push_str(&ty);
785 line.push(' ');
786 let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
787 line.push_str(&java_ident(fname));
788 let mut rendered = None;
789 if let Some(v) = init {
790 rendered = render_static_value(pool, v, f_class);
791 }
792 if rendered.is_none() && require_init {
793 let default = match desc_type(&f.desc) {
797 JavaType::Boolean => "false",
798 JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
799 JavaType::Long => "0L",
800 JavaType::Float => "0.0F",
801 JavaType::Double => "0.0",
802 _ => "null",
803 };
804 rendered = Some(default.to_string());
805 }
806 if let Some(text) = rendered {
807 let text = if matches!(desc_type(&f.desc), JavaType::Long)
811 && text.chars().all(|c| c.is_ascii_digit() || c == '-')
812 {
813 format!("{text}L")
814 } else {
815 text
816 };
817 line.push_str(" = ");
818 line.push_str(&text);
819 }
820 line.push(';');
821 out.push_str(&ind);
822 out.push_str(&line);
823 out.push('\n');
824}
825
826pub const ACC_VOLATILE_HINT: u32 = 0x40;
829pub const ACC_TRANSIENT_HINT: u32 = 0x80;
830
831fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
832 Some(match v {
833 StaticValue::Int(i) => i.to_string(),
834 StaticValue::Float(f) => format_float(*f as f64, true),
835 StaticValue::Double(d) => format_float(*d, false),
836 StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
837 StaticValue::Type(t) => format!("{}.class", dotted(t)),
838 StaticValue::Boolean(b) => b.to_string(),
839 StaticValue::Null => "null".into(),
840 StaticValue::Field(cls, name) => {
841 let n = java_ident(name);
842 if cls == owner {
843 n.into_owned()
844 } else {
845 format!("{}.{}", print_class_name(pool, cls), n)
846 }
847 }
848 StaticValue::Other => return None,
849 })
850}
851
852fn emit_method(
857 pool: &DexPool,
858 class: &PoolClass,
859 ctx: &DexCtx<'_>,
860 m: &PoolMethod,
861 depth: usize,
862 out: &mut String,
863) -> anyhow::Result<bool> {
864 let ind = indent(depth);
865 let desc = m.parsed_desc();
866
867 let mut sig = String::with_capacity(192);
869 let a = m.access;
870 if a & ACC_PUBLIC != 0 {
871 sig.push_str("public ");
872 } else if a & ACC_PRIVATE != 0 {
873 sig.push_str("private ");
874 } else if a & ACC_PROTECTED != 0 {
875 sig.push_str("protected ");
876 }
877 let is_clinit = &*m.name == "<clinit>";
878 let is_init = &*m.name == "<init>";
879 if a & ACC_STATIC != 0 || is_clinit {
880 sig.push_str("static ");
881 }
882 if a & ACC_FINAL != 0 {
883 sig.push_str("final ");
884 }
885 if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
889 sig.push_str("synchronized ");
890 }
891 if a & ACC_NATIVE != 0 {
892 sig.push_str("native ");
893 }
894 if a & ACC_ABSTRACT != 0 {
895 sig.push_str("abstract ");
896 }
897 if a & ACC_SYNTHETIC != 0 {
898 sig.push_str("/* synthetic */ ");
899 }
900
901 let body = decompile_method(pool, class, m).ok().flatten();
903 let param_names: Vec<String> = body
904 .as_ref()
905 .map(|b| {
906 let mut ps: Vec<(u16, String)> =
907 b.vt.vars
908 .iter()
909 .filter(|v| v.is_param && v.name != "this")
910 .map(|v| (v.slot, v.name.clone()))
911 .collect();
912 ps.sort_by_key(|(s, _)| *s);
913 ps.into_iter().map(|(_, n)| n).collect()
914 })
915 .unwrap_or_else(|| {
916 (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
917 .map(|i| format!("p{}", i + 1))
918 .collect()
919 });
920
921 if is_clinit {
922 } else {
924 let Some(d) = &desc else { return Ok(false) };
925 if is_init {
926 let dname = crate::apply_class_rename(&class.name);
931 let (_, simple) = split_name(&dname);
932 let base = if depth <= 1 {
936 simple.to_string()
937 } else {
938 let seg = simple.rsplit('$').next().unwrap_or(&simple);
941 if seg.is_empty() {
942 simple.to_string()
943 } else {
944 seg.to_string()
945 }
946 };
947 let name = if is_restricted_type_name(&base) {
948 format!("_{base}")
949 } else {
950 base
951 };
952 sig.push_str(&java_ident(&name));
953 } else {
954 sig.push_str(&type_name(pool, &d.ret));
955 sig.push(' ');
956 let mname =
957 jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
958 sig.push_str(&java_ident(mname));
959 }
960 sig.push('(');
961 let n = d.args.len();
962 let varargs = a & ACC_VARARGS != 0 && n > 0;
963 for (i, arg) in d.args.iter().enumerate() {
964 if i > 0 {
965 sig.push_str(", ");
966 }
967 let name = param_names
968 .get(i)
969 .cloned()
970 .unwrap_or_else(|| format!("p{}", i));
971 if varargs && i + 1 == n {
972 if let JavaType::Array(inner) = arg {
973 sig.push_str(&type_name(pool, inner));
974 sig.push_str("...");
975 } else {
976 sig.push_str(&type_name(pool, arg));
977 }
978 } else {
979 sig.push_str(&type_name(pool, arg));
980 }
981 sig.push(' ');
982 sig.push_str(&java_ident(&name));
985 }
986 sig.push(')');
987 }
988
989 if is_clinit {
990 let Some(b) = body else { return Ok(false) };
991 out.push_str(&ind);
996 out.push_str("static {\n");
997 let hdr = out.len();
998 let printer = Printer::new(ctx, &b.vt)
999 .with_indent(depth + 1)
1000 .with_output(std::mem::take(out));
1001 let t_print = std::time::Instant::now();
1002 let mut rendered = printer.into_string(&b.body);
1003 crate::method::phase_hit(3, t_print);
1004 if rendered[hdr..].trim().is_empty() {
1005 rendered.truncate(hdr);
1006 }
1007 rendered.push_str(&ind);
1008 rendered.push_str("}\n");
1009 *out = rendered;
1010 return Ok(true);
1011 }
1012 if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
1013 out.push_str(&ind);
1014 out.push_str(&sig);
1015 out.push_str(";\n");
1016 return Ok(true);
1017 }
1018 let Some(b) = body else { return Ok(false) };
1019
1020 let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
1022 match &b.desc.ret {
1023 JavaType::Boolean => {
1024 printer = printer.with_ret_bool(true);
1025 }
1026 JavaType::Char => {
1027 printer = printer.with_ret_char(true);
1028 }
1029 JavaType::Byte => {
1030 printer = printer.with_ret_narrow(true, false);
1031 }
1032 JavaType::Short => {
1033 printer = printer.with_ret_narrow(false, true);
1034 }
1035 _ => {}
1036 }
1037 out.push_str(&ind);
1038 out.push_str(&sig);
1039 out.push_str(" {\n");
1040 let hdr = out.len();
1041 let printer = printer.with_output(std::mem::take(out));
1042 let t_print = std::time::Instant::now();
1043 let mut rendered = printer.into_string(&b.body);
1044 crate::method::phase_hit(3, t_print);
1045 if rendered[hdr..].trim().is_empty() {
1046 rendered.truncate(hdr);
1047 }
1048 rendered.push_str(&ind);
1049 rendered.push_str("}\n");
1050 *out = rendered;
1051 Ok(true)
1052}
1053
1054fn indent(depth: usize) -> String {
1059 " ".repeat(depth)
1060}
1061
1062pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1072 static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1075 if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
1076 eprintln!("[ident] {name:?}");
1077 }
1078 let clean = name
1079 .chars()
1080 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
1081 let keyword = matches!(
1085 name,
1086 "abstract"
1087 | "assert"
1088 | "boolean"
1089 | "break"
1090 | "byte"
1091 | "case"
1092 | "catch"
1093 | "char"
1094 | "class"
1095 | "const"
1096 | "continue"
1097 | "default"
1098 | "do"
1099 | "double"
1100 | "else"
1101 | "enum"
1102 | "extends"
1103 | "final"
1104 | "finally"
1105 | "float"
1106 | "for"
1107 | "goto"
1108 | "if"
1109 | "implements"
1110 | "import"
1111 | "instanceof"
1112 | "int"
1113 | "interface"
1114 | "long"
1115 | "native"
1116 | "new"
1117 | "package"
1118 | "private"
1119 | "protected"
1120 | "public"
1121 | "return"
1122 | "short"
1123 | "static"
1124 | "strictfp"
1125 | "super"
1126 | "switch"
1127 | "synchronized"
1128 | "this"
1129 | "throw"
1130 | "throws"
1131 | "transient"
1132 | "try"
1133 | "void"
1134 | "volatile"
1135 | "while"
1136 | "true"
1137 | "false"
1138 | "null"
1139 | "_"
1143 );
1144 let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
1148 if clean && !keyword && !digit_start {
1149 std::borrow::Cow::Borrowed(name)
1150 } else if keyword || digit_start {
1151 std::borrow::Cow::Owned(format!("_{name}"))
1152 } else {
1153 let mapped: String = name
1156 .chars()
1157 .map(|c| {
1158 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1159 c
1160 } else {
1161 '_'
1162 }
1163 })
1164 .collect();
1165 if mapped == "_" {
1166 std::borrow::Cow::Owned("__".to_string())
1167 } else {
1168 std::borrow::Cow::Owned(mapped)
1169 }
1170 }
1171}
1172
1173fn split_name(internal: &str) -> (String, String) {
1174 match internal.rfind('/') {
1175 Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
1176 None => (String::new(), internal.to_string()),
1177 }
1178}
1179
1180pub fn dotted(internal: &str) -> String {
1182 let mut out = internal.replace('/', ".");
1183 let mut i = 0;
1186 while let Some(p) = out[i..].find('$') {
1187 let at = i + p;
1188 let head_ok = at > 0
1191 && out[..at]
1192 .chars()
1193 .next_back()
1194 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
1195 let tail_ok = out[at + 1..]
1196 .chars()
1197 .next()
1198 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1199 if tail_ok && head_ok {
1200 out.replace_range(at..at + 1, ".");
1201 i = at + 1;
1202 } else {
1203 i = at + 1;
1204 }
1205 }
1206 sanitize_fq(&out)
1209}
1210
1211fn join_dotted(pool: &DexPool, names: &[String]) -> String {
1212 names
1213 .iter()
1214 .map(|n| print_class_name(pool, n))
1215 .collect::<Vec<_>>()
1216 .join(", ")
1217}
1218
1219pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
1222 match t {
1223 JavaType::Void => "void".into(),
1224 JavaType::Object(n) => print_class_name(pool, n),
1225 JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
1226 other => other.to_java(false),
1227 }
1228}
1229
1230pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
1233 let cow = crate::apply_class_rename(internal);
1234 let internal: &str = &cow;
1235 let mut out = String::new();
1236 let mut off = 0usize;
1242 loop {
1243 let rest = &internal[off..];
1244 match rest.find('$') {
1245 Some(i) => {
1246 let seg = &rest[..i];
1247 let prefix = &internal[..off + i];
1248 let known = pool.get(prefix).is_some()
1249 || jdc_core::rename::is_renamed_display(prefix)
1250 || !pool.has_dollar_prefix(prefix);
1251 let tail_ok = rest[i + 1..]
1258 .chars()
1259 .next()
1260 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1261 out.push_str(&seg.replace('/', "."));
1262 out.push_str(if known && tail_ok { "." } else { "$" });
1263 off += i + 1;
1264 }
1265 None => {
1266 out.push_str(&rest.replace('/', "."));
1267 return sanitize_ref(&out);
1268 }
1269 }
1270 }
1271}
1272
1273pub(crate) fn sanitize_fq(dotted: &str) -> String {
1279 dotted
1280 .split('.')
1281 .map(|seg| {
1282 if is_java_keyword_name(seg)
1283 || is_restricted_type_name(seg)
1284 || seg.chars().next().is_some_and(|c| c.is_ascii_digit())
1285 {
1286 format!("_{seg}")
1287 } else if seg
1288 .chars()
1289 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
1290 {
1291 seg.to_string()
1292 } else {
1293 let mapped: String = seg
1296 .chars()
1297 .map(|c| {
1298 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1299 c
1300 } else {
1301 '_'
1302 }
1303 })
1304 .collect();
1305 if mapped == "_" {
1306 "__".to_string()
1307 } else {
1308 mapped
1309 }
1310 }
1311 })
1312 .collect::<Vec<_>>()
1313 .join(".")
1314}
1315
1316pub(crate) fn is_restricted_type_name(s: &str) -> bool {
1320 matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
1321}
1322
1323fn is_java_keyword_name(s: &str) -> bool {
1324 matches!(
1325 s,
1326 "abstract"
1327 | "assert"
1328 | "boolean"
1329 | "break"
1330 | "byte"
1331 | "case"
1332 | "catch"
1333 | "char"
1334 | "class"
1335 | "const"
1336 | "continue"
1337 | "default"
1338 | "do"
1339 | "double"
1340 | "else"
1341 | "enum"
1342 | "extends"
1343 | "final"
1344 | "finally"
1345 | "float"
1346 | "for"
1347 | "goto"
1348 | "if"
1349 | "implements"
1350 | "import"
1351 | "instanceof"
1352 | "int"
1353 | "interface"
1354 | "long"
1355 | "native"
1356 | "new"
1357 | "package"
1358 | "private"
1359 | "protected"
1360 | "public"
1361 | "return"
1362 | "short"
1363 | "static"
1364 | "strictfp"
1365 | "super"
1366 | "switch"
1367 | "synchronized"
1368 | "this"
1369 | "throw"
1370 | "throws"
1371 | "transient"
1372 | "try"
1373 | "void"
1374 | "volatile"
1375 | "while"
1376 | "true"
1377 | "false"
1378 | "null"
1379 | "_"
1383 )
1384}
1385
1386fn sanitize_ref(name: &str) -> String {
1387 sanitize_fq(name)
1388}
1389
1390pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
1392 print_class_name(pool, internal)
1393}
1394
1395#[allow(dead_code)]
1397fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
1398 let _ = java_type_to_generic;
1399}