use jdc_core::emit::{escape_string, format_float, Printer};
use jdc_core::types::JavaType;
use jdc_core::Ctx;
use crate::access::*;
use crate::PoolField;
use crate::ctx::{java_type_to_generic, DexCtx};
use crate::method::decompile_method;
use crate::{desc_type, DexPool, PoolClass, PoolMethod, StaticValue};
use ddc_dex::insn::InsnKind;
#[derive(Debug, Clone)]
pub struct ClassOptions {
pub provenance: bool,
}
impl Default for ClassOptions {
fn default() -> Self {
ClassOptions { provenance: true }
}
}
pub type PendingMonitor = (
std::sync::mpsc::Receiver<Result<String, String>>,
String,
std::time::Instant,
);
pub fn decompile_class(
pool: &std::sync::Arc<DexPool>,
class: &PoolClass,
opts: &ClassOptions,
pending: &std::sync::Mutex<Vec<PendingMonitor>>,
) -> anyhow::Result<String> {
if class_is_risky(pool, class) {
let pool2 = pool.clone();
let cls = class.clone();
let opts2 = opts.clone();
let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
let name = class.name.clone();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
let _ = std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(move || {
let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
let _ = tx.send(r);
});
pending.lock().unwrap().push((rx, name, deadline));
return Err(anyhow::anyhow!(
"deferred to monitored thread (pathological-CFG guard)"
));
}
decompile_class_impl(pool, class, opts)
}
fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
for m in class.all_methods() {
if m.code_off == 0 {
continue;
}
if let Some(dex) = pool.dex(m.dex_idx) {
if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
if insns > 16_000 {
return true;
}
}
}
}
false
}
fn decompile_class_impl(
pool: &DexPool,
class: &PoolClass,
opts: &ClassOptions,
) -> anyhow::Result<String> {
let ctx = DexCtx::new(pool, class);
let mut out = String::with_capacity(
(class.all_methods().count() * 1024).min(256 * 1024) + 512,
);
if opts.provenance {
static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
out.push_str(BANNER.get_or_init(|| {
format!(
"// Decompiled by https://github.com/ejfkdev/ddc {}\n",
env!("CARGO_PKG_VERSION")
)
}));
if let Some(dex) = pool.dex(class.dex_idx) {
if let Some(label) = pool.dex_labels.get(class.dex_idx) {
out.push_str("// From: ");
out.push_str(label);
out.push_str(" (DEX ");
out.push_str(&dex.version);
out.push_str(")\n");
}
}
}
if let Some(src) = &class.source_file {
out.push_str("// Source file: ");
out.push_str(src);
out.push('\n');
}
if class.is_synthetic() {
out.push_str("// synthetic\n");
}
let (pkg, _) = split_name(&class.name);
let mut obscured_map = compute_obscured_renders(pool, class);
let blocked: jdc_core::FxHashSet<String> = pool
.package_simples()
.get(&pkg)
.cloned()
.unwrap_or_default();
obscured_map.retain(|_internal, simple| !blocked.contains(simple));
set_obscured_state(class.name.clone(), obscured_map.clone(), blocked.clone());
let mut body_buf = String::with_capacity(out.capacity() / 2);
let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
let recorded = take_recorded_and_clear();
body_res?;
if !pkg.is_empty() {
out.push('\n');
out.push_str("package ");
let dotted = pkg.replace('/', ".");
out.push_str(&sanitize_fq(&dotted));
out.push_str(";\n");
let mut import_set: jdc_core::FxHashSet<String> =
obscured_map.keys().cloned().collect();
for r in recorded {
if pool.get(&r).is_some() {
let simple = r.rsplit('/').next().unwrap_or("");
if !blocked.contains(simple) {
import_set.insert(r);
}
}
}
let mut imports: Vec<String> = import_set
.iter()
.map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
.collect();
imports.sort();
for display in imports {
out.push_str(&format!("import {};\n", display));
}
}
out.push('\n');
out.push_str(&body_buf);
Ok(out)
}
use jdc_core::ir::expr::{ConstVal, Expr};
use jdc_core::ir::stmt::Stmt;
struct EnumConst {
#[allow(dead_code)]
field: String,
name: String,
extra_args: Vec<Expr>,
}
fn collect_enum_constants(
pool: &DexPool,
class: &PoolClass,
ctx: &DexCtx<'_>,
) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
let _ = ctx;
let const_fields: Vec<&PoolField> = class
.static_fields
.iter()
.filter(|f| f.access & crate::access::ACC_ENUM != 0)
.collect();
if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
return None;
}
let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
use std::collections::HashMap;
let mut const_name: Vec<String> = Vec::new();
let mut const_field: Vec<String> = Vec::new();
let mut const_extra: Vec<Vec<Expr>> = Vec::new();
let mut const_ord: Vec<i64> = Vec::new();
let mut var_of: HashMap<u32, usize> = HashMap::default(); let mut drop_stmts: Vec<usize> = Vec::new();
if !matches!(&body.body, Stmt::Block(_)) { return None; }
let mut defs: HashMap<u32, &Expr> = HashMap::default();
let mut tainted = false;
let self_name: std::sync::Arc<str> = class.name.as_str().into();
let self_ty =
jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
for (i, st) in match &body.body {
Stmt::Block(v) => v.iter().enumerate(),
_ => return None,
} {
let def_shape: Option<(u32, &Expr)> = match st {
Stmt::LocalDef {
var,
init: Some(e),
..
} => Some((*var, e)),
Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
match (&**target, &**value) {
(Expr::Local { var, .. }, e) => Some((*var, e)),
_ => None,
}
}
_ => None,
};
if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
if ncls.as_ref() == class.name && args.len() >= 2 {
if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
(&args[0], &args[1])
{
if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
var_of.insert(var, const_name.len());
def_site.insert(i, (var, const_name.len()));
const_ord.push(*ord0 as i64);
const_name.push(n.to_string());
const_field.push(String::new());
const_extra.push(resolve_enum_extras(
&args[2..],
&defs,
tainted,
&var_of,
&const_name,
&self_name,
&self_ty,
)?);
drop_stmts.push(i);
}
}
}
track_def(st, &mut defs, &mut tainted);
}
let mut defs2: HashMap<u32, &Expr> = HashMap::default();
let mut tainted2 = false;
let mut cur_def: HashMap<u32, usize> = HashMap::default();
for (i, st) in match &body.body {
Stmt::Block(v) => v.iter().enumerate(),
_ => return None,
} {
if let Some((var, idx)) = def_site.get(&i) {
cur_def.insert(*var, *idx);
}
if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
if let Expr::Field {
cls,
name: fname,
is_static: true,
..
} = &**target
{
if cls.as_ref() != class.name {
continue;
}
if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
continue;
}
if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { { return None; } }
let idx = match &**value {
Expr::Local { var, .. } => cur_def.get(var).copied()?,
Expr::New { cls: ncls, args, .. }
if ncls.as_ref() == class.name && args.len() >= 2 =>
{
if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
(&args[0], &args[1])
{
if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
let idx = const_name.len();
const_ord.push(*ord0 as i64);
const_name.push(n.to_string());
const_field.push(String::new());
const_extra.push(resolve_enum_extras(
&args[2..],
&defs2,
tainted2,
&var_of,
&const_name,
&self_name,
&self_ty,
)?);
idx
} else {
{ return None; }
}
}
_ => return None,
};
const_field[idx] = fname.to_string();
drop_stmts.push(i);
}
}
track_def(st, &mut defs2, &mut tainted2);
}
let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
.iter()
.map(|(k, v)| (*k, (*v).clone()))
.collect();
drop(defs2);
let defs2: std::collections::HashMap<u32, &Expr> =
defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
return None;
}
for i in 0..const_name.len() {
let field = &const_field[i];
if !field.is_empty() && field != &const_name[i] {
let id = java_ident(field);
if id.is_empty() || id.as_ref() != field.as_str() {
{ return None; }
}
const_name[i] = field.clone();
}
}
{
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
if !const_name.iter().all(|c| seen.insert(c.as_str())) { return None; }
}
let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
{
let self_cls: &str = class.name.as_str();
if let Stmt::Block(vs) = &mut body.body {
for st in vs.iter_mut() {
crate::passes::walk_stmt_exprs(st, &mut |e| {
crate::passes::deep_rewrite(e, &mut |x| {
let Expr::NewArray { elem, init: Some(list), .. } = x else {
return;
};
if elem.erased() != JavaType::Object(self_cls.into()) {
return;
}
for slot in list.iter_mut() {
let (n, ord, rest) = match slot {
Expr::New { cls, args, raw: false, .. }
if cls.as_ref() == self_cls && args.len() >= 2 =>
{
match (&args[0], &args[1]) {
(
Expr::Const(ConstVal::Str(sv)),
Expr::Const(ConstVal::Int(o)),
) => (sv.to_string(), *o as i64, args[2..].to_vec()),
_ => continue,
}
}
_ => continue,
};
if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
continue;
}
if const_name.contains(&n)
|| synth.iter().any(|(_, s2, _)| *s2 == n)
{
continue;
}
let Some(extras) = resolve_enum_extras(
&rest,
&defs2,
tainted2,
&var_of,
&const_name,
&self_name,
&self_ty,
) else {
continue;
};
*slot = Expr::Field {
owner: None,
cls: self_name.clone(),
name: std::sync::Arc::from(n.as_str()),
ty: self_ty.clone(),
is_static: true,
};
synth.push((ord, n, extras));
}
});
});
}
}
}
{
let mut cur: HashMap<u32, usize> = HashMap::default();
if let Stmt::Block(vs) = &mut body.body {
for (i, st) in vs.iter_mut().enumerate() {
if let Some((var, idx)) = def_site.get(&i) {
cur.insert(*var, *idx);
}
crate::passes::walk_stmt_exprs(st, &mut |e| {
crate::passes::deep_rewrite(e, &mut |x| {
if let Expr::Local { var, .. } = x {
if let Some(&idx) = cur.get(var) {
let name: std::sync::Arc<str> =
std::sync::Arc::from(const_name[idx].as_str());
*x = Expr::Field {
owner: None,
cls: self_name.clone(),
name,
ty: self_ty.clone(),
is_static: true,
};
}
}
});
});
}
}
}
if let Stmt::Block(vs) = &mut body.body {
let drop_set: std::collections::HashSet<usize> =
drop_stmts.iter().copied().collect();
vs.retain(|_| true);
let mut idx = 0usize;
vs.retain(|_| {
let keep = !drop_set.contains(&idx);
idx += 1;
keep
});
}
crate::passes::drop_dead_locals(&mut body.body);
let mut merged: Vec<(i64, EnumConst)> = const_ord
.into_iter()
.zip(
const_field
.into_iter()
.zip(const_name)
.zip(const_extra)
.map(|((field, name), extra_args)| EnumConst {
field,
name,
extra_args,
}),
)
.collect();
for (ord, name, extra_args) in synth {
merged.push((
ord,
EnumConst {
field: name.clone(),
name,
extra_args,
},
));
}
if merged.is_empty() {
return None; }
merged.sort_by_key(|(o, _)| *o);
let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
let mut member_taken: jdc_core::FxHashSet<String> = merged
.iter()
.map(|(_, c)| c.name.clone())
.chain(
class
.static_fields
.iter()
.chain(class.instance_fields.iter())
.map(|f| f.name.to_string()),
)
.chain(class.all_methods().map(|m| m.name.to_string()))
.collect();
let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
let mut expect: i64 = 0;
let mut pad_k = 0u32;
for (ord, c) in merged {
if ord < expect {
{ return None; } }
if ord > expect {
if has_extras {
{ return None; } }
while expect < ord {
let pname = loop {
let cand = format!("_r{pad_k}");
pad_k += 1;
if !member_taken.contains(&cand) {
member_taken.insert(cand.clone());
break cand;
}
};
padded.push(EnumConst {
field: pname.clone(),
name: pname,
extra_args: Vec::new(),
});
expect += 1;
}
}
expect = ord + 1;
padded.push(c);
}
let out = padded;
Some((out, body))
}
fn track_def<'e>(
st: &'e Stmt,
defs: &mut std::collections::HashMap<u32, &'e Expr>,
tainted: &mut bool,
) {
match st {
Stmt::LocalDef { var, init, .. } => match init {
Some(e) => {
defs.insert(*var, e);
}
None => {
defs.remove(var);
}
},
Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
if let Expr::Local { var, .. } = &**target {
if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
defs.insert(*var, value);
} else {
defs.remove(var);
}
}
}
Stmt::ExprStmt(_) => {}
_ => *tainted = true,
}
}
fn resolve_enum_extras(
extras: &[Expr],
defs: &std::collections::HashMap<u32, &Expr>,
tainted: bool,
var_of: &std::collections::HashMap<u32, usize>,
const_name: &[String],
self_name: &std::sync::Arc<str>,
self_ty: &jdc_core::ir::expr::TypeRef,
) -> Option<Vec<Expr>> {
extras
.iter()
.map(|e| {
let mut out = e.clone();
let mut fail = false;
resolve_locals_in(
&mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
&mut fail,
);
if fail {
None
} else {
Some(out)
}
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn resolve_locals_in(
e: &mut Expr,
defs: &std::collections::HashMap<u32, &Expr>,
tainted: bool,
var_of: &std::collections::HashMap<u32, usize>,
const_name: &[String],
self_name: &std::sync::Arc<str>,
self_ty: &jdc_core::ir::expr::TypeRef,
depth: u32,
fail: &mut bool,
) {
crate::passes::deep_rewrite(e, &mut |x| {
if let Expr::Local { var, .. } = x {
if let Some(&ci) = var_of.get(var) {
if let Some(nm) = const_name.get(ci) {
*x = Expr::Field {
owner: None,
cls: self_name.clone(),
name: std::sync::Arc::from(nm.as_str()),
ty: self_ty.clone(),
is_static: true,
};
return;
}
}
if tainted || depth > 4 {
*fail = true;
return;
}
let Some(d) = defs.get(var).copied() else {
*fail = true;
return;
};
let mut sub = d.clone();
resolve_locals_in(
&mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
depth + 1, fail,
);
if *fail {
return;
}
*x = sub;
}
});
}
fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
let dummy_vt = jdc_core::var::VarTable::default();
let mut p = Printer::new(ctx, &dummy_vt);
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
p.expr(a, 0, out);
}
let _ = pool;
}
#[allow(clippy::only_used_in_recursion)]
fn emit_class_body(
pool: &DexPool,
class: &PoolClass,
ctx: &DexCtx<'_>,
opts: &ClassOptions,
out: &mut String,
depth: usize,
) -> anyhow::Result<()> {
let ind = indent(depth);
let cname = crate::apply_class_rename(&class.name);
let (_, simple) = split_name(&cname);
let simple = if depth == 0 {
simple.to_string()
} else {
let seg = simple.rsplit('$').next().unwrap_or(&simple);
if seg.is_empty() {
simple.to_string()
} else {
seg.to_string()
}
};
let simple = if is_restricted_type_name(&simple) {
format!("_{simple}")
} else {
simple
};
let is_iface = class.is_interface();
let is_enum = class.is_enum();
let mut head = String::new();
let a = class.access;
if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
head.push_str("public ");
}
if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
head.push_str("static ");
}
if a & ACC_FINAL != 0 && !is_enum {
head.push_str("final ");
}
if a & ACC_ABSTRACT != 0 && !is_iface {
head.push_str("abstract ");
}
if a & ACC_ANNOTATION != 0 {
head.push('@');
}
let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
collect_enum_constants(pool, class, ctx)
} else {
None
};
if let Some(ecs) = &enum_consts {
head.push_str("enum ");
head.push_str(&java_ident(&simple));
let _ = ecs;
} else if is_enum {
let abstract_ = a & ACC_ABSTRACT != 0;
head.push_str(if abstract_ {
"/* enum */ class "
} else {
"/* enum */ final class "
});
head.push_str(&java_ident(&simple));
} else if is_iface {
head.push_str("interface ");
head.push_str(&java_ident(&simple));
} else {
head.push_str("class ");
head.push_str(&java_ident(&simple));
}
let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
if is_iface {
let is_annot = a & ACC_ANNOTATION != 0;
if !class.interfaces.is_empty() && !is_annot {
head.push_str(" extends ");
head.push_str(&join_dotted(pool, &class.interfaces));
}
} else {
if let Some(sup) = &class.super_name {
if sup != "java/lang/Object" && !is_enum {
head.push_str(" extends ");
head.push_str(&render_super(sup));
}
}
if !class.interfaces.is_empty() {
head.push_str(if is_iface {
" extends "
} else {
" implements "
});
head.push_str(&join_dotted(pool, &class.interfaces));
}
}
out.push_str(&ind);
out.push_str(&head);
out.push_str(" {\n");
if let Some((ecs, _)) = &enum_consts {
for (i, ec) in ecs.iter().enumerate() {
out.push_str(&indent(depth + 1));
out.push_str(&java_ident(&ec.name));
if !ec.extra_args.is_empty() {
let mut line = String::from("(");
render_enum_args(ctx, pool, &ec.extra_args, &mut line);
line.push(')');
out.push_str(&line);
}
if i + 1 < ecs.len() {
out.push_str(",\n");
} else {
out.push_str(";\n");
}
}
out.push('\n');
}
let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
(|| {
let has_null_final = class.static_fields.iter().enumerate().any(|(i, f)| {
f.access & crate::access::ACC_FINAL != 0
&& matches!(class.static_values.get(i), Some(StaticValue::Null))
});
if !has_null_final {
return None;
}
let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
let dex = pool.dex(m.dex_idx)?;
let ci = dex.code_at(m.code_off)?;
let mut set: jdc_core::FxHashSet<(
std::sync::Arc<str>,
std::sync::Arc<str>,
)> = jdc_core::FxHashSet::default();
for ins in &ci.insns {
if let InsnKind::SPut { field_idx, .. } = ins.kind {
let fid = dex.field(field_idx);
set.insert((
std::sync::Arc::from(dex.string(fid.name_idx)),
std::sync::Arc::from(dex.type_name(fid.type_idx)),
));
}
}
Some(set)
})();
let mut field_emitted = false;
for (i, f) in class.static_fields.iter().enumerate() {
if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
continue;
}
if field_emitted || !class.instance_fields.is_empty() {
out.push('\n');
}
field_emitted = true;
let blank_final = f.access & crate::access::ACC_FINAL != 0
&& matches!(class.static_values.get(i), Some(StaticValue::Null))
&& clinit_sputs
.as_ref()
.is_some_and(|s| {
s.contains(&(
std::sync::Arc::from(f.name.as_str()),
std::sync::Arc::from(f.desc.as_str()),
))
});
emit_field(
pool,
f,
if blank_final {
None
} else {
class.static_values.get(i)
},
&class.name,
out,
depth + 1,
true,
class.is_interface(),
);
}
if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
out.push('\n');
}
for (i, f) in class.instance_fields.iter().enumerate() {
if i > 0 {
out.push('\n');
}
emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
}
fn sig_key(m: &PoolMethod) -> (&str, &str) {
let d: &str = &m.desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
(&*m.name, &d[lo..hi])
}
let methods: Vec<&PoolMethod> = class.all_methods().collect();
let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
for (i, m) in methods.iter().enumerate() {
let key = sig_key(m);
match claim.get(&key) {
Some(&j)
if m.access & crate::access::ACC_BRIDGE == 0
&& methods[j].access & crate::access::ACC_BRIDGE != 0 =>
{
claim.insert(key, i);
}
None => {
claim.insert(key, i);
}
_ => {}
}
}
let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
for (i, m) in methods.iter().enumerate() {
if &*m.name == "<clinit>" {
continue; }
if claim.get(&sig_key(m)) != Some(&i) {
continue;
}
if let Some((ecs, _)) = &enum_consts {
let d = m.parsed_desc();
let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
let one_str = d
.as_ref()
.map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
.unwrap_or(false);
let static_ = m.is_static();
if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
continue;
}
if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
continue;
}
let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
let mut c = (*m).clone();
c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
Some(c)
} else {
None
};
let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
let mark = out.len();
if emitted_any {
out.push('\n');
}
if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
emitted_any = true;
} else {
out.truncate(mark);
}
let _ = ecs;
continue;
}
let mark = out.len();
if emitted_any {
out.push('\n');
}
if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
emitted_any = true;
} else {
out.truncate(mark);
}
}
if !class.is_interface()
&& enum_consts.is_none()
&& !class.all_methods().any(|m| &*m.name == "<init>")
{
if let Some(mut sup) = class.super_name.clone() {
loop {
if sup == "java/lang/Object" {
break;
}
let Some(sc) = pool.get(&sup) else { break };
let ctors: Vec<&PoolMethod> = sc
.all_methods()
.filter(|m| &*m.name == "<init>")
.collect();
if !ctors.is_empty() {
for sm in ctors {
if sm.access & crate::access::ACC_PRIVATE != 0
|| (sm.access
& (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
== 0)
{
continue; }
let Some(d) = sm.parsed_desc() else { continue };
let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
"public "
} else {
"protected "
};
if emitted_any {
out.push('\n');
}
out.push_str(&format!(" {}", " ".repeat(depth)));
out.push_str(mods);
out.push_str(&java_ident(&simple));
out.push('(');
let mut names = Vec::with_capacity(d.args.len());
for (i, a) in d.args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&type_name(pool, a));
let nm = format!("p{}", i + 1);
out.push(' ');
out.push_str(&nm);
names.push(nm);
}
out.push_str(") {\n");
out.push_str(&format!(" {} super({});\n", " ".repeat(depth), names.join(", ")));
out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
emitted_any = true;
}
break;
}
match &sc.super_name {
Some(n) => sup = n.clone(),
None => break,
}
}
}
}
let skip_clinit = class.is_interface();
if let Some((_, clinit_body)) = enum_consts.take() {
let empty = match &clinit_body.body {
Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
_ => false,
};
if !empty {
if emitted_any {
out.push('\n');
}
out.push_str(&indent(depth + 1));
out.push_str("static {\n");
let p = Printer::new(ctx, &clinit_body.vt);
let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
for line in text.lines() {
if line.trim().is_empty() {
continue;
}
out.push_str(line);
out.push('\n');
}
out.push_str(&indent(depth + 1));
out.push_str("}\n");
emitted_any = true;
}
} else if let Some(clinit) = (!skip_clinit)
.then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
.flatten()
{
let mark = out.len();
if emitted_any {
out.push('\n');
}
if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
emitted_any = true;
} else {
out.truncate(mark);
}
}
for nested in nested_members(pool, class, ctx) {
if emitted_any {
out.push('\n');
}
let nested_ctx = DexCtx::new(pool, nested);
out.push('\n');
emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
emitted_any = true;
}
out.push_str(&ind);
out.push_str("}\n");
Ok(())
}
fn nested_members<'a>(
pool: &'a DexPool,
class: &'a PoolClass,
ctx: &DexCtx<'_>,
) -> Vec<&'a PoolClass> {
let mut out = Vec::new();
for name in pool.children_of(&class.name) {
let Some(pc) = pool.get(name) else { continue };
let Some(rest) = name
.strip_prefix(class.name.as_str())
.and_then(|t| t.strip_prefix('$'))
else {
continue;
};
if rest.is_empty() || rest.starts_with('-') {
continue;
}
if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
continue;
}
let tail = rest.rsplit('$').next().unwrap_or(rest);
if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
continue; }
if tail.starts_with(|c: char| c.is_ascii_digit()) {
continue; }
out.push(pc);
}
out
}
#[allow(clippy::too_many_arguments)]
fn emit_field(
pool: &DexPool,
f: &crate::PoolField,
init: Option<&StaticValue>,
f_class: &str,
out: &mut String,
depth: usize,
is_static: bool,
require_init: bool,
) {
let ind = indent(depth);
let mut line = String::new();
let a = f.access;
if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
line.push_str("public ");
} else if a & ACC_PRIVATE != 0 {
line.push_str("private ");
} else if a & ACC_PROTECTED != 0 {
line.push_str("protected ");
}
if is_static {
line.push_str("static ");
}
if a & ACC_FINAL != 0 {
line.push_str("final ");
}
if a & ACC_SYNTHETIC != 0 {
line.push_str("/* synthetic */ ");
}
if a & ACC_TRANSIENT_HINT != 0 {
line.push_str("transient ");
}
if a & ACC_VOLATILE_HINT != 0 {
line.push_str("volatile ");
}
let ty = type_name(pool, &desc_type(&f.desc));
line.push_str(&ty);
line.push(' ');
let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
line.push_str(&java_ident(fname));
let mut rendered = None;
if let Some(v) = init {
rendered = render_static_value(pool, v, f_class);
}
if rendered.is_none() && require_init {
let default = match desc_type(&f.desc) {
JavaType::Boolean => "false",
JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
JavaType::Long => "0L",
JavaType::Float => "0.0F",
JavaType::Double => "0.0",
_ => "null",
};
rendered = Some(default.to_string());
}
if let Some(text) = rendered {
let text = if matches!(desc_type(&f.desc), JavaType::Long)
&& text.chars().all(|c| c.is_ascii_digit() || c == '-')
{
format!("{text}L")
} else {
text
};
line.push_str(" = ");
line.push_str(&text);
}
line.push(';');
out.push_str(&ind);
out.push_str(&line);
out.push('\n');
}
pub const ACC_VOLATILE_HINT: u32 = 0x40;
pub const ACC_TRANSIENT_HINT: u32 = 0x80;
fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
Some(match v {
StaticValue::Int(i) => i.to_string(),
StaticValue::Float(f) => format_float(*f as f64, true),
StaticValue::Double(d) => format_float(*d, false),
StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
StaticValue::Type(t) => format!("{}.class", dotted(t)),
StaticValue::Boolean(b) => b.to_string(),
StaticValue::Null => "null".into(),
StaticValue::Field(cls, name) => {
let n = java_ident(name);
if cls == owner {
n.into_owned()
} else {
format!("{}.{}", print_class_name(pool, cls), n)
}
}
StaticValue::Other => return None,
})
}
fn emit_method(
pool: &DexPool,
class: &PoolClass,
ctx: &DexCtx<'_>,
m: &PoolMethod,
depth: usize,
enum_promoted: bool,
out: &mut String,
) -> anyhow::Result<bool> {
let ind = indent(depth);
let desc = m.parsed_desc();
let mut sig = String::with_capacity(192);
let a = m.access;
if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
sig.push_str("public ");
} else if a & ACC_PRIVATE != 0 {
sig.push_str("private ");
} else if a & ACC_PROTECTED != 0 {
sig.push_str("protected ");
}
let is_clinit = &*m.name == "<clinit>";
let is_init = &*m.name == "<init>";
if a & ACC_STATIC != 0 || is_clinit {
sig.push_str("static ");
}
if a & ACC_FINAL != 0 {
sig.push_str("final ");
}
if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
sig.push_str("synchronized ");
}
if a & ACC_NATIVE != 0 {
sig.push_str("native ");
}
if a & ACC_ABSTRACT != 0 {
sig.push_str("abstract ");
}
if a & ACC_SYNTHETIC != 0 {
sig.push_str("/* synthetic */ ");
}
let mut body = decompile_method(pool, class, m).ok().flatten();
if class.is_interface()
&& a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
&& body.is_some()
{
sig.insert_str(0, "default ");
}
let param_names: Vec<String> = body
.as_ref()
.map(|b| {
let mut ps: Vec<(u16, String)> =
b.vt.vars
.iter()
.filter(|v| v.is_param && v.name != "this")
.map(|v| (v.slot, v.name.clone()))
.collect();
ps.sort_by_key(|(s, _)| *s);
ps.into_iter().map(|(_, n)| n).collect()
})
.unwrap_or_else(|| {
(0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
.map(|i| format!("p{}", i + 1))
.collect()
});
let mut inner_arg0 = 0usize;
if is_init {
let tail = class.name.rsplit('$').next().unwrap_or("");
let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
if let Some(d) = desc.as_ref() {
if let Some(JavaType::Object(outer)) = d.args.first() {
let enclosing: Option<String> = class
.nesting
.enclosing_class
.clone()
.or_else(|| crate::find_outer_name(pool, &class.name));
let direct = enclosing.as_deref() == Some(outer.as_ref());
if direct && !digit_simple && ctx.class_has_this0(&class.name) {
let param0 = body.as_ref().and_then(|b| {
b.vt.vars
.iter()
.find(|v| v.is_param && v.name != "this")
.map(|v| v.id)
});
let written = match (&body, param0) {
(Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
_ => false,
};
if let (Some(p0), false) = (param0, written) {
if let Some(b) = body.as_mut() {
let mut eligible: Vec<String> = vec![class.name.clone()];
if let Some(sup) = &class.super_name {
let sup_enclosing =
crate::find_outer_name(pool, sup);
if sup_enclosing.as_deref() == Some(outer.as_ref())
&& ctx.class_has_this0(sup)
{
eligible.push(sup.clone());
}
}
let renamed = crate::apply_class_rename(outer);
let display = dotted(renamed.as_ref());
let ty =
jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
crate::passes::rewrite_inner_ctor_outer_param(
&mut b.body,
p0,
&display,
&ty,
&eligible,
);
inner_arg0 = 1;
}
}
}
}
}
}
if is_clinit {
} else {
let Some(d) = &desc else { return Ok(false) };
if is_init {
let dname = crate::apply_class_rename(&class.name);
let (_, simple) = split_name(&dname);
let base = if depth <= 1 {
simple.to_string()
} else {
let seg = simple.rsplit('$').next().unwrap_or(&simple);
if seg.is_empty() {
simple.to_string()
} else {
seg.to_string()
}
};
let name = if is_restricted_type_name(&base) {
format!("_{base}")
} else {
base
};
sig.push_str(&java_ident(&name));
} else {
sig.push_str(&type_name(pool, &d.ret));
sig.push(' ');
let mname =
jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
sig.push_str(&java_ident(mname));
}
sig.push('(');
let mut arg0 = 0;
if enum_promoted
&& is_init
&& matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
&& matches!(d.args.get(1), Some(JavaType::Int))
{
let synthetic: Vec<u32> = body
.as_ref()
.map(|b| {
b.vt
.vars
.iter()
.filter(|v| v.is_param && v.name != "this")
.take(2)
.map(|v| v.id)
.collect()
})
.unwrap_or_default();
let is_bridge = matches!(
d.args.last(),
Some(JavaType::Object(m)) if m.as_ref() == "kotlin/jvm/internal/DefaultConstructorMarker"
);
let mut lead = [0usize, 0usize];
let refs_ok = body.as_ref().is_some_and(|b| {
let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
let mut c = b.body.clone();
let cls_name = class.name.as_str();
crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
for (k, a) in args.iter().enumerate() {
if let Expr::Local { var: v, .. } = a {
if let Some(pi) = synthetic.iter().position(|p| p == v) {
if k == pi && k < 2 {
lead[pi] += 1;
}
}
}
}
}
}
});
synthetic.iter().enumerate().all(|(pi, id)| {
let total = uses.get(id).copied().unwrap_or(0);
if is_bridge {
total == lead[pi]
} else {
total == 0
}
})
});
if refs_ok {
arg0 = 2;
if let (Some(b), true) = (body.as_mut(), is_bridge) {
let cls_name = class.name.as_str();
let syn = synthetic;
crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
let mut drop_n = 0;
for a in args.iter().take(2) {
if let Expr::Local { var: v, .. } = a {
if syn.contains(v) && drop_n == v - syn[0] {
drop_n += 1;
continue;
}
}
break;
}
for _ in 0..drop_n {
args.remove(0);
}
}
}
});
}
}
}
let n = d.args.len();
let varargs = a & ACC_VARARGS != 0 && n > 0;
let arg0 = arg0 + inner_arg0;
for (i, arg) in d.args.iter().enumerate().skip(arg0) {
if i > arg0 {
sig.push_str(", ");
}
let name = param_names
.get(i)
.cloned()
.unwrap_or_else(|| format!("p{}", i));
if varargs && i + 1 == n {
if let JavaType::Array(inner) = arg {
sig.push_str(&type_name(pool, inner));
sig.push_str("...");
} else {
sig.push_str(&type_name(pool, arg));
}
} else {
sig.push_str(&type_name(pool, arg));
}
sig.push(' ');
sig.push_str(&java_ident(&name));
}
sig.push(')');
}
if is_clinit {
let Some(b) = body else { return Ok(false) };
out.push_str(&ind);
out.push_str("static {\n");
let hdr = out.len();
let printer = Printer::new(ctx, &b.vt)
.with_indent(depth + 1)
.with_output(std::mem::take(out));
let t_print = std::time::Instant::now();
let mut rendered = printer.into_string(&b.body);
crate::method::phase_hit(3, t_print);
if rendered[hdr..].trim().is_empty() {
rendered.truncate(hdr);
}
rendered.push_str(&ind);
rendered.push_str("}\n");
*out = rendered;
return Ok(true);
}
if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
out.push_str(&ind);
out.push_str(&sig);
out.push_str(";\n");
return Ok(true);
}
let Some(b) = body else { return Ok(false) };
let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
match &b.desc.ret {
JavaType::Boolean => {
printer = printer.with_ret_bool(true);
}
JavaType::Char => {
printer = printer.with_ret_char(true);
}
JavaType::Byte => {
printer = printer.with_ret_narrow(true, false);
}
JavaType::Short => {
printer = printer.with_ret_narrow(false, true);
}
_ => {}
}
out.push_str(&ind);
out.push_str(&sig);
out.push_str(" {\n");
let hdr = out.len();
let printer = printer.with_output(std::mem::take(out));
let t_print = std::time::Instant::now();
let mut rendered = printer.into_string(&b.body);
crate::method::phase_hit(3, t_print);
if rendered[hdr..].trim().is_empty() {
rendered.truncate(hdr);
}
rendered.push_str(&ind);
rendered.push_str("}\n");
*out = rendered;
Ok(true)
}
fn indent(depth: usize) -> String {
" ".repeat(depth)
}
pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
eprintln!("[ident] {name:?}");
}
let clean = name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
let keyword = matches!(
name,
"abstract"
| "assert"
| "boolean"
| "break"
| "byte"
| "case"
| "catch"
| "char"
| "class"
| "const"
| "continue"
| "default"
| "do"
| "double"
| "else"
| "enum"
| "extends"
| "final"
| "finally"
| "float"
| "for"
| "goto"
| "if"
| "implements"
| "import"
| "instanceof"
| "int"
| "interface"
| "long"
| "native"
| "new"
| "package"
| "private"
| "protected"
| "public"
| "return"
| "short"
| "static"
| "strictfp"
| "super"
| "switch"
| "synchronized"
| "this"
| "throw"
| "throws"
| "transient"
| "try"
| "void"
| "volatile"
| "while"
| "true"
| "false"
| "null"
| "_"
);
let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
if clean && !keyword && !digit_start {
std::borrow::Cow::Borrowed(name)
} else if keyword || digit_start {
std::borrow::Cow::Owned(format!("_{name}"))
} else {
let mapped: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
c
} else {
'_'
}
})
.collect();
if mapped == "_" {
std::borrow::Cow::Owned("__".to_string())
} else {
std::borrow::Cow::Owned(mapped)
}
}
}
fn split_name(internal: &str) -> (String, String) {
match internal.rfind('/') {
Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
None => (String::new(), internal.to_string()),
}
}
pub fn dotted(internal: &str) -> String {
let mut out = internal.replace('/', ".");
let mut i = 0;
while let Some(p) = out[i..].find('$') {
let at = i + p;
let head_ok = at > 0
&& out[..at]
.chars()
.next_back()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
let tail_ok = out[at + 1..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
if tail_ok && head_ok {
out.replace_range(at..at + 1, ".");
i = at + 1;
} else {
i = at + 1;
}
}
sanitize_fq(&out)
}
fn join_dotted(pool: &DexPool, names: &[String]) -> String {
names
.iter()
.map(|n| print_class_name(pool, n))
.collect::<Vec<_>>()
.join(", ")
}
pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
match t {
JavaType::Void => "void".into(),
JavaType::Object(n) => print_class_name(pool, n),
JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
other => other.to_java(false),
}
}
struct ObscureState {
class: String,
map: jdc_core::FxHashMap<String, String>,
recorded: jdc_core::FxHashSet<String>,
blocked: jdc_core::FxHashSet<String>,
}
thread_local! {
static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) fn set_obscured_state(
class: String,
map: jdc_core::FxHashMap<String, String>,
blocked: jdc_core::FxHashSet<String>,
) {
OBSCURE.with(|m| {
*m.borrow_mut() = Some(ObscureState {
class,
map,
recorded: jdc_core::FxHashSet::default(),
blocked,
});
});
}
pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
OBSCURE.with(|m| {
let mut out = jdc_core::FxHashSet::default();
let Ok(st) = m.try_borrow() else { return out };
let Some(st) = st.as_ref() else { return out };
for simple in st.map.values() {
out.insert(simple.clone());
}
for internal in &st.recorded {
if let Some(tail) = internal.rsplit(['/', '$']).next() {
if !tail.is_empty() {
out.insert(tail.to_string());
}
}
}
out
})
}
pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
OBSCURE.with(|m| {
m.borrow_mut()
.take()
.map(|st| st.recorded)
.unwrap_or_default()
})
}
struct WidenState {
classes: jdc_core::FxHashSet<String>,
methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
}
static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
pub(crate) fn install_access_widening(pool: &crate::DexPool) {
let t0 = std::time::Instant::now();
let raw = crate::refscan::access_widening_scan(pool.dexes());
let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
for c in &raw.classes {
if let Some(pc) = pool.get_if_materialized(c) {
if pc.access & crate::access::ACC_PUBLIC == 0 {
classes.insert(c.clone());
}
}
}
let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
jdc_core::FxHashMap::default();
for (owner, set) in &raw.methods {
let Some(pc) = pool.get_if_materialized(owner) else {
continue;
};
let is_enum = pc.access & crate::access::ACC_ENUM != 0;
let mut keep: jdc_core::FxHashSet<(String, String)> =
jdc_core::FxHashSet::default();
for (name, desc) in set {
if name == "<clinit>" || (is_enum && name == "<init>") {
continue;
}
let nonpub = pc
.all_methods()
.any(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str()
&& m.access & crate::access::ACC_PUBLIC == 0);
if nonpub {
keep.insert((name.clone(), desc.clone()));
}
}
if !keep.is_empty() {
methods.insert(owner.clone(), keep);
}
}
{
let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
jdc_core::FxHashMap::default();
for name in &pool.order {
let Some(pc) = pool.get_if_materialized(name) else {
continue;
};
if let Some(sup) = &pc.super_name {
subs.entry(sup.clone()).or_default().push(name.clone());
}
for i in &pc.interfaces {
subs.entry(i.clone()).or_default().push(name.clone());
}
}
let seeds: Vec<(String, String, String)> = methods
.iter()
.flat_map(|(o, set)| {
set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
})
.collect();
for (owner, mname, mdesc) in seeds {
let mut queue: Vec<String> = match subs.get(&owner) {
Some(v) => v.clone(),
None => continue,
};
let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
while let Some(c) = queue.pop() {
if !seen.insert(c.clone()) {
continue;
}
let Some(pc) = pool.get_if_materialized(&c) else {
continue;
};
let declares_nonpub = pc.all_methods().any(|m| {
&*m.name == mname.as_str()
&& &*m.desc == mdesc.as_str()
&& m.access & crate::access::ACC_PUBLIC == 0
});
if declares_nonpub {
methods
.entry(c.clone())
.or_default()
.insert((mname.clone(), mdesc.clone()));
}
if let Some(v) = subs.get(&c) {
queue.extend(v.iter().cloned());
}
}
}
}
let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
for (owner, set) in &raw.fields {
let Some(pc) = pool.get_if_materialized(owner) else {
continue;
};
let keep: jdc_core::FxHashSet<String> = set
.iter()
.filter(|name| {
pc.static_fields
.iter()
.chain(pc.instance_fields.iter())
.any(|f| f.name == name.as_str()
&& f.access & crate::access::ACC_PUBLIC == 0)
})
.cloned()
.collect();
if !keep.is_empty() {
fields.insert(owner.clone(), keep);
}
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
classes.len(),
methods.len(),
fields.len(),
t0.elapsed()
);
}
let _ = WIDEN.set(WidenState { classes, methods, fields });
}
#[inline]
pub(crate) fn widen_class(internal: &str) -> bool {
WIDEN.get().is_some_and(|w| w.classes.contains(internal))
}
#[inline]
pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
WIDEN.get().is_some_and(|w| {
w.methods
.get(owner)
.is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
})
}
#[inline]
pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
WIDEN.get().is_some_and(|w| {
w.fields.get(owner).is_some_and(|s| s.contains(name))
})
}
pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
OBSCURE.with(|m| {
let mut st = m.borrow_mut();
let st = st.as_mut()?;
if let Some(simple) = st.map.get(internal) {
return Some(simple.clone());
}
let first = internal.split('/').next().unwrap_or("");
let own = st.class.rsplit('/').next().unwrap_or("");
if first == own && internal.split('/').count() >= 2 {
let simple = internal
.rsplit(['/', '$'])
.next()
.unwrap_or("")
.to_string();
if !simple.is_empty() && !st.blocked.contains(&simple) {
st.recorded.insert(internal.to_string());
return Some(simple);
}
}
None
})
}
fn compute_obscured_renders(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashMap<String, String> {
let own_simple = class.name.rsplit('/').next().unwrap_or("");
if own_simple.is_empty() {
return jdc_core::FxHashMap::default();
}
let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
for sup in class.interfaces.iter() {
refs.insert(sup.clone());
}
if let Some(sup) = &class.super_name {
refs.insert(sup.clone());
}
for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
if let JavaType::Object(n) = crate::desc_type(&f.desc) {
refs.insert(n.to_string());
}
}
for m in class.all_methods() {
if let Some(d) = m.parsed_desc() {
for a in d.args.iter() {
if let JavaType::Object(n) = a {
refs.insert(n.to_string());
}
}
if let JavaType::Object(n) = &d.ret {
refs.insert(n.to_string());
}
}
}
let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
for r in refs {
let first = r.split('/').next().unwrap_or("");
if first == own_simple && r.split('/').count() >= 2 && pool.get(&r).is_some() {
if r == class.name || r.starts_with(&format!("{}$", class.name)) {
continue;
}
let display = crate::apply_class_rename(&r);
let simple = display
.rsplit(['/', '$'])
.next()
.unwrap_or("")
.to_string();
if !simple.is_empty() {
out.insert(r.clone(), simple);
}
}
}
out
}
pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
if let Some(simple) = obscured_render_pub(internal) {
return sanitize_ref(&simple);
}
let cow = crate::apply_class_rename(internal);
let internal: &str = &cow;
if internal.contains('$') && pool.get(internal).is_some() {
if let Some(root) = emission_root(pool, internal) {
if root.contains('$') {
let below = &internal[root.len()..];
if below.is_empty() {
return sanitize_ref(&internal.replace('/', "."));
}
let segs: Vec<&str> = below[1..].split('$').collect();
if segs.iter().any(|s| s.is_empty()) {
return sanitize_ref(&internal.replace('/', "."));
}
let mut out = root.replace('/', ".");
for s in segs {
out.push('.');
out.push_str(s);
}
return sanitize_ref(&out);
}
}
}
let mut out = String::new();
let mut off = 0usize;
loop {
let rest = &internal[off..];
match rest.find('$') {
Some(i) => {
let seg = &rest[..i];
let prefix = &internal[..off + i];
let known = pool.get(prefix).is_some()
|| jdc_core::rename::is_renamed_display(prefix)
|| !pool.get(internal).is_some();
let tail_ok = rest[i + 1..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
out.push_str(&seg.replace('/', "."));
out.push_str(if known && tail_ok { "." } else { "$" });
off += i + 1;
}
None => {
out.push_str(&rest.replace('/', "."));
return sanitize_ref(&out);
}
}
}
}
fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
let mut cur = internal.to_string();
loop {
let outer = pool.outer_of(&cur)?;
let rest = cur
.strip_prefix(outer)
.and_then(|t| t.strip_prefix('$'))
.unwrap_or("");
if !crate::clean_member_tail(rest) {
return Some(cur);
}
cur = outer.to_string();
}
}
pub(crate) fn sanitize_fq(dotted: &str) -> String {
dotted
.split('.')
.map(|seg| {
if is_java_keyword_name(seg)
|| is_restricted_type_name(seg)
|| seg.chars().next().is_some_and(|c| c.is_ascii_digit())
{
format!("_{seg}")
} else if seg
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
{
seg.to_string()
} else {
let mapped: String = seg
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
c
} else {
'_'
}
})
.collect();
if mapped == "_" {
"__".to_string()
} else {
mapped
}
}
})
.collect::<Vec<_>>()
.join(".")
}
pub(crate) fn is_restricted_type_name(s: &str) -> bool {
matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
}
fn is_java_keyword_name(s: &str) -> bool {
matches!(
s,
"abstract"
| "assert"
| "boolean"
| "break"
| "byte"
| "case"
| "catch"
| "char"
| "class"
| "const"
| "continue"
| "default"
| "do"
| "double"
| "else"
| "enum"
| "extends"
| "final"
| "finally"
| "float"
| "for"
| "goto"
| "if"
| "implements"
| "import"
| "instanceof"
| "int"
| "interface"
| "long"
| "native"
| "new"
| "package"
| "private"
| "protected"
| "public"
| "return"
| "short"
| "static"
| "strictfp"
| "super"
| "switch"
| "synchronized"
| "this"
| "throw"
| "throws"
| "transient"
| "try"
| "void"
| "volatile"
| "while"
| "true"
| "false"
| "null"
| "_"
)
}
fn sanitize_ref(name: &str) -> String {
sanitize_fq(name)
}
pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
print_class_name(pool, internal)
}
#[allow(dead_code)]
fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
let _ = java_type_to_generic;
}