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};
13use ddc_dex::insn::InsnKind;
14
15#[derive(Debug, Clone)]
16pub struct ClassOptions {
17 pub provenance: bool,
19}
20
21impl Default for ClassOptions {
22 fn default() -> Self {
23 ClassOptions { provenance: true }
24 }
25}
26
27pub type PendingMonitor = (
37 std::sync::mpsc::Receiver<Result<String, String>>,
38 String,
39 std::time::Instant,
40);
41
42pub fn decompile_class(
43 pool: &std::sync::Arc<DexPool>,
44 class: &PoolClass,
45 opts: &ClassOptions,
46 pending: &std::sync::Mutex<Vec<PendingMonitor>>,
47) -> anyhow::Result<String> {
48 if class_is_risky(pool, class) {
49 let pool2 = pool.clone();
53 let cls = class.clone();
54 let opts2 = opts.clone();
55 let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
56 let name = class.name.clone();
57 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
63 let _ = std::thread::Builder::new()
64 .stack_size(64 * 1024 * 1024)
65 .spawn(move || {
66 let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
67 let _ = tx.send(r);
68 });
69 pending.lock().unwrap().push((rx, name, deadline));
70 return Err(anyhow::anyhow!(
71 "deferred to monitored thread (pathological-CFG guard)"
72 ));
73 }
74 decompile_class_impl(pool, class, opts)
75}
76
77fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
82 for m in class.all_methods() {
85 if m.code_off == 0 {
86 continue;
87 }
88 if let Some(dex) = pool.dex(m.dex_idx) {
89 if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
90 if insns > 16_000 {
91 return true;
92 }
93 }
94 }
95 }
96 false
97}
98
99fn decompile_class_impl(
100 pool: &DexPool,
101 class: &PoolClass,
102 opts: &ClassOptions,
103) -> anyhow::Result<String> {
104 let ctx = DexCtx::new(pool, class);
105 let mut out = String::with_capacity(
114 (class.all_methods().count() * 1024).min(256 * 1024) + 512,
115 );
116 if opts.provenance {
117 static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
118 out.push_str(BANNER.get_or_init(|| {
119 format!(
120 "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
121 env!("CARGO_PKG_VERSION")
122 )
123 }));
124 if let Some(dex) = pool.dex(class.dex_idx) {
130 if let Some(label) = pool.dex_labels.get(class.dex_idx) {
131 out.push_str("// From: ");
132 out.push_str(label);
133 out.push_str(" (DEX ");
134 out.push_str(&dex.version);
135 out.push_str(")\n");
136 }
137 }
138 }
139 if let Some(src) = &class.source_file {
140 out.push_str("// Source file: ");
141 out.push_str(src);
142 out.push('\n');
143 }
144 if class.is_synthetic() {
145 out.push_str("// synthetic\n");
146 }
147 let (pkg, _) = split_name(&class.name);
148 let mut obscured_map = compute_obscured_renders(pool, class);
154 let blocked: jdc_core::FxHashSet<String> = pool
158 .package_simples()
159 .get(&pkg)
160 .cloned()
161 .unwrap_or_default();
162 obscured_map.retain(|_internal, simple| !blocked.contains(simple));
163 set_obscured_state(class.name.clone(), obscured_map.clone(), blocked.clone());
164 let mut body_buf = String::with_capacity(out.capacity() / 2);
165 let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
166 let recorded = take_recorded_and_clear();
167 body_res?;
168 if !pkg.is_empty() {
169 out.push('\n');
170 out.push_str("package ");
171 let dotted = pkg.replace('/', ".");
172 out.push_str(&sanitize_fq(&dotted));
173 out.push_str(";\n");
174 let mut import_set: jdc_core::FxHashSet<String> =
178 obscured_map.keys().cloned().collect();
179 for r in recorded {
180 if pool.get(&r).is_some() {
181 let simple = r.rsplit('/').next().unwrap_or("");
182 if !blocked.contains(simple) {
183 import_set.insert(r);
184 }
185 }
186 }
187 let mut imports: Vec<String> = import_set
188 .iter()
189 .map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
190 .collect();
191 imports.sort();
192 for display in imports {
193 out.push_str(&format!("import {};\n", display));
194 }
195 }
196 out.push('\n');
197 out.push_str(&body_buf);
198 Ok(out)
199}
200
201use jdc_core::ir::expr::{ConstVal, Expr};
208use jdc_core::ir::stmt::Stmt;
209struct EnumConst {
210 #[allow(dead_code)]
211 field: String,
212 name: String,
213 extra_args: Vec<Expr>,
214}
215
216fn collect_enum_constants(
224 pool: &DexPool,
225 class: &PoolClass,
226 ctx: &DexCtx<'_>,
227) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
228 let _ = ctx;
229 let const_fields: Vec<&PoolField> = class
230 .static_fields
231 .iter()
232 .filter(|f| f.access & crate::access::ACC_ENUM != 0)
233 .collect();
234 if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
240 return None;
241 }
242 let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
243 let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
244
245 use std::collections::HashMap;
254 let mut const_name: Vec<String> = Vec::new();
255 let mut const_field: Vec<String> = Vec::new();
256 let mut const_extra: Vec<Vec<Expr>> = Vec::new();
257 let mut const_ord: Vec<i64> = Vec::new();
262 let mut var_of: HashMap<u32, usize> = HashMap::default(); let mut drop_stmts: Vec<usize> = Vec::new();
264
265 if !matches!(&body.body, Stmt::Block(_)) { return None; }
267
268 let mut defs: HashMap<u32, &Expr> = HashMap::default();
274 let mut tainted = false;
275 let self_name: std::sync::Arc<str> = class.name.as_str().into();
276 let self_ty =
277 jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
278 let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
285 for (i, st) in match &body.body {
286 Stmt::Block(v) => v.iter().enumerate(),
287 _ => return None,
288 } {
289 let def_shape: Option<(u32, &Expr)> = match st {
291 Stmt::LocalDef {
292 var,
293 init: Some(e),
294 ..
295 } => Some((*var, e)),
296 Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
297 match (&**target, &**value) {
298 (Expr::Local { var, .. }, e) => Some((*var, e)),
299 _ => None,
300 }
301 }
302 _ => None,
303 };
304 if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
305 if ncls.as_ref() == class.name && args.len() >= 2 {
306 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
307 (&args[0], &args[1])
308 {
309 if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
310 var_of.insert(var, const_name.len());
311 def_site.insert(i, (var, const_name.len()));
312 const_ord.push(*ord0 as i64);
313 const_name.push(n.to_string());
314 const_field.push(String::new());
315 const_extra.push(resolve_enum_extras(
316 &args[2..],
317 &defs,
318 tainted,
319 &var_of,
320 &const_name,
321 &self_name,
322 &self_ty,
323 )?);
324 drop_stmts.push(i);
325 }
326 }
327 }
328 track_def(st, &mut defs, &mut tainted);
329 }
330
331 let mut defs2: HashMap<u32, &Expr> = HashMap::default();
336 let mut tainted2 = false;
337 let mut cur_def: HashMap<u32, usize> = HashMap::default();
338 for (i, st) in match &body.body {
339 Stmt::Block(v) => v.iter().enumerate(),
340 _ => return None,
341 } {
342 if let Some((var, idx)) = def_site.get(&i) {
343 cur_def.insert(*var, *idx);
344 }
345 if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
346 if let Expr::Field {
347 cls,
348 name: fname,
349 is_static: true,
350 ..
351 } = &**target
352 {
353 if cls.as_ref() != class.name {
354 continue;
355 }
356 if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
357 continue;
358 }
359 if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { { return None; } }
361 let idx = match &**value {
362 Expr::Local { var, .. } => cur_def.get(var).copied()?,
363 Expr::New { cls: ncls, args, .. }
364 if ncls.as_ref() == class.name && args.len() >= 2 =>
365 {
366 if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
367 (&args[0], &args[1])
368 {
369 if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
370 let idx = const_name.len();
371 const_ord.push(*ord0 as i64);
372 const_name.push(n.to_string());
373 const_field.push(String::new());
374 const_extra.push(resolve_enum_extras(
375 &args[2..],
376 &defs2,
377 tainted2,
378 &var_of,
379 &const_name,
380 &self_name,
381 &self_ty,
382 )?);
383 idx
384 } else {
385 { return None; }
386 }
387 }
388 _ => return None,
389 };
390 const_field[idx] = fname.to_string();
391 drop_stmts.push(i);
392 }
393 }
394 track_def(st, &mut defs2, &mut tainted2);
395 }
396 let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
399 .iter()
400 .map(|(k, v)| (*k, (*v).clone()))
401 .collect();
402 drop(defs2);
403 let defs2: std::collections::HashMap<u32, &Expr> =
404 defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
405
406 if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
413 return None;
414 }
415 for i in 0..const_name.len() {
422 let field = &const_field[i];
423 if !field.is_empty() && field != &const_name[i] {
424 let id = java_ident(field);
425 if id.is_empty() || id.as_ref() != field.as_str() {
426 { return None; }
427 }
428 const_name[i] = field.clone();
429 }
430 }
431 {
432 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
433 if !const_name.iter().all(|c| seen.insert(c.as_str())) { return None; }
434 }
435
436 let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
444 {
445 let self_cls: &str = class.name.as_str();
446 if let Stmt::Block(vs) = &mut body.body {
447 for st in vs.iter_mut() {
448 crate::passes::walk_stmt_exprs(st, &mut |e| {
449 crate::passes::deep_rewrite(e, &mut |x| {
450 let Expr::NewArray { elem, init: Some(list), .. } = x else {
451 return;
452 };
453 if elem.erased() != JavaType::Object(self_cls.into()) {
454 return;
455 }
456 for slot in list.iter_mut() {
457 let (n, ord, rest) = match slot {
458 Expr::New { cls, args, raw: false, .. }
459 if cls.as_ref() == self_cls && args.len() >= 2 =>
460 {
461 match (&args[0], &args[1]) {
462 (
463 Expr::Const(ConstVal::Str(sv)),
464 Expr::Const(ConstVal::Int(o)),
465 ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
466 _ => continue,
467 }
468 }
469 _ => continue,
470 };
471 if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
472 continue;
473 }
474 if const_name.contains(&n)
475 || synth.iter().any(|(_, s2, _)| *s2 == n)
476 {
477 continue;
478 }
479 let Some(extras) = resolve_enum_extras(
480 &rest,
481 &defs2,
482 tainted2,
483 &var_of,
484 &const_name,
485 &self_name,
486 &self_ty,
487 ) else {
488 continue;
489 };
490 *slot = Expr::Field {
491 owner: None,
492 cls: self_name.clone(),
493 name: std::sync::Arc::from(n.as_str()),
494 ty: self_ty.clone(),
495 is_static: true,
496 };
497 synth.push((ord, n, extras));
498 }
499 });
500 });
501 }
502 }
503 }
504
505 {
512 let mut cur: HashMap<u32, usize> = HashMap::default();
513 if let Stmt::Block(vs) = &mut body.body {
514 for (i, st) in vs.iter_mut().enumerate() {
515 if let Some((var, idx)) = def_site.get(&i) {
516 cur.insert(*var, *idx);
517 }
518 crate::passes::walk_stmt_exprs(st, &mut |e| {
519 crate::passes::deep_rewrite(e, &mut |x| {
520 if let Expr::Local { var, .. } = x {
521 if let Some(&idx) = cur.get(var) {
522 let name: std::sync::Arc<str> =
523 std::sync::Arc::from(const_name[idx].as_str());
524 *x = Expr::Field {
525 owner: None,
526 cls: self_name.clone(),
527 name,
528 ty: self_ty.clone(),
529 is_static: true,
530 };
531 }
532 }
533 });
534 });
535 }
536 }
537 }
538
539 if let Stmt::Block(vs) = &mut body.body {
541 let drop_set: std::collections::HashSet<usize> =
542 drop_stmts.iter().copied().collect();
543 vs.retain(|_| true);
544 let mut idx = 0usize;
545 vs.retain(|_| {
546 let keep = !drop_set.contains(&idx);
547 idx += 1;
548 keep
549 });
550 }
551 crate::passes::drop_dead_locals(&mut body.body);
555
556 let mut merged: Vec<(i64, EnumConst)> = const_ord
557 .into_iter()
558 .zip(
559 const_field
560 .into_iter()
561 .zip(const_name)
562 .zip(const_extra)
563 .map(|((field, name), extra_args)| EnumConst {
564 field,
565 name,
566 extra_args,
567 }),
568 )
569 .collect();
570 for (ord, name, extra_args) in synth {
571 merged.push((
572 ord,
573 EnumConst {
574 field: name.clone(),
575 name,
576 extra_args,
577 },
578 ));
579 }
580 if merged.is_empty() {
581 return None; }
583 merged.sort_by_key(|(o, _)| *o);
584 let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
594 let mut member_taken: jdc_core::FxHashSet<String> = merged
595 .iter()
596 .map(|(_, c)| c.name.clone())
597 .chain(
598 class
599 .static_fields
600 .iter()
601 .chain(class.instance_fields.iter())
602 .map(|f| f.name.to_string()),
603 )
604 .chain(class.all_methods().map(|m| m.name.to_string()))
605 .collect();
606 let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
607 let mut expect: i64 = 0;
608 let mut pad_k = 0u32;
609 for (ord, c) in merged {
610 if ord < expect {
611 { return None; } }
613 if ord > expect {
614 if has_extras {
615 { return None; } }
617 while expect < ord {
618 let pname = loop {
619 let cand = format!("_r{pad_k}");
620 pad_k += 1;
621 if !member_taken.contains(&cand) {
622 member_taken.insert(cand.clone());
623 break cand;
624 }
625 };
626 padded.push(EnumConst {
627 field: pname.clone(),
628 name: pname,
629 extra_args: Vec::new(),
630 });
631 expect += 1;
632 }
633 }
634 expect = ord + 1;
635 padded.push(c);
636 }
637 let out = padded;
638 Some((out, body))
639}
640
641fn track_def<'e>(
646 st: &'e Stmt,
647 defs: &mut std::collections::HashMap<u32, &'e Expr>,
648 tainted: &mut bool,
649) {
650 match st {
651 Stmt::LocalDef { var, init, .. } => match init {
652 Some(e) => {
653 defs.insert(*var, e);
654 }
655 None => {
656 defs.remove(var);
657 }
658 },
659 Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
660 if let Expr::Local { var, .. } = &**target {
661 if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
662 defs.insert(*var, value);
663 } else {
664 defs.remove(var);
665 }
666 }
667 }
668 Stmt::ExprStmt(_) => {}
670 _ => *tainted = true,
671 }
672}
673
674fn resolve_enum_extras(
691 extras: &[Expr],
692 defs: &std::collections::HashMap<u32, &Expr>,
693 tainted: bool,
694 var_of: &std::collections::HashMap<u32, usize>,
695 const_name: &[String],
696 self_name: &std::sync::Arc<str>,
697 self_ty: &jdc_core::ir::expr::TypeRef,
698) -> Option<Vec<Expr>> {
699 extras
700 .iter()
701 .map(|e| {
702 let mut out = e.clone();
703 let mut fail = false;
704 resolve_locals_in(
705 &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
706 &mut fail,
707 );
708 if fail {
709 None
710 } else {
711 Some(out)
712 }
713 })
714 .collect()
715}
716
717#[allow(clippy::too_many_arguments)]
718fn resolve_locals_in(
719 e: &mut Expr,
720 defs: &std::collections::HashMap<u32, &Expr>,
721 tainted: bool,
722 var_of: &std::collections::HashMap<u32, usize>,
723 const_name: &[String],
724 self_name: &std::sync::Arc<str>,
725 self_ty: &jdc_core::ir::expr::TypeRef,
726 depth: u32,
727 fail: &mut bool,
728) {
729 crate::passes::deep_rewrite(e, &mut |x| {
730 if let Expr::Local { var, .. } = x {
731 if let Some(&ci) = var_of.get(var) {
735 if let Some(nm) = const_name.get(ci) {
736 *x = Expr::Field {
737 owner: None,
738 cls: self_name.clone(),
739 name: std::sync::Arc::from(nm.as_str()),
740 ty: self_ty.clone(),
741 is_static: true,
742 };
743 return;
744 }
745 }
746 if tainted || depth > 4 {
747 *fail = true;
748 return;
749 }
750 let Some(d) = defs.get(var).copied() else {
751 *fail = true;
752 return;
753 };
754 let mut sub = d.clone();
755 resolve_locals_in(
756 &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
757 depth + 1, fail,
758 );
759 if *fail {
760 return;
761 }
762 *x = sub;
763 }
764 });
765}
766
767fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
771 let dummy_vt = jdc_core::var::VarTable::default();
772 let mut p = Printer::new(ctx, &dummy_vt);
773 for (i, a) in args.iter().enumerate() {
774 if i > 0 {
775 out.push_str(", ");
776 }
777 p.expr(a, 0, out);
778 }
779 let _ = pool;
780}
781
782#[allow(clippy::only_used_in_recursion)]
783fn emit_class_body(
784 pool: &DexPool,
785 class: &PoolClass,
786 ctx: &DexCtx<'_>,
787 opts: &ClassOptions,
788 out: &mut String,
789 depth: usize,
790) -> anyhow::Result<()> {
791 let ind = indent(depth);
792 let cname = crate::apply_class_rename(&class.name);
798 let (_, simple) = split_name(&cname);
799 let simple = if depth == 0 {
804 simple.to_string()
805 } else {
806 let seg = simple.rsplit('$').next().unwrap_or(&simple);
809 if seg.is_empty() {
810 simple.to_string()
811 } else {
812 seg.to_string()
813 }
814 };
815 let simple = if is_restricted_type_name(&simple) {
819 format!("_{simple}")
820 } else {
821 simple
822 };
823 let is_iface = class.is_interface();
824 let is_enum = class.is_enum();
825
826 let mut head = String::new();
827 let a = class.access;
828 if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
829 head.push_str("public ");
830 }
831 if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
835 head.push_str("static ");
836 }
837 if a & ACC_FINAL != 0 && !is_enum {
838 head.push_str("final ");
839 }
840 if a & ACC_ABSTRACT != 0 && !is_iface {
841 head.push_str("abstract ");
842 }
843 if a & ACC_ANNOTATION != 0 {
844 head.push('@');
845 }
846 let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
847 collect_enum_constants(pool, class, ctx)
848 } else {
849 None
850 };
851 if let Some(ecs) = &enum_consts {
852 head.push_str("enum ");
859 head.push_str(&java_ident(&simple));
860 let _ = ecs;
861 } else if is_enum {
862 let abstract_ = a & ACC_ABSTRACT != 0;
866 head.push_str(if abstract_ {
867 "/* enum */ class "
868 } else {
869 "/* enum */ final class "
870 });
871 head.push_str(&java_ident(&simple));
872 } else if is_iface {
873 head.push_str("interface ");
874 head.push_str(&java_ident(&simple));
875 } else {
876 head.push_str("class ");
877 head.push_str(&java_ident(&simple));
878 }
879 let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
883 if is_iface {
884 let is_annot = a & ACC_ANNOTATION != 0;
889 if !class.interfaces.is_empty() && !is_annot {
890 head.push_str(" extends ");
891 head.push_str(&join_dotted(pool, &class.interfaces));
892 }
893 } else {
894 if let Some(sup) = &class.super_name {
895 if sup != "java/lang/Object" && !is_enum {
896 head.push_str(" extends ");
897 head.push_str(&render_super(sup));
898 }
899 }
900 if !class.interfaces.is_empty() {
901 head.push_str(if is_iface {
902 " extends "
903 } else {
904 " implements "
905 });
906 head.push_str(&join_dotted(pool, &class.interfaces));
907 }
908 }
909
910 out.push_str(&ind);
911 out.push_str(&head);
912 out.push_str(" {\n");
913
914 if let Some((ecs, _)) = &enum_consts {
917 for (i, ec) in ecs.iter().enumerate() {
918 out.push_str(&indent(depth + 1));
919 out.push_str(&java_ident(&ec.name));
920 if !ec.extra_args.is_empty() {
921 let mut line = String::from("(");
922 render_enum_args(ctx, pool, &ec.extra_args, &mut line);
923 line.push(')');
924 out.push_str(&line);
925 }
926 if i + 1 < ecs.len() {
927 out.push_str(",\n");
928 } else {
929 out.push_str(";\n");
930 }
931 }
932 out.push('\n');
933 }
934
935 let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
943 (|| {
947 let has_null_final = class.static_fields.iter().enumerate().any(|(i, f)| {
948 f.access & crate::access::ACC_FINAL != 0
949 && matches!(class.static_values.get(i), Some(StaticValue::Null))
950 });
951 if !has_null_final {
952 return None;
953 }
954 let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
955 let dex = pool.dex(m.dex_idx)?;
956 let ci = dex.code_at(m.code_off)?;
957 let mut set: jdc_core::FxHashSet<(
958 std::sync::Arc<str>,
959 std::sync::Arc<str>,
960 )> = jdc_core::FxHashSet::default();
961 for ins in &ci.insns {
962 if let InsnKind::SPut { field_idx, .. } = ins.kind {
963 let fid = dex.field(field_idx);
964 set.insert((
967 std::sync::Arc::from(dex.string(fid.name_idx)),
968 std::sync::Arc::from(dex.type_name(fid.type_idx)),
969 ));
970 }
971 }
972 Some(set)
973 })();
974 let mut field_emitted = false;
975 for (i, f) in class.static_fields.iter().enumerate() {
976 if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
978 continue;
979 }
980 if field_emitted || !class.instance_fields.is_empty() {
981 out.push('\n');
982 }
983 field_emitted = true;
984 let blank_final = f.access & crate::access::ACC_FINAL != 0
986 && matches!(class.static_values.get(i), Some(StaticValue::Null))
987 && clinit_sputs
988 .as_ref()
989 .is_some_and(|s| {
990 s.contains(&(
991 std::sync::Arc::from(f.name.as_str()),
992 std::sync::Arc::from(f.desc.as_str()),
993 ))
994 });
995 emit_field(
996 pool,
997 f,
998 if blank_final {
999 None
1000 } else {
1001 class.static_values.get(i)
1002 },
1003 &class.name,
1004 out,
1005 depth + 1,
1006 true,
1007 class.is_interface(),
1008 );
1009 }
1010 if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1011 out.push('\n');
1012 }
1013 for (i, f) in class.instance_fields.iter().enumerate() {
1014 if i > 0 {
1015 out.push('\n');
1016 }
1017 emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1018 }
1019
1020 fn sig_key(m: &PoolMethod) -> (&str, &str) {
1032 let d: &str = &m.desc;
1033 let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1034 let hi = d.find(')').unwrap_or(d.len());
1035 (&*m.name, &d[lo..hi])
1036 }
1037 let methods: Vec<&PoolMethod> = class.all_methods().collect();
1038 let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1039 for (i, m) in methods.iter().enumerate() {
1040 let key = sig_key(m);
1041 match claim.get(&key) {
1042 Some(&j)
1043 if m.access & crate::access::ACC_BRIDGE == 0
1044 && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1045 {
1046 claim.insert(key, i);
1047 }
1048 None => {
1049 claim.insert(key, i);
1050 }
1051 _ => {}
1052 }
1053 }
1054 let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
1063 for (i, m) in methods.iter().enumerate() {
1064 if &*m.name == "<clinit>" {
1065 continue; }
1067 if claim.get(&sig_key(m)) != Some(&i) {
1068 continue;
1069 }
1070 if let Some((ecs, _)) = &enum_consts {
1074 let d = m.parsed_desc();
1075 let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
1076 let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
1077 let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
1078 let one_str = d
1079 .as_ref()
1080 .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
1081 .unwrap_or(false);
1082 let static_ = m.is_static();
1083 if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
1084 continue;
1085 }
1086 if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
1087 continue;
1088 }
1089 let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
1091 let mut c = (*m).clone();
1092 c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
1093 Some(c)
1094 } else {
1095 None
1096 };
1097 let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
1098 let mark = out.len();
1099 if emitted_any {
1100 out.push('\n');
1101 }
1102 if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
1103 emitted_any = true;
1104 } else {
1105 out.truncate(mark);
1106 }
1107 let _ = ecs;
1108 continue;
1109 }
1110 let mark = out.len();
1111 if emitted_any {
1112 out.push('\n');
1113 }
1114 if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
1115 emitted_any = true;
1116 } else {
1117 out.truncate(mark);
1118 }
1119 }
1120 if !class.is_interface()
1130 && enum_consts.is_none()
1131 && !class.all_methods().any(|m| &*m.name == "<init>")
1132 {
1133 if let Some(mut sup) = class.super_name.clone() {
1134 loop {
1135 if sup == "java/lang/Object" {
1136 break;
1137 }
1138 let Some(sc) = pool.get(&sup) else { break };
1139 let ctors: Vec<&PoolMethod> = sc
1140 .all_methods()
1141 .filter(|m| &*m.name == "<init>")
1142 .collect();
1143 if !ctors.is_empty() {
1144 for sm in ctors {
1145 if sm.access & crate::access::ACC_PRIVATE != 0
1146 || (sm.access
1147 & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
1148 == 0)
1149 {
1150 continue; }
1152 let Some(d) = sm.parsed_desc() else { continue };
1153 let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
1154 "public "
1155 } else {
1156 "protected "
1157 };
1158 if emitted_any {
1159 out.push('\n');
1160 }
1161 out.push_str(&format!(" {}", " ".repeat(depth)));
1162 out.push_str(mods);
1163 out.push_str(&java_ident(&simple));
1164 out.push('(');
1165 let mut names = Vec::with_capacity(d.args.len());
1166 for (i, a) in d.args.iter().enumerate() {
1167 if i > 0 {
1168 out.push_str(", ");
1169 }
1170 out.push_str(&type_name(pool, a));
1171 let nm = format!("p{}", i + 1);
1172 out.push(' ');
1173 out.push_str(&nm);
1174 names.push(nm);
1175 }
1176 out.push_str(") {\n");
1177 out.push_str(&format!(" {} super({});\n", " ".repeat(depth), names.join(", ")));
1178 out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
1179 emitted_any = true;
1180 }
1181 break;
1182 }
1183 match &sc.super_name {
1184 Some(n) => sup = n.clone(),
1185 None => break,
1186 }
1187 }
1188 }
1189 }
1190
1191 let skip_clinit = class.is_interface();
1196 if let Some((_, clinit_body)) = enum_consts.take() {
1197 let empty = match &clinit_body.body {
1201 Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
1202 _ => false,
1203 };
1204 if !empty {
1205 if emitted_any {
1206 out.push('\n');
1207 }
1208 out.push_str(&indent(depth + 1));
1209 out.push_str("static {\n");
1210 let p = Printer::new(ctx, &clinit_body.vt);
1211 let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
1212 for line in text.lines() {
1213 if line.trim().is_empty() {
1214 continue;
1215 }
1216 out.push_str(line);
1217 out.push('\n');
1218 }
1219 out.push_str(&indent(depth + 1));
1220 out.push_str("}\n");
1221 emitted_any = true;
1222 }
1223 } else if let Some(clinit) = (!skip_clinit)
1224 .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
1225 .flatten()
1226 {
1227 let mark = out.len();
1228 if emitted_any {
1229 out.push('\n');
1230 }
1231 if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
1232 emitted_any = true;
1233 } else {
1234 out.truncate(mark);
1235 }
1236 }
1237
1238 for nested in nested_members(pool, class, ctx) {
1241 if emitted_any {
1242 out.push('\n');
1243 }
1244 let nested_ctx = DexCtx::new(pool, nested);
1245 out.push('\n');
1246 emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
1247 emitted_any = true;
1248 }
1249
1250 out.push_str(&ind);
1251 out.push_str("}\n");
1252 Ok(())
1253}
1254
1255fn nested_members<'a>(
1258 pool: &'a DexPool,
1259 class: &'a PoolClass,
1260 ctx: &DexCtx<'_>,
1261) -> Vec<&'a PoolClass> {
1262 let mut out = Vec::new();
1263 for name in pool.children_of(&class.name) {
1264 let Some(pc) = pool.get(name) else { continue };
1265 let Some(rest) = name
1272 .strip_prefix(class.name.as_str())
1273 .and_then(|t| t.strip_prefix('$'))
1274 else {
1275 continue;
1276 };
1277 if rest.is_empty() || rest.starts_with('-') {
1278 continue;
1279 }
1280 if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
1281 continue;
1282 }
1283 let tail = rest.rsplit('$').next().unwrap_or(rest);
1284 if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
1285 continue; }
1287 if tail.starts_with(|c: char| c.is_ascii_digit()) {
1288 continue; }
1290 out.push(pc);
1291 }
1292 out
1293}
1294
1295#[allow(clippy::too_many_arguments)]
1299fn emit_field(
1300 pool: &DexPool,
1301 f: &crate::PoolField,
1302 init: Option<&StaticValue>,
1303 f_class: &str,
1304 out: &mut String,
1305 depth: usize,
1306 is_static: bool,
1307 require_init: bool,
1308) {
1309 let ind = indent(depth);
1310 let mut line = String::new();
1311 let a = f.access;
1312 if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
1313 line.push_str("public ");
1314 } else if a & ACC_PRIVATE != 0 {
1315 line.push_str("private ");
1316 } else if a & ACC_PROTECTED != 0 {
1317 line.push_str("protected ");
1318 }
1319 if is_static {
1320 line.push_str("static ");
1321 }
1322 if a & ACC_FINAL != 0 {
1323 line.push_str("final ");
1324 }
1325 if a & ACC_SYNTHETIC != 0 {
1326 line.push_str("/* synthetic */ ");
1327 }
1328 if a & ACC_TRANSIENT_HINT != 0 {
1329 line.push_str("transient ");
1330 }
1331 if a & ACC_VOLATILE_HINT != 0 {
1332 line.push_str("volatile ");
1333 }
1334 let ty = type_name(pool, &desc_type(&f.desc));
1335 line.push_str(&ty);
1336 line.push(' ');
1337 let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
1338 line.push_str(&java_ident(fname));
1339 let mut rendered = None;
1340 if let Some(v) = init {
1341 rendered = render_static_value(pool, v, f_class);
1342 }
1343 if rendered.is_none() && require_init {
1344 let default = match desc_type(&f.desc) {
1348 JavaType::Boolean => "false",
1349 JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
1350 JavaType::Long => "0L",
1351 JavaType::Float => "0.0F",
1352 JavaType::Double => "0.0",
1353 _ => "null",
1354 };
1355 rendered = Some(default.to_string());
1356 }
1357 if let Some(text) = rendered {
1358 let text = if matches!(desc_type(&f.desc), JavaType::Long)
1362 && text.chars().all(|c| c.is_ascii_digit() || c == '-')
1363 {
1364 format!("{text}L")
1365 } else {
1366 text
1367 };
1368 line.push_str(" = ");
1369 line.push_str(&text);
1370 }
1371 line.push(';');
1372 out.push_str(&ind);
1373 out.push_str(&line);
1374 out.push('\n');
1375}
1376
1377pub const ACC_VOLATILE_HINT: u32 = 0x40;
1380pub const ACC_TRANSIENT_HINT: u32 = 0x80;
1381
1382fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
1383 Some(match v {
1384 StaticValue::Int(i) => i.to_string(),
1385 StaticValue::Float(f) => format_float(*f as f64, true),
1386 StaticValue::Double(d) => format_float(*d, false),
1387 StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
1388 StaticValue::Type(t) => format!("{}.class", dotted(t)),
1389 StaticValue::Boolean(b) => b.to_string(),
1390 StaticValue::Null => "null".into(),
1391 StaticValue::Field(cls, name) => {
1392 let n = java_ident(name);
1393 if cls == owner {
1394 n.into_owned()
1395 } else {
1396 format!("{}.{}", print_class_name(pool, cls), n)
1397 }
1398 }
1399 StaticValue::Other => return None,
1400 })
1401}
1402
1403fn emit_method(
1411 pool: &DexPool,
1412 class: &PoolClass,
1413 ctx: &DexCtx<'_>,
1414 m: &PoolMethod,
1415 depth: usize,
1416 enum_promoted: bool,
1417 out: &mut String,
1418) -> anyhow::Result<bool> {
1419 let ind = indent(depth);
1420 let desc = m.parsed_desc();
1421
1422 let mut sig = String::with_capacity(192);
1424 let a = m.access;
1425 if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
1426 sig.push_str("public ");
1427 } else if a & ACC_PRIVATE != 0 {
1428 sig.push_str("private ");
1429 } else if a & ACC_PROTECTED != 0 {
1430 sig.push_str("protected ");
1431 }
1432 let is_clinit = &*m.name == "<clinit>";
1433 let is_init = &*m.name == "<init>";
1434 if a & ACC_STATIC != 0 || is_clinit {
1435 sig.push_str("static ");
1436 }
1437 if a & ACC_FINAL != 0 {
1438 sig.push_str("final ");
1439 }
1440 if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
1444 sig.push_str("synchronized ");
1445 }
1446 if a & ACC_NATIVE != 0 {
1447 sig.push_str("native ");
1448 }
1449 if a & ACC_ABSTRACT != 0 {
1450 sig.push_str("abstract ");
1451 }
1452 if a & ACC_SYNTHETIC != 0 {
1453 sig.push_str("/* synthetic */ ");
1454 }
1455
1456 let mut body = decompile_method(pool, class, m).ok().flatten();
1458 if class.is_interface()
1465 && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
1466 && body.is_some()
1467 {
1468 sig.insert_str(0, "default ");
1469 }
1470 let param_names: Vec<String> = body
1471 .as_ref()
1472 .map(|b| {
1473 let mut ps: Vec<(u16, String)> =
1474 b.vt.vars
1475 .iter()
1476 .filter(|v| v.is_param && v.name != "this")
1477 .map(|v| (v.slot, v.name.clone()))
1478 .collect();
1479 ps.sort_by_key(|(s, _)| *s);
1480 ps.into_iter().map(|(_, n)| n).collect()
1481 })
1482 .unwrap_or_else(|| {
1483 (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
1484 .map(|i| format!("p{}", i + 1))
1485 .collect()
1486 });
1487
1488 let mut inner_arg0 = 0usize;
1497 if is_init {
1498 let tail = class.name.rsplit('$').next().unwrap_or("");
1499 let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
1500 if let Some(d) = desc.as_ref() {
1501 if let Some(JavaType::Object(outer)) = d.args.first() {
1502 let enclosing: Option<String> = class
1508 .nesting
1509 .enclosing_class
1510 .clone()
1511 .or_else(|| crate::find_outer_name(pool, &class.name));
1512 let direct = enclosing.as_deref() == Some(outer.as_ref());
1513 if direct && !digit_simple && ctx.class_has_this0(&class.name) {
1514 let param0 = body.as_ref().and_then(|b| {
1515 b.vt.vars
1516 .iter()
1517 .find(|v| v.is_param && v.name != "this")
1518 .map(|v| v.id)
1519 });
1520 let written = match (&body, param0) {
1521 (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
1522 _ => false,
1523 };
1524 if let (Some(p0), false) = (param0, written) {
1525 if let Some(b) = body.as_mut() {
1526 let mut eligible: Vec<String> = vec![class.name.clone()];
1531 if let Some(sup) = &class.super_name {
1532 let sup_enclosing =
1533 crate::find_outer_name(pool, sup);
1534 if sup_enclosing.as_deref() == Some(outer.as_ref())
1535 && ctx.class_has_this0(sup)
1536 {
1537 eligible.push(sup.clone());
1538 }
1539 }
1540 let renamed = crate::apply_class_rename(outer);
1541 let display = dotted(renamed.as_ref());
1542 let ty =
1543 jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
1544 crate::passes::rewrite_inner_ctor_outer_param(
1545 &mut b.body,
1546 p0,
1547 &display,
1548 &ty,
1549 &eligible,
1550 );
1551 inner_arg0 = 1;
1552 }
1553 }
1554 }
1555 }
1556 }
1557 }
1558
1559 if is_clinit {
1560 } else {
1562 let Some(d) = &desc else { return Ok(false) };
1563 if is_init {
1564 let dname = crate::apply_class_rename(&class.name);
1569 let (_, simple) = split_name(&dname);
1570 let base = if depth <= 1 {
1574 simple.to_string()
1575 } else {
1576 let seg = simple.rsplit('$').next().unwrap_or(&simple);
1579 if seg.is_empty() {
1580 simple.to_string()
1581 } else {
1582 seg.to_string()
1583 }
1584 };
1585 let name = if is_restricted_type_name(&base) {
1586 format!("_{base}")
1587 } else {
1588 base
1589 };
1590 sig.push_str(&java_ident(&name));
1591 } else {
1592 sig.push_str(&type_name(pool, &d.ret));
1593 sig.push(' ');
1594 let mname =
1595 jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
1596 sig.push_str(&java_ident(mname));
1597 }
1598 sig.push('(');
1599 let mut arg0 = 0;
1609 if enum_promoted
1610 && is_init
1611 && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
1612 && matches!(d.args.get(1), Some(JavaType::Int))
1613 {
1614 let synthetic: Vec<u32> = body
1621 .as_ref()
1622 .map(|b| {
1623 b.vt
1624 .vars
1625 .iter()
1626 .filter(|v| v.is_param && v.name != "this")
1627 .take(2)
1628 .map(|v| v.id)
1629 .collect()
1630 })
1631 .unwrap_or_default();
1632 let is_bridge = matches!(
1639 d.args.last(),
1640 Some(JavaType::Object(m)) if m.as_ref() == "kotlin/jvm/internal/DefaultConstructorMarker"
1641 );
1642 let mut lead = [0usize, 0usize];
1643 let refs_ok = body.as_ref().is_some_and(|b| {
1644 let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
1645 let mut c = b.body.clone();
1646 let cls_name = class.name.as_str();
1647 crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
1648 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
1649 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
1650 for (k, a) in args.iter().enumerate() {
1651 if let Expr::Local { var: v, .. } = a {
1652 if let Some(pi) = synthetic.iter().position(|p| p == v) {
1653 if k == pi && k < 2 {
1654 lead[pi] += 1;
1655 }
1656 }
1657 }
1658 }
1659 }
1660 }
1661 });
1662 synthetic.iter().enumerate().all(|(pi, id)| {
1663 let total = uses.get(id).copied().unwrap_or(0);
1664 if is_bridge {
1665 total == lead[pi]
1666 } else {
1667 total == 0
1668 }
1669 })
1670 });
1671 if refs_ok {
1672 arg0 = 2;
1673 if let (Some(b), true) = (body.as_mut(), is_bridge) {
1676 let cls_name = class.name.as_str();
1677 let syn = synthetic;
1678 crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
1679 if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
1680 if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
1681 let mut drop_n = 0;
1682 for a in args.iter().take(2) {
1683 if let Expr::Local { var: v, .. } = a {
1684 if syn.contains(v) && drop_n == v - syn[0] {
1685 drop_n += 1;
1686 continue;
1687 }
1688 }
1689 break;
1690 }
1691 for _ in 0..drop_n {
1692 args.remove(0);
1693 }
1694 }
1695 }
1696 });
1697 }
1698 }
1699 }
1700 let n = d.args.len();
1701 let varargs = a & ACC_VARARGS != 0 && n > 0;
1702 let arg0 = arg0 + inner_arg0;
1703 for (i, arg) in d.args.iter().enumerate().skip(arg0) {
1704 if i > arg0 {
1705 sig.push_str(", ");
1706 }
1707 let name = param_names
1708 .get(i)
1709 .cloned()
1710 .unwrap_or_else(|| format!("p{}", i));
1711 if varargs && i + 1 == n {
1712 if let JavaType::Array(inner) = arg {
1713 sig.push_str(&type_name(pool, inner));
1714 sig.push_str("...");
1715 } else {
1716 sig.push_str(&type_name(pool, arg));
1717 }
1718 } else {
1719 sig.push_str(&type_name(pool, arg));
1720 }
1721 sig.push(' ');
1722 sig.push_str(&java_ident(&name));
1725 }
1726 sig.push(')');
1727 }
1728
1729 if is_clinit {
1730 let Some(b) = body else { return Ok(false) };
1731 out.push_str(&ind);
1736 out.push_str("static {\n");
1737 let hdr = out.len();
1738 let printer = Printer::new(ctx, &b.vt)
1739 .with_indent(depth + 1)
1740 .with_output(std::mem::take(out));
1741 let t_print = std::time::Instant::now();
1742 let mut rendered = printer.into_string(&b.body);
1743 crate::method::phase_hit(3, t_print);
1744 if rendered[hdr..].trim().is_empty() {
1745 rendered.truncate(hdr);
1746 }
1747 rendered.push_str(&ind);
1748 rendered.push_str("}\n");
1749 *out = rendered;
1750 return Ok(true);
1751 }
1752 if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
1753 out.push_str(&ind);
1754 out.push_str(&sig);
1755 out.push_str(";\n");
1756 return Ok(true);
1757 }
1758 let Some(b) = body else { return Ok(false) };
1759
1760 let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
1762 match &b.desc.ret {
1763 JavaType::Boolean => {
1764 printer = printer.with_ret_bool(true);
1765 }
1766 JavaType::Char => {
1767 printer = printer.with_ret_char(true);
1768 }
1769 JavaType::Byte => {
1770 printer = printer.with_ret_narrow(true, false);
1771 }
1772 JavaType::Short => {
1773 printer = printer.with_ret_narrow(false, true);
1774 }
1775 _ => {}
1776 }
1777 out.push_str(&ind);
1778 out.push_str(&sig);
1779 out.push_str(" {\n");
1780 let hdr = out.len();
1781 let printer = printer.with_output(std::mem::take(out));
1782 let t_print = std::time::Instant::now();
1783 let mut rendered = printer.into_string(&b.body);
1784 crate::method::phase_hit(3, t_print);
1785 if rendered[hdr..].trim().is_empty() {
1786 rendered.truncate(hdr);
1787 }
1788 rendered.push_str(&ind);
1789 rendered.push_str("}\n");
1790 *out = rendered;
1791 Ok(true)
1792}
1793
1794fn indent(depth: usize) -> String {
1799 " ".repeat(depth)
1800}
1801
1802pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1812 static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1815 if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
1816 eprintln!("[ident] {name:?}");
1817 }
1818 let clean = name
1819 .chars()
1820 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
1821 let keyword = matches!(
1825 name,
1826 "abstract"
1827 | "assert"
1828 | "boolean"
1829 | "break"
1830 | "byte"
1831 | "case"
1832 | "catch"
1833 | "char"
1834 | "class"
1835 | "const"
1836 | "continue"
1837 | "default"
1838 | "do"
1839 | "double"
1840 | "else"
1841 | "enum"
1842 | "extends"
1843 | "final"
1844 | "finally"
1845 | "float"
1846 | "for"
1847 | "goto"
1848 | "if"
1849 | "implements"
1850 | "import"
1851 | "instanceof"
1852 | "int"
1853 | "interface"
1854 | "long"
1855 | "native"
1856 | "new"
1857 | "package"
1858 | "private"
1859 | "protected"
1860 | "public"
1861 | "return"
1862 | "short"
1863 | "static"
1864 | "strictfp"
1865 | "super"
1866 | "switch"
1867 | "synchronized"
1868 | "this"
1869 | "throw"
1870 | "throws"
1871 | "transient"
1872 | "try"
1873 | "void"
1874 | "volatile"
1875 | "while"
1876 | "true"
1877 | "false"
1878 | "null"
1879 | "_"
1883 );
1884 let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
1888 if clean && !keyword && !digit_start {
1889 std::borrow::Cow::Borrowed(name)
1890 } else if keyword || digit_start {
1891 std::borrow::Cow::Owned(format!("_{name}"))
1892 } else {
1893 let mapped: String = name
1896 .chars()
1897 .map(|c| {
1898 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1899 c
1900 } else {
1901 '_'
1902 }
1903 })
1904 .collect();
1905 if mapped == "_" {
1906 std::borrow::Cow::Owned("__".to_string())
1907 } else {
1908 std::borrow::Cow::Owned(mapped)
1909 }
1910 }
1911}
1912
1913fn split_name(internal: &str) -> (String, String) {
1914 match internal.rfind('/') {
1915 Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
1916 None => (String::new(), internal.to_string()),
1917 }
1918}
1919
1920pub fn dotted(internal: &str) -> String {
1922 let mut out = internal.replace('/', ".");
1923 let mut i = 0;
1926 while let Some(p) = out[i..].find('$') {
1927 let at = i + p;
1928 let head_ok = at > 0
1931 && out[..at]
1932 .chars()
1933 .next_back()
1934 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
1935 let tail_ok = out[at + 1..]
1936 .chars()
1937 .next()
1938 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1939 if tail_ok && head_ok {
1940 out.replace_range(at..at + 1, ".");
1941 i = at + 1;
1942 } else {
1943 i = at + 1;
1944 }
1945 }
1946 sanitize_fq(&out)
1949}
1950
1951fn join_dotted(pool: &DexPool, names: &[String]) -> String {
1952 names
1953 .iter()
1954 .map(|n| print_class_name(pool, n))
1955 .collect::<Vec<_>>()
1956 .join(", ")
1957}
1958
1959pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
1962 match t {
1963 JavaType::Void => "void".into(),
1964 JavaType::Object(n) => print_class_name(pool, n),
1965 JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
1966 other => other.to_java(false),
1967 }
1968}
1969
1970struct ObscureState {
1986 class: String,
1989 map: jdc_core::FxHashMap<String, String>,
1991 recorded: jdc_core::FxHashSet<String>,
1994 blocked: jdc_core::FxHashSet<String>,
1999}
2000
2001thread_local! {
2002 static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
2003 const { std::cell::RefCell::new(None) };
2004}
2005
2006pub(crate) fn set_obscured_state(
2008 class: String,
2009 map: jdc_core::FxHashMap<String, String>,
2010 blocked: jdc_core::FxHashSet<String>,
2011) {
2012 OBSCURE.with(|m| {
2013 *m.borrow_mut() = Some(ObscureState {
2014 class,
2015 map,
2016 recorded: jdc_core::FxHashSet::default(),
2017 blocked,
2018 });
2019 });
2020}
2021
2022pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
2029 OBSCURE.with(|m| {
2030 let mut out = jdc_core::FxHashSet::default();
2031 let Ok(st) = m.try_borrow() else { return out };
2032 let Some(st) = st.as_ref() else { return out };
2033 for simple in st.map.values() {
2034 out.insert(simple.clone());
2035 }
2036 for internal in &st.recorded {
2037 if let Some(tail) = internal.rsplit(['/', '$']).next() {
2038 if !tail.is_empty() {
2039 out.insert(tail.to_string());
2040 }
2041 }
2042 }
2043 out
2044 })
2045}
2046
2047pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
2049 OBSCURE.with(|m| {
2050 m.borrow_mut()
2051 .take()
2052 .map(|st| st.recorded)
2053 .unwrap_or_default()
2054 })
2055}
2056
2057struct WidenState {
2066 classes: jdc_core::FxHashSet<String>,
2067 methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
2068 fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
2069}
2070
2071static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
2072
2073pub(crate) fn install_access_widening(pool: &crate::DexPool) {
2074 let t0 = std::time::Instant::now();
2075 let raw = crate::refscan::access_widening_scan(pool.dexes());
2076 let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2077 for c in &raw.classes {
2078 if let Some(pc) = pool.get_if_materialized(c) {
2079 if pc.access & crate::access::ACC_PUBLIC == 0 {
2080 classes.insert(c.clone());
2081 }
2082 }
2083 }
2084 let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
2085 jdc_core::FxHashMap::default();
2086 for (owner, set) in &raw.methods {
2087 let Some(pc) = pool.get_if_materialized(owner) else {
2088 continue;
2089 };
2090 let is_enum = pc.access & crate::access::ACC_ENUM != 0;
2091 let mut keep: jdc_core::FxHashSet<(String, String)> =
2092 jdc_core::FxHashSet::default();
2093 for (name, desc) in set {
2094 if name == "<clinit>" || (is_enum && name == "<init>") {
2098 continue;
2099 }
2100 let nonpub = pc
2101 .all_methods()
2102 .any(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str()
2103 && m.access & crate::access::ACC_PUBLIC == 0);
2104 if nonpub {
2105 keep.insert((name.clone(), desc.clone()));
2106 }
2107 }
2108 if !keep.is_empty() {
2109 methods.insert(owner.clone(), keep);
2110 }
2111 }
2112 {
2117 let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
2118 jdc_core::FxHashMap::default();
2119 for name in &pool.order {
2120 let Some(pc) = pool.get_if_materialized(name) else {
2121 continue;
2122 };
2123 if let Some(sup) = &pc.super_name {
2124 subs.entry(sup.clone()).or_default().push(name.clone());
2125 }
2126 for i in &pc.interfaces {
2127 subs.entry(i.clone()).or_default().push(name.clone());
2128 }
2129 }
2130 let seeds: Vec<(String, String, String)> = methods
2131 .iter()
2132 .flat_map(|(o, set)| {
2133 set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
2134 })
2135 .collect();
2136 for (owner, mname, mdesc) in seeds {
2137 let mut queue: Vec<String> = match subs.get(&owner) {
2138 Some(v) => v.clone(),
2139 None => continue,
2140 };
2141 let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2142 while let Some(c) = queue.pop() {
2143 if !seen.insert(c.clone()) {
2144 continue;
2145 }
2146 let Some(pc) = pool.get_if_materialized(&c) else {
2147 continue;
2148 };
2149 let declares_nonpub = pc.all_methods().any(|m| {
2150 &*m.name == mname.as_str()
2151 && &*m.desc == mdesc.as_str()
2152 && m.access & crate::access::ACC_PUBLIC == 0
2153 });
2154 if declares_nonpub {
2155 methods
2156 .entry(c.clone())
2157 .or_default()
2158 .insert((mname.clone(), mdesc.clone()));
2159 }
2160 if let Some(v) = subs.get(&c) {
2161 queue.extend(v.iter().cloned());
2162 }
2163 }
2164 }
2165 }
2166 let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
2167 jdc_core::FxHashMap::default();
2168 for (owner, set) in &raw.fields {
2169 let Some(pc) = pool.get_if_materialized(owner) else {
2170 continue;
2171 };
2172 let keep: jdc_core::FxHashSet<String> = set
2173 .iter()
2174 .filter(|name| {
2175 pc.static_fields
2176 .iter()
2177 .chain(pc.instance_fields.iter())
2178 .any(|f| f.name == name.as_str()
2179 && f.access & crate::access::ACC_PUBLIC == 0)
2180 })
2181 .cloned()
2182 .collect();
2183 if !keep.is_empty() {
2184 fields.insert(owner.clone(), keep);
2185 }
2186 }
2187 if std::env::var("DDC_STATS").is_ok() {
2188 eprintln!(
2189 "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
2190 classes.len(),
2191 methods.len(),
2192 fields.len(),
2193 t0.elapsed()
2194 );
2195 }
2196 let _ = WIDEN.set(WidenState { classes, methods, fields });
2197}
2198
2199#[inline]
2200pub(crate) fn widen_class(internal: &str) -> bool {
2201 WIDEN.get().is_some_and(|w| w.classes.contains(internal))
2202}
2203
2204#[inline]
2205pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
2206 WIDEN.get().is_some_and(|w| {
2207 w.methods
2208 .get(owner)
2209 .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
2210 })
2211}
2212
2213#[inline]
2214pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
2215 WIDEN.get().is_some_and(|w| {
2216 w.fields.get(owner).is_some_and(|s| s.contains(name))
2217 })
2218}
2219
2220pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
2225 OBSCURE.with(|m| {
2226 let mut st = m.borrow_mut();
2227 let st = st.as_mut()?;
2228 if let Some(simple) = st.map.get(internal) {
2229 return Some(simple.clone());
2230 }
2231 let first = internal.split('/').next().unwrap_or("");
2232 let own = st.class.rsplit('/').next().unwrap_or("");
2233 if first == own && internal.split('/').count() >= 2 {
2234 let simple = internal
2238 .rsplit(['/', '$'])
2239 .next()
2240 .unwrap_or("")
2241 .to_string();
2242 if !simple.is_empty() && !st.blocked.contains(&simple) {
2243 st.recorded.insert(internal.to_string());
2244 return Some(simple);
2245 }
2246 }
2247 None
2248 })
2249}
2250
2251fn compute_obscured_renders(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashMap<String, String> {
2256 let own_simple = class.name.rsplit('/').next().unwrap_or("");
2257 if own_simple.is_empty() {
2258 return jdc_core::FxHashMap::default();
2259 }
2260 let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2261 for sup in class.interfaces.iter() {
2262 refs.insert(sup.clone());
2263 }
2264 if let Some(sup) = &class.super_name {
2265 refs.insert(sup.clone());
2266 }
2267 for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
2268 if let JavaType::Object(n) = crate::desc_type(&f.desc) {
2269 refs.insert(n.to_string());
2270 }
2271 }
2272 for m in class.all_methods() {
2273 if let Some(d) = m.parsed_desc() {
2274 for a in d.args.iter() {
2275 if let JavaType::Object(n) = a {
2276 refs.insert(n.to_string());
2277 }
2278 }
2279 if let JavaType::Object(n) = &d.ret {
2280 refs.insert(n.to_string());
2281 }
2282 }
2283 }
2284 let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
2285 for r in refs {
2286 let first = r.split('/').next().unwrap_or("");
2294 if first == own_simple && r.split('/').count() >= 2 && pool.get(&r).is_some() {
2295 if r == class.name || r.starts_with(&format!("{}$", class.name)) {
2301 continue;
2302 }
2303 let display = crate::apply_class_rename(&r);
2304 let simple = display
2305 .rsplit(['/', '$'])
2306 .next()
2307 .unwrap_or("")
2308 .to_string();
2309 if !simple.is_empty() {
2310 out.insert(r.clone(), simple);
2311 }
2312 }
2313 }
2314 out
2315}
2316
2317pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
2318 if let Some(simple) = obscured_render_pub(internal) {
2319 return sanitize_ref(&simple);
2320 }
2321 let cow = crate::apply_class_rename(internal);
2322 let internal: &str = &cow;
2323 if internal.contains('$') && pool.get(internal).is_some() {
2333 if let Some(root) = emission_root(pool, internal) {
2334 if root.contains('$') {
2335 let below = &internal[root.len()..];
2336 if below.is_empty() {
2337 return sanitize_ref(&internal.replace('/', "."));
2339 }
2340 let segs: Vec<&str> = below[1..].split('$').collect();
2341 if segs.iter().any(|s| s.is_empty()) {
2342 return sanitize_ref(&internal.replace('/', "."));
2345 }
2346 let mut out = root.replace('/', ".");
2347 for s in segs {
2348 out.push('.');
2349 out.push_str(s);
2350 }
2351 return sanitize_ref(&out);
2352 }
2353 }
2354 }
2355 let mut out = String::new();
2356 let mut off = 0usize;
2362 loop {
2363 let rest = &internal[off..];
2364 match rest.find('$') {
2365 Some(i) => {
2366 let seg = &rest[..i];
2367 let prefix = &internal[..off + i];
2368 let known = pool.get(prefix).is_some()
2369 || jdc_core::rename::is_renamed_display(prefix)
2370 || !pool.get(internal).is_some();
2377 let tail_ok = rest[i + 1..]
2384 .chars()
2385 .next()
2386 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
2387 out.push_str(&seg.replace('/', "."));
2388 out.push_str(if known && tail_ok { "." } else { "$" });
2389 off += i + 1;
2390 }
2391 None => {
2392 out.push_str(&rest.replace('/', "."));
2393 return sanitize_ref(&out);
2394 }
2395 }
2396 }
2397}
2398
2399fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
2405 let mut cur = internal.to_string();
2406 loop {
2407 let outer = pool.outer_of(&cur)?;
2408 let rest = cur
2409 .strip_prefix(outer)
2410 .and_then(|t| t.strip_prefix('$'))
2411 .unwrap_or("");
2412 if !crate::clean_member_tail(rest) {
2413 return Some(cur);
2414 }
2415 cur = outer.to_string();
2416 }
2417}
2418
2419pub(crate) fn sanitize_fq(dotted: &str) -> String {
2425 dotted
2426 .split('.')
2427 .map(|seg| {
2428 if is_java_keyword_name(seg)
2429 || is_restricted_type_name(seg)
2430 || seg.chars().next().is_some_and(|c| c.is_ascii_digit())
2431 {
2432 format!("_{seg}")
2433 } else if seg
2434 .chars()
2435 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
2436 {
2437 seg.to_string()
2438 } else {
2439 let mapped: String = seg
2442 .chars()
2443 .map(|c| {
2444 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
2445 c
2446 } else {
2447 '_'
2448 }
2449 })
2450 .collect();
2451 if mapped == "_" {
2452 "__".to_string()
2453 } else {
2454 mapped
2455 }
2456 }
2457 })
2458 .collect::<Vec<_>>()
2459 .join(".")
2460}
2461
2462pub(crate) fn is_restricted_type_name(s: &str) -> bool {
2466 matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
2467}
2468
2469fn is_java_keyword_name(s: &str) -> bool {
2470 matches!(
2471 s,
2472 "abstract"
2473 | "assert"
2474 | "boolean"
2475 | "break"
2476 | "byte"
2477 | "case"
2478 | "catch"
2479 | "char"
2480 | "class"
2481 | "const"
2482 | "continue"
2483 | "default"
2484 | "do"
2485 | "double"
2486 | "else"
2487 | "enum"
2488 | "extends"
2489 | "final"
2490 | "finally"
2491 | "float"
2492 | "for"
2493 | "goto"
2494 | "if"
2495 | "implements"
2496 | "import"
2497 | "instanceof"
2498 | "int"
2499 | "interface"
2500 | "long"
2501 | "native"
2502 | "new"
2503 | "package"
2504 | "private"
2505 | "protected"
2506 | "public"
2507 | "return"
2508 | "short"
2509 | "static"
2510 | "strictfp"
2511 | "super"
2512 | "switch"
2513 | "synchronized"
2514 | "this"
2515 | "throw"
2516 | "throws"
2517 | "transient"
2518 | "try"
2519 | "void"
2520 | "volatile"
2521 | "while"
2522 | "true"
2523 | "false"
2524 | "null"
2525 | "_"
2529 )
2530}
2531
2532fn sanitize_ref(name: &str) -> String {
2533 sanitize_fq(name)
2534}
2535
2536pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
2538 print_class_name(pool, internal)
2539}
2540
2541#[allow(dead_code)]
2543fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
2544 let _ = java_type_to_generic;
2545}