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, Copy, PartialEq, Eq)]
pub enum CureMode {
Full,
Fast,
}
#[derive(Debug, Clone)]
pub struct ClassOptions {
pub provenance: bool,
pub cure: CureMode,
}
impl Default for ClassOptions {
fn default() -> Self {
ClassOptions {
provenance: true,
cure: CureMode::Full,
}
}
}
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 own_display_full = crate::apply_class_rename(&class.name);
let (pkg, _) = split_name(&own_display_full);
let pkg_lookup: &str = match crate::root_pkg_display() {
Some(rp) if rp == pkg => "",
_ => pkg.as_str(),
};
let shadow = inherited_field_shadows(pool, class);
let mut obscured_map = compute_obscured_renders(pool, class, &shadow);
static EMPTY_BLOCKED: std::sync::OnceLock<std::sync::Arc<jdc_core::FxHashSet<String>>> =
std::sync::OnceLock::new();
let blocked_base: std::sync::Arc<jdc_core::FxHashSet<String>> = pool
.package_simples()
.get(pkg_lookup)
.cloned()
.unwrap_or_else(|| {
std::sync::Arc::clone(EMPTY_BLOCKED.get_or_init(|| {
std::sync::Arc::new(jdc_core::FxHashSet::default())
}))
});
let mut blocked_removed: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut blocked_extra: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
{
let own_simple0 = class.name.rsplit(['/', '$']).next().unwrap_or("");
let can_obscure = shadow.iter().any(|f| pool.package_simples().contains_key(f))
|| (!own_simple0.is_empty()
&& pool.package_simples().contains_key(own_simple0));
if can_obscure && !blocked_base.is_empty() {
let used = sibling_ref_simples(pool, class);
if !used.is_empty() {
blocked_removed.extend(
blocked_base
.iter()
.filter(|b| !used.contains(*b))
.map(|b| b.to_string()),
);
}
}
}
static PKG_RENAMED_DISPLAYS: std::sync::OnceLock<
jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
> = std::sync::OnceLock::new();
let blocked_renamed: Option<&'static jdc_core::FxHashSet<String>> = {
let cache = PKG_RENAMED_DISPLAYS.get_or_init(|| {
let mut m: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
if let Some(ren) = jdc_core::rename::with_renames(|r| r.clone()) {
for (raw, disp) in ren.iter() {
if raw == disp {
continue;
}
let pkg_key = raw.rfind('/').map(|i| &raw[..i]).unwrap_or("");
let tail = disp
.rsplit(['/', '$'])
.next()
.unwrap_or(disp.as_str())
.to_string();
m.entry(pkg_key.to_string()).or_default().insert(tail);
}
}
m
});
cache.get(pkg_lookup)
};
{
let own_raw = class.name.rsplit('/').next().unwrap_or("");
let prefix = format!("{}$", own_raw);
let extras: Vec<String> = blocked_base
.iter()
.filter(|b| b.starts_with(&prefix))
.filter_map(|b| b.rsplit('$').next().map(|t| t.to_string()))
.collect();
blocked_extra.extend(extras);
for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
blocked_extra.insert(crate::classdec::java_ident(&f.name).into_owned());
}
}
obscured_map.retain(|_internal, simple| {
!blocked_extra.contains(simple)
&& (blocked_removed.contains(simple)
|| !blocked_base.contains(simple))
&& !blocked_renamed.is_some_and(|e| e.contains(simple))
});
let own_display_internal = crate::apply_class_rename(&class.name);
let own_disp_simple = own_display_internal
.rsplit(['/', '$'])
.next()
.unwrap_or("")
.to_string();
{
let mut by_simple: std::collections::BTreeMap<&str, Vec<&String>> =
std::collections::BTreeMap::new();
for (internal, simple) in obscured_map.iter() {
by_simple.entry(simple.as_str()).or_default().push(internal);
}
let mut drop: Vec<String> = Vec::new();
for (simple, mut internals) in by_simple {
if simple == own_disp_simple.as_str() {
drop.extend(internals.into_iter().cloned());
} else if internals.len() > 1 {
internals.sort();
drop.extend(internals.into_iter().skip(1).cloned());
}
}
for d in &drop {
obscured_map.remove(d);
}
}
let own_field_names: jdc_core::FxHashSet<String> = class
.static_fields
.iter()
.chain(class.instance_fields.iter())
.map(|f| {
jdc_core::rename::field_display(&class.name, &f.name, &f.desc)
.map(|d| d.to_string())
.unwrap_or_else(|| java_ident(&f.name).into_owned())
})
.collect();
set_obscured_state(
class.name.clone(),
obscured_map.clone(),
std::sync::Arc::clone(&blocked_base),
blocked_removed.clone(),
blocked_extra.clone(),
blocked_renamed,
shadow,
own_field_names,
);
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();
let recorded_static = take_recorded_static_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 display = if crate::is_fw_shadow(&r) {
std::borrow::Cow::Borrowed(r.as_str())
} else {
crate::apply_class_rename(&r)
};
let simple = display.rsplit(['/', '$']).next().unwrap_or("");
if !simple.is_empty()
&& !blocked_extra.contains(simple)
&& (blocked_removed.contains(simple)
|| !blocked_base.contains(simple))
&& !blocked_renamed.is_some_and(|e| e.contains(simple))
{
import_set.insert(r);
}
}
}
let mut imports: Vec<String> = import_set
.iter()
.map(|internal| {
let renamed = if crate::is_fw_shadow(internal) {
std::borrow::Cow::Borrowed(internal.as_str())
} else {
crate::apply_class_rename(internal)
};
crate::classdec::dotted(&renamed)
})
.collect();
imports.sort();
let own_display_dotted = crate::classdec::dotted(&own_display_internal);
let own_dot_simple = own_display_dotted.rsplit('.').next().unwrap_or("");
let mut seen_simples: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
for display in imports {
let simple = display.rsplit('.').next().unwrap_or("");
if simple == own_dot_simple || !seen_simples.insert(simple.to_string()) {
continue;
}
out.push_str(&format!("import {};\n", display));
}
if !recorded_static.is_empty() {
let mut statics: Vec<(String, String)> = recorded_static
.into_iter()
.map(|(member, owner)| (owner, member))
.collect();
statics.sort();
for (owner, member) in statics {
out.push_str(&format!("import static {}.{};\n", owner, member));
}
}
}
out.push('\n');
out.push_str(&body_buf);
out = crate::cure_pass::run(out, opts.cure);
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 dbg_enum() -> bool {
static CACHED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| std::env::var_os("DDC_DBG_ENUM").is_some())
}
fn dbg_enum_none(ln: u32) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
if dbg_enum() {
eprintln!("[enum-none] line {}", ln);
}
None
}
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 dbg_enum_none(line!());
}
for m in class.all_methods() {
if &*m.name != "<init>" {
continue;
}
let dex = pool.dex(m.dex_idx)?;
let ci = dex.code_at(m.code_off)?;
for ins in &ci.insns {
if let ddc_dex::insn::InsnKind::SGet { field_idx, .. } = ins.kind {
let fr = dex.field(field_idx);
if dex.class_name(fr.class_idx) == class.name.as_str()
&& dex.type_name(fr.type_idx) != "Lcom/meituan/robust/ChangeQuickRedirect;"
{
return dbg_enum_none(line!());
}
}
}
}
let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
if let Stmt::Block(vs) = &mut body.body {
crate::passes::flatten_top_blocks(vs);
}
if dbg_enum() {
if let Stmt::Block(vs) = &body.body {
for (i, st) in vs.iter().enumerate() {
let d = format!("{:?}", st);
eprintln!("[enum-stmt {}] {}", i, d.chars().take(220).collect::<String>());
}
}
}
crate::passes::strip_clinit_hotfix_guard(&mut body.body);
if let Stmt::Block(vs) = &mut body.body {
let self_cls: &str = class.name.as_str();
let top_def = |st: &Stmt| -> bool {
let v = match st {
Stmt::LocalDef { init: Some(e), .. } => e,
Stmt::ExprStmt(Expr::Assign { value, .. }) => value,
_ => return false,
};
matches!(v, Expr::New { cls, args, .. }
if cls.as_ref() == self_cls && args.len() >= 2)
};
if !vs.iter().any(top_def) {
let branch_news = |st: &Stmt| -> usize {
let mut n = 0usize;
crate::passes::visit_all_exprs(st, &mut |x| {
if let Expr::New { cls, args, .. } = x {
if cls.as_ref() == self_cls && args.len() >= 2 {
n += 1;
}
}
});
n
};
let mut pick: Option<(usize, bool)> = None;
for (i, st) in vs.iter().enumerate() {
if let Stmt::If { then_stmt, else_stmt, .. } = st {
let t = branch_news(then_stmt) > 0;
let e = else_stmt
.as_ref()
.map(|s2| branch_news(s2) > 0)
.unwrap_or(false);
if t != e {
pick = Some((i, t));
break;
}
}
}
if let Some((i, then_is_const)) = pick {
let st = std::mem::replace(&mut vs[i], Stmt::Block(Vec::new()));
if let Stmt::If { then_stmt, else_stmt, .. } = st {
let keep = if then_is_const {
Some(then_stmt)
} else {
else_stmt
};
if let Some(bx) = keep {
let inner = match *bx {
Stmt::Block(v) => v,
other => vec![other],
};
vs.splice(i..=i, inner);
crate::passes::flatten_top_blocks(vs);
}
}
}
}
}
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 dbg_enum_none(line!()); }
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 dbg_enum_none(line!()),
} {
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 dbg_enum_none(line!()); }
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 dbg_enum_none(line!()),
} {
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;
}
let bound = const_fields.iter().any(|f| {
f.name.as_ref() == &**fname
|| jdc_core::rename::field_display(
class.name.as_str(),
&f.name,
&f.desc,
)
.is_some_and(|d| d == &**fname)
});
if !bound {
continue;
}
if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { return dbg_enum_none(line!()); }
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 dbg_enum_none(line!()); }
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 dbg_enum_none(line!());
}
}
_ => return dbg_enum_none(line!()),
};
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() {
{
if dbg_enum() {
eprintln!("[enum-gate677] const_fields={:?} const_field={:?} const_name={:?} const_ord={:?}",
const_fields.iter().map(|f| f.name.to_string()).collect::<Vec<_>>(),
const_field, const_name, const_ord);
}
return dbg_enum_none(line!());
}
}
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 dbg_enum_none(line!());
}
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 dbg_enum_none(line!()); }
}
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 dbg_enum_none(line!()); }
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 dbg_enum_none(line!()); }
if ord > expect {
let tmpl: Vec<Expr> = if has_extras {
if !enum_ctors_benign(pool, class) {
return dbg_enum_none(line!()); }
match padded.last() {
Some(prev) => prev.extra_args.clone(),
None => c.extra_args.clone(),
}
} else {
Vec::new()
};
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: tmpl.clone(),
});
expect += 1;
}
}
expect = ord + 1;
padded.push(c);
}
let out = padded;
Some((out, body))
}
fn enum_ctors_benign(pool: &DexPool, class: &PoolClass) -> bool {
let mut checked = 0usize;
for m in class.all_methods() {
if &*m.name != "<init>" {
continue;
}
checked += 1;
let Some(dex) = pool.dex(m.dex_idx) else {
return false;
};
let Some(ci) = dex.code_at(m.code_off) else {
return false;
};
for ins in &ci.insns {
use ddc_dex::insn::InsnKind;
match ins.kind {
InsnKind::If { .. }
| InsnKind::Throw { .. }
| InsnKind::PackedSwitch { .. }
| InsnKind::SparseSwitch { .. }
| InsnKind::Goto { .. } => return false,
InsnKind::Invoke { method_idx, .. } => {
let mr = dex.method(method_idx);
if dex.string(mr.name_idx) != "<init>" {
return false;
}
}
_ => {}
}
}
}
checked > 0
}
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;
}
struct ParsedStub {
ret: JavaType,
args: Vec<JavaType>,
}
fn throwable_ctor_sigs(fw: &str) -> Option<Vec<String>> {
if !crate::fwdb::is_subtype(fw, "java/lang/Throwable") {
return None;
}
let mut out: Vec<String> = Vec::new();
crate::fwdb::for_each_method(fw, |d, f| {
if d.starts_with("<init>(")
&& f & crate::fwdb::MF_STATIC == 0
&& f & crate::fwdb::MF_PRIVATE == 0
&& f & crate::fwdb::MF_UNKNOWN == 0
{
let hi = d.find(')').unwrap_or(d.len());
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
out.push(d[lo..hi].to_string());
}
});
out.sort();
out.dedup();
Some(out)
}
fn synth_missing_interface_stubs(
pool: &DexPool,
class: &PoolClass,
out: &mut String,
depth: usize,
emitted_any: &mut bool,
) {
use crate::access::*;
if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
return;
}
fn argsig(d: &str) -> &str {
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
&d[lo..hi]
}
let mut required: jdc_core::FxHashMap<(String, String), Option<(String, String)>> =
jdc_core::FxHashMap::default();
let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
let mut stack: Vec<String> = Vec::new();
{
let mut cur: Option<&PoolClass> = Some(class);
let mut hops = 0;
while let Some(c) = cur {
hops += 1;
if hops > 64 {
break;
}
stack.extend(c.interfaces.iter().cloned());
let mut hit_fw = false;
cur = c.super_name.as_ref().and_then(|s| {
if s == "java/lang/Object" {
None
} else {
let pc = pool.get(s);
if pc.is_none() {
hit_fw = true;
}
pc
}
});
if hit_fw {
return;
}
}
}
let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
while let Some(iname) = stack.pop() {
if !seen.insert(iname.clone()) {
continue;
}
let Some(ic) = pool.get(&iname) else {
if crate::fwdb::class_flags(&iname) & crate::fwdb::CF_INTERFACE != 0 {
crate::fwdb::for_each_method(&iname, |d, f| {
if f & crate::fwdb::MF_ABSTRACT == 0
|| f & crate::fwdb::MF_STATIC != 0
|| f & crate::fwdb::MF_UNKNOWN != 0
|| d.starts_with("<init>")
{
return;
}
let hi = d.find('(').unwrap_or(d.len());
let mn = d[..hi].to_string();
let md = d[hi..].to_string();
let key = (mn.clone(), argsig(&md).to_string());
match required.entry(key) {
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(Some((mn, md)));
}
std::collections::hash_map::Entry::Occupied(mut o) => {
if o.get().as_ref().is_some_and(|(_, d0)| *d0 != md) {
o.insert(None);
}
}
}
});
}
continue;
};
for m in ic.all_methods() {
if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
continue;
}
let key = (m.name.to_string(), argsig(&m.desc).to_string());
if m.code_off == 0 {
let mdesc = m.desc.to_string();
match required.entry(key) {
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(Some((m.name.to_string(), mdesc)));
}
std::collections::hash_map::Entry::Occupied(mut o) => {
if o.get().as_ref().is_some_and(|(_, d)| *d != mdesc) {
o.insert(None);
}
}
}
} else {
defaults.insert(key);
}
}
stack.extend(ic.interfaces.iter().cloned());
}
if required.is_empty() {
return;
}
let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
provided.insert(("hashCode".into(), String::new()));
provided.insert(("toString".into(), String::new()));
let mut chain_sigs: jdc_core::FxHashMap<(String, String), jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
let mut cur: Option<&PoolClass> = Some(class);
while let Some(c) = cur {
for m in c.all_methods() {
if !m.is_static() && &*m.name != "<init>" && &*m.name != "<clinit>" {
chain_sigs
.entry((m.name.to_string(), argsig(&m.desc).to_string()))
.or_default()
.insert(m.desc.to_string());
}
}
for m in c.all_methods() {
if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
let renamed = jdc_core::rename::field_display(&c.name, &m.name, &m.desc)
.is_some();
if !renamed {
provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
}
}
}
cur = c.super_name.as_ref().and_then(|s| {
if s == "java/lang/Object" {
None
} else {
pool.get(s)
}
});
}
let mut chain_conflict: jdc_core::FxHashSet<(String, String)> =
jdc_core::FxHashSet::default();
for ((name, _argsig), descs) in chain_sigs.iter() {
if descs.len() > 1 {
for d in descs {
chain_conflict.insert((name.clone(), d.clone()));
}
}
}
let mut missing: Vec<(String, String)> = Vec::new();
for (key, m) in &required {
let Some(m) = m else { continue }; if defaults.contains(key) || provided.contains(key) {
continue;
}
if m.1.ends_with(")Lkotlin/Unit;") {
continue;
}
if chain_conflict.contains(&(m.0.clone(), m.1.clone())) {
continue;
}
missing.push(m.clone());
}
missing.sort();
let ind = " ".repeat(depth + 1);
for (mname, mdesc) in missing {
let lo = mdesc.find('(').map(|i| i + 1).unwrap_or(0);
let hi = mdesc.find(')').unwrap_or(mdesc.len());
let ret_s = &mdesc[hi + 1..];
let args_v = split_arg_descs(&mdesc[lo..hi]);
let d = ParsedStub { ret: crate::desc_type(ret_s), args: args_v };
if *emitted_any {
out.push('\n');
}
out.push_str(&ind);
out.push_str("public ");
out.push_str(&type_name(pool, &d.ret));
out.push(' ');
out.push_str(&java_ident(&mname));
out.push('(');
for (i, a) in d.args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&type_name(pool, a));
out.push_str(&format!(" p{}", i + 1));
}
out.push_str(") {\n");
out.push_str(&ind);
out.push_str(" throw new AbstractMethodError();\n");
out.push_str(&ind);
out.push_str("}\n");
*emitted_any = true;
}
}
const ENUM_NAME_FIELD: &str = "$ddcName";
const ENUM_ORD_FIELD: &str = "$ddcOrdinal";
fn fallback_enum_base(class: &PoolClass) -> bool {
class.is_enum()
&& class
.super_name
.as_ref()
.map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
.unwrap_or(true)
}
fn enum_synth_active(class: &PoolClass) -> bool {
fallback_enum_base(class)
&& enum_trace_ctor(class)
&& !class
.static_fields
.iter()
.chain(class.instance_fields.iter())
.any(|f| &*f.name == ENUM_NAME_FIELD || &*f.name == ENUM_ORD_FIELD)
}
fn enum_trace_ctor(class: &PoolClass) -> bool {
class.all_methods().any(|m| {
&*m.name == "<init>"
&& m.parsed_desc()
.map(|d| {
matches!(d.args.first(), Some(JavaType::Object(s))
if s.as_ref() == "java/lang/String")
&& matches!(d.args.get(1), Some(JavaType::Int))
})
.unwrap_or(false)
})
}
fn synth_enum_members(
pool: &DexPool,
class: &PoolClass,
out: &mut String,
depth: usize,
emitted_any: &mut bool,
) {
use crate::access::*;
if !fallback_enum_base(class) {
return;
}
let self_ty = JavaType::Object(class.name.as_str().into());
let self_name = type_name(pool, &self_ty);
let self_arr = JavaType::Array(Box::new(self_ty));
let arr_name = type_name(pool, &self_arr);
let traced = enum_synth_active(class);
let has_values = class.all_methods().any(|m| {
&*m.name == "values"
&& m.access & ACC_STATIC != 0
&& m.parsed_desc()
.map(|d| d.ret == self_arr && d.args.is_empty())
.unwrap_or(false)
});
let ind = " ".repeat(depth + 1);
let push_line = |out: &mut String, l: &str| {
out.push_str(&ind);
out.push_str(l);
out.push('\n');
};
if !has_values {
let want = format!("[L{};", class.name);
if let Some(vf) = class.static_fields.iter().find(|f| &*f.desc == want.as_str()) {
if *emitted_any {
out.push('\n');
}
*emitted_any = true;
push_line(out, &format!("public static {} values() {{", arr_name));
push_line(
out,
&format!(" return ({}) {}.clone();", arr_name, java_ident(&vf.name)),
);
push_line(out, "}");
}
}
if !traced {
return; }
if *emitted_any {
out.push('\n');
}
*emitted_any = true;
push_line(out, &format!("private java.lang.String {};", ENUM_NAME_FIELD));
push_line(out, &format!("private int {};", ENUM_ORD_FIELD));
out.push('\n');
push_line(out, &format!(
"public java.lang.String name() {{\n{} return this.{};\n{}}}",
ind, ENUM_NAME_FIELD, ind
));
out.push('\n');
push_line(out, &format!(
"public int ordinal() {{\n{} return this.{};\n{}}}",
ind, ENUM_ORD_FIELD, ind
));
out.push('\n');
push_line(out, &format!(
"public int compareTo({0} o) {{\n{1} return this.{2} - o.{2};\n{1}}}",
self_name, ind, ENUM_ORD_FIELD
));
out.push('\n');
push_line(out, &format!(
"public static {0} valueOf(java.lang.String s) {{\n{1} for ({0} v : values()) {{\n{1} if (v.name().equals(s)) {{\n{1} return v;\n{1} }}\n{1} }}\n{1} throw new java.lang.IllegalArgumentException(s);\n{1}}}",
self_name, ind
));
}
fn synth_enum_ordinal_scan(
pool: &DexPool,
class: &PoolClass,
out: &mut String,
depth: usize,
emitted_any: &mut bool,
) {
if !fallback_enum_base(class) || enum_trace_ctor(class) {
return;
}
if class
.all_methods()
.any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
{
return;
}
let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
let arr_name = type_name(pool, &self_arr);
let ind = " ".repeat(depth + 1);
if *emitted_any {
out.push('\n');
}
*emitted_any = true;
out.push_str(&ind);
out.push_str("public int ordinal() {\n");
out.push_str(&ind);
out.push_str(&format!(" {} vs = values();\n", arr_name));
out.push_str(&ind);
out.push_str(" for (int i = 0; i < vs.length; i++) {\n");
out.push_str(&ind);
out.push_str(" if (vs[i] == this) {\n");
out.push_str(&ind);
out.push_str(" return i;\n");
out.push_str(&ind);
out.push_str(" }\n");
out.push_str(&ind);
out.push_str(" }\n");
out.push_str(&ind);
out.push_str(" return 0;\n");
out.push_str(&ind);
out.push_str("}\n");
}
fn strip_robust_enum_ctor_guard(body: &mut Stmt) {
let is_patchy = |e: &Expr| {
matches!(e, Expr::Method { cls, name, .. }
if cls.contains("PatchProxy")
&& (&**name == "isSupport"
|| &**name == "accessDispatch"
|| &**name == "proxy"
|| &**name == "isSupportClinit"
|| &**name == "accessDispatchClinit"))
};
let Stmt::Block(vs) = body else { return };
if !vs.iter().any(|st| {
let mut h = false;
crate::passes::visit_all_exprs(st, &mut |x| {
if is_patchy(x) {
h = true;
}
});
h
}) {
return;
}
let mut dead: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
let patchy_arg_locals = |st: &Stmt, out: &mut jdc_core::FxHashSet<u32>| {
crate::passes::visit_all_exprs(st, &mut |x| {
if let Expr::Method { args, .. } = x {
if is_patchy(x) {
for a in args {
let mut aa = a.clone();
crate::passes::deep_rewrite(&mut aa, &mut |y| {
if let Expr::Local { var, .. } = y {
out.insert(*var);
}
});
}
}
}
});
};
for _ in 0..4 {
let before = vs.len();
let mut i = 0usize;
while i < vs.len() {
let action = {
let st = &vs[i];
match st {
Stmt::LocalDef { var, .. } if dead.contains(var) => Some((*var, false)),
Stmt::LocalDef { var, init: Some(e), .. } => {
let mut has_patchy = false;
let mut ec = e.clone();
crate::passes::deep_rewrite(&mut ec, &mut |x| {
if is_patchy(x) {
has_patchy = true;
}
});
if has_patchy {
Some((*var, true))
} else {
None
}
}
Stmt::If { .. } => {
let mut h = false;
crate::passes::visit_all_exprs(st, &mut |x| {
if is_patchy(x) {
h = true;
}
});
if h {
Some((u32::MAX, true))
} else {
None
}
}
Stmt::ExprStmt(Expr::Assign { target, .. }) => {
let mut tgt_dead = false;
let mut tc = target.clone();
crate::passes::deep_rewrite(&mut tc, &mut |x| {
if let Expr::Local { var, .. } = x {
if dead.contains(var) {
tgt_dead = true;
}
}
});
if tgt_dead {
Some((u32::MAX, false))
} else {
None
}
}
_ => None,
}
};
if let Some((var, collect_args)) = action {
if collect_args {
patchy_arg_locals(&vs[i], &mut dead);
}
if var != u32::MAX {
dead.insert(var);
}
vs.remove(i);
} else {
i += 1;
}
}
if vs.len() == before {
break;
}
}
}
#[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(&sanitize_ref(&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(&sanitize_ref(&simple));
} else if is_iface {
head.push_str("interface ");
head.push_str(&sanitize_ref(&simple));
} else {
head.push_str("class ");
head.push_str(&sanitize_ref(&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 {
let suppress_extends =
is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
if sup != "java/lang/Object" && !suppress_extends {
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_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
f.access & crate::access::ACC_FINAL != 0
&& class.static_values.get(i).is_some()
});
if !has_valued_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
&& !class.is_interface()
&& clinit_sputs
.as_ref()
.is_some_and(|s| {
s.contains(&(
f.name.clone(),
f.desc.clone(),
))
});
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>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
let d: &'m str = &m.desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
let n = if renames_on {
jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
} else {
&*m.name
};
(n, &d[lo..hi])
}
let renames_on = jdc_core::rename::member_rename_active();
let suppress_dex_valueof = class.is_enum()
&& enum_consts.is_none()
&& enum_synth_active(class);
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(&class.name, renames_on, 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(&class.name, renames_on, m)) != Some(&i) {
continue;
}
if bridge_shadowed_by_ancestor(pool, class, m) {
continue;
}
if suppress_dex_valueof
&& m.is_static()
&& &*m.name == "valueOf"
&& m.parsed_desc().map(|d| {
d.args.len() == 1
&& matches!(d.args[0], JavaType::Object(ref sn)
if sn.as_ref() == "java/lang/String")
&& d.ret == JavaType::Object(class.name.as_str().into())
}).unwrap_or(false)
{
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(&sanitize_ref(&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,
}
}
}
}
if !class.is_interface()
&& enum_consts.is_none()
&& class.all_methods().any(|m| &*m.name == "<init>")
{
if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
fn arg_sig(d: &str) -> &str {
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
&d[lo..hi]
}
let declared: jdc_core::FxHashSet<&str> = class
.all_methods()
.filter(|m| &*m.name == "<init>")
.map(|m| arg_sig(&m.desc))
.collect();
let mut missing: Vec<&String> = refs
.iter()
.filter(|s| !declared.contains(s.as_str()))
.collect();
missing.sort();
for sig in missing {
let mut sup = class.super_name.clone();
let mut found: Option<&PoolMethod> = None;
while let Some(s) = sup {
if s == "java/lang/Object" {
break;
}
let Some(sc) = pool.get(&s) else { break };
if let Some(sm) = sc.all_methods().find(|m| {
&*m.name == "<init>"
&& arg_sig(&m.desc) == sig.as_str()
&& m.access & crate::access::ACC_PRIVATE == 0
&& m.access
& (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
!= 0
}) {
found = Some(sm);
break;
}
sup = sc.super_name.clone();
}
let Some(sm) = found else {
if sig.is_empty() {
if emitted_any {
out.push('\n');
}
out.push_str(&format!(" {}", " ".repeat(depth)));
out.push_str("public ");
out.push_str(&sanitize_ref(&simple));
out.push_str("() {\n");
out.push_str(&format!(
" {} super();\n",
" ".repeat(depth)
));
out.push_str(&format!(" {}}}\n", " ".repeat(depth)));
emitted_any = true;
}
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(&sanitize_ref(&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;
}
}
}
if !class.is_interface() && enum_consts.is_none() {
let direct_fw = class
.super_name
.as_ref()
.map(|s| s != "java/lang/Object" && pool.get(s).is_none())
.unwrap_or(false);
if direct_fw {
if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
fn arg_sig(d: &str) -> &str {
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
&d[lo..hi]
}
let declared: jdc_core::FxHashSet<&str> = class
.all_methods()
.filter(|m| &*m.name == "<init>")
.map(|m| arg_sig(&m.desc))
.collect();
let mut missing: Vec<&String> = refs
.iter()
.filter(|s| !declared.contains(s.as_str()))
.collect();
missing.sort();
for sig in missing {
if sig.is_empty() {
continue;
}
let args = split_arg_descs(sig);
if emitted_any {
out.push('\n');
}
out.push_str(&format!(" {}", " ".repeat(depth)));
out.push_str("public ");
out.push_str(&sanitize_ref(&simple));
out.push('(');
let mut names = Vec::with_capacity(args.len());
for (i, a) in 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;
}
}
}
}
if !class.is_interface()
&& enum_consts.is_none()
&& !class.all_methods().any(|m| &*m.name == "<init>")
{
let mut fw_anc: Option<&str> = None;
if let Some(sup) = class.super_name.as_deref() {
let mut cur = sup;
let mut hops = 0;
loop {
hops += 1;
if hops > 32 || cur == "java/lang/Object" {
break;
}
match pool.get(cur) {
Some(sc) => {
if sc.all_methods().any(|m| &*m.name == "<init>") {
break;
}
match sc.super_name.as_deref() {
Some(n) => cur = n,
None => break,
}
}
None => {
fw_anc = Some(cur);
break;
}
}
}
}
if let Some(sigs) = fw_anc.and_then(throwable_ctor_sigs) {
let ref_sigs = pool.ctor_ref_sigs(&class.name);
for sig in &sigs {
if !sig.is_empty()
&& ref_sigs.is_some_and(|rs| rs.contains(sig))
{
continue;
}
let args = split_arg_descs(sig);
if emitted_any {
out.push('\n');
}
out.push_str(&format!(" {}", " ".repeat(depth)));
out.push_str("public ");
out.push_str(&sanitize_ref(&simple));
out.push('(');
let mut names = Vec::with_capacity(args.len());
for (i, a) in 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;
}
}
}
for h in crate::passes::take_ctor_helpers(&class.name) {
if emitted_any {
out.push('\n');
}
out.push_str(&indent(depth + 1));
out.push_str("private static ");
out.push_str(&type_name(pool, &h.ret));
out.push(' ');
out.push_str(&h.name);
out.push('(');
for (i, (ty, nm)) in h.params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&type_name(pool, ty));
out.push(' ');
out.push_str(&java_ident(nm));
}
out.push_str(") {\n");
let p = Printer::new(ctx, &h.vt);
let text = p.with_indent(depth + 2).into_string(&h.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;
}
for (ri, (_desc, r)) in crate::passes::take_ctor_relays(&class.name)
.into_iter()
.enumerate()
{
if ri > 0 {
continue;
}
if emitted_any {
out.push('\n');
}
let disp = crate::apply_class_rename(&class.name);
let (_, simple) = split_name(&disp);
let ctor_name = if depth == 0 {
simple.clone()
} else {
let seg = simple.rsplit('$').next().unwrap_or("");
if seg.is_empty() {
simple.clone()
} else {
seg.to_string()
}
};
out.push_str(&indent(depth + 1));
out.push_str("private ");
out.push_str(&java_ident(&ctor_name));
out.push_str("(Object[] h$relay) {\n");
out.push_str(&indent(depth + 2));
out.push_str(if r.is_super { "super(" } else { "this(" });
for (i, t) in r.formals.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let slot = format!("h$relay[{}]", i);
match t {
JavaType::Boolean => out.push_str(&format!("((Boolean) {}).booleanValue()", slot)),
JavaType::Byte => out.push_str(&format!("((Byte) {}).byteValue()", slot)),
JavaType::Char => out.push_str(&format!("((Character) {}).charValue()", slot)),
JavaType::Short => out.push_str(&format!("((Short) {}).shortValue()", slot)),
JavaType::Int => out.push_str(&format!("((Integer) {}).intValue()", slot)),
JavaType::Float => out.push_str(&format!("((Float) {}).floatValue()", slot)),
JavaType::Long => out.push_str(&format!("((Long) {}).longValue()", slot)),
JavaType::Double => out.push_str(&format!("((Double) {}).doubleValue()", slot)),
other => {
out.push('(');
out.push_str(&type_name(pool, other));
out.push_str(") ");
out.push_str(&slot);
}
}
}
out.push_str(");\n");
out.push_str(&indent(depth + 1));
out.push_str("}\n");
emitted_any = true;
}
synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
if class.is_enum() && enum_consts.is_none() {
synth_enum_members(pool, class, out, depth, &mut emitted_any);
synth_enum_ordinal_scan(pool, class, out, depth, &mut emitted_any);
}
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 !crate::clean_member_tail(rest) {
continue;
}
if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
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 && !unfinal_field(f_class, &f.name) {
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);
let fname: std::borrow::Cow<str> = if fname == "$assertionsDisabled" {
std::borrow::Cow::Borrowed(jdc_core::analysis::ASSERT_FIELD)
} else {
std::borrow::Cow::Borrowed(fname)
};
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, desc) => {
let n: std::borrow::Cow<str> = match jdc_core::rename::field_display(cls, name, desc) {
Some(d) => std::borrow::Cow::Borrowed(d),
None => java_ident(name),
};
if cls == owner {
n.into_owned()
} else {
format!("{}.{}", print_class_name(pool, cls), n)
}
}
StaticValue::Other => return None,
})
}
fn bridge_shadowed_by_ancestor(pool: &DexPool, class: &PoolClass, m: &PoolMethod) -> bool {
if m.access & crate::access::ACC_BRIDGE == 0 || &*m.name == "<init>" || m.code_off == 0 {
return false;
}
let Some(d) = m.parsed_desc() else {
return false;
};
let Some(dex) = pool.dex(m.dex_idx) else {
return false;
};
let dex = &*dex;
let Some(code) = dex.code_insns_bytes_at(m.code_off) else {
return false;
};
let mut head_ok = false;
let mut bad_tail = false;
let mut cnt = 0usize;
let mut first = true;
ddc_dex::insn::scan_instructions(code, &mut |op, pc, bytes| {
cnt += 1;
if first {
first = false;
if op == 0x6f || op == 0x75 {
let unit = bytes
.get(2 * (pc + 1)..2 * (pc + 1) + 2)
.map(|b| u16::from_le_bytes([b[0], b[1]]) as u32)
.unwrap_or(u32::MAX);
if unit != u32::MAX {
let mr = dex.method(unit);
head_ok = dex.string(mr.name_idx) == &*m.name;
}
}
} else if !matches!(op, 0x0a | 0x0b | 0x0c | 0x0e | 0x0f | 0x10 | 0x11) {
bad_tail = true;
}
});
if !head_ok || bad_tail || !(2..=3).contains(&cnt) {
return false;
}
let mut cur: Option<String> = class.super_name.clone();
let mut hops = 0u32;
while let Some(s) = cur {
hops += 1;
if hops > 64 {
return false;
}
let Some(pc) = pool.get_if_materialized(&s) else {
return false; };
if pc.all_methods().any(|am| {
am.name == m.name
&& am.access & crate::access::ACC_PRIVATE == 0
&& am.parsed_desc().is_some_and(|ad| ad.args == d.args)
}) {
return true;
}
cur = pc.super_name.clone();
}
false
}
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 {
let fallback_enum_base_ctor = class.is_enum()
&& !enum_promoted
&& &*m.name == "<init>"
&& class
.super_name
.as_ref()
.map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
.unwrap_or(true);
if !fallback_enum_base_ctor {
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 &*m.name == "<clinit>" {
if let Some(b) = &mut body {
crate::passes::strip_clinit_hotfix_guard(&mut b.body);
}
}
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(&sanitize_ref(&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('(');
if enum_promoted && is_init {
if let Some(b) = body.as_mut() {
strip_robust_enum_ctor_guard(&mut b.body);
}
}
if is_init
&& !enum_promoted
&& class.is_enum()
&& enum_synth_active(class)
&& matches!(d.args.first(), Some(JavaType::Object(sx)) if sx.as_ref() == "java/lang/String")
&& matches!(d.args.get(1), Some(JavaType::Int))
{
if let Some(b) = body.as_mut() {
let starts_with_delegation =
crate::passes::is_bare_ctor_call(crate::passes::first_leaf_stmt(&b.body));
let ps: Vec<u32> = b
.vt
.vars
.iter()
.filter(|v| v.is_param && v.name != "this")
.take(2)
.map(|v| v.id)
.collect();
if ps.len() == 2 && !starts_with_delegation {
let mk_store = |var: u32, fname: &str, fty: JavaType| {
Stmt::ExprStmt(Expr::Assign {
target: Box::new(Expr::Field {
owner: Some(Box::new(Expr::This)),
cls: std::sync::Arc::from(class.name.as_str()),
name: std::sync::Arc::from(fname),
ty: jdc_core::ir::expr::TypeRef::J(fty.clone()),
is_static: false,
}),
op: jdc_core::ir::expr::AssignOp::Plain,
value: Box::new(Expr::Local {
var,
ty: jdc_core::ir::expr::TypeRef::J(fty),
}),
})
};
let mut inj = vec![
mk_store(ps[0], ENUM_NAME_FIELD, JavaType::Object("java/lang/String".into())),
mk_store(ps[1], ENUM_ORD_FIELD, JavaType::Int),
];
if let Stmt::Block(vs) = &mut b.body {
let mut rest = std::mem::take(vs);
inj.append(&mut rest);
*vs = inj;
} else {
let prev = std::mem::replace(&mut b.body, Stmt::Block(Vec::new()));
inj.push(prev);
b.body = Stmt::Block(inj);
}
}
}
}
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 n_args = d.args.len();
let is_bridge = n_args >= 2
&& matches!(&d.args[n_args - 1], JavaType::Object(ref m)
if m.starts_with("kotlin/jvm/internal/"))
&& matches!(d.args[n_args - 2], JavaType::Int);
if synthetic.len() == 2 && !is_bridge {
if let Some(b) = body.as_ref() {
let (p0, p1) = (synthetic[0], synthetic[1]);
let mut lead_reads = 0usize;
{
let cls_name = class.name.as_str();
crate::passes::visit_stmt_exprs_ro(&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 {
for a in args.iter().take(2) {
if let Expr::Local { var: v, .. } = a {
if *v == p0 || *v == p1 {
lead_reads += 1;
}
}
}
}
}
});
}
let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
let reads = uses.get(&p0).copied().unwrap_or(0)
+ uses.get(&p1).copied().unwrap_or(0);
let written = crate::passes::local_is_written(&b.body, p0)
|| crate::passes::local_is_written(&b.body, p1);
if lead_reads == 0 && reads > 0 && !written {
let mk = |nm: &str, ret: JavaType| Expr::Method {
owner: Some(Box::new(Expr::This)),
cls: std::sync::Arc::from("java/lang/Enum"),
name: std::sync::Arc::from(nm),
desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
ret,
args: Vec::new(),
}),
args: Vec::new(),
is_static: false,
is_interface: false,
is_special: false,
is_super: false,
is_dynamic: false,
type_args: Vec::new(),
};
let name_call =
mk("name", JavaType::Object("java/lang/String".into()));
let ord_call = mk("ordinal", JavaType::Int);
if let Some(b) = body.as_mut() {
crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
crate::passes::deep_rewrite(e, &mut |x| {
if let Expr::Local { var, .. } = x {
if *var == p0 {
*x = name_call.clone();
} else if *var == p1 {
*x = ord_call.clone();
}
}
});
});
}
}
}
}
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 cls_name = class.name.as_str();
crate::passes::visit_stmt_exprs_ro(&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 {
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);
total == lead[pi]
})
});
if refs_ok {
arg0 = 2;
if let Some(b) = body.as_mut() {
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(crate) fn split_arg_descs(sig: &str) -> Vec<JavaType> {
let mut out = Vec::new();
let b = sig.as_bytes();
let mut i = 0usize;
while i < b.len() {
let start = i;
while i < b.len() && b[i] == b'[' {
i += 1;
}
if i < b.len() {
if b[i] == b'L' {
while i < b.len() && b[i] != b';' {
i += 1;
}
if i < b.len() {
i += 1; }
} else {
i += 1; }
}
if i > start {
out.push(desc_type(&sig[start..i]));
} else {
break; }
}
out
}
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,
shadow: jdc_core::FxHashSet<String>,
map: jdc_core::FxHashMap<String, String>,
recorded: jdc_core::FxHashSet<String>,
recorded_static: jdc_core::FxHashMap<String, String>,
own_field_names: jdc_core::FxHashSet<String>,
blocked_base: std::sync::Arc<jdc_core::FxHashSet<String>>,
blocked_removed: jdc_core::FxHashSet<String>,
blocked_extra: jdc_core::FxHashSet<String>,
blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
}
thread_local! {
static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
const { std::cell::RefCell::new(None) };
}
#[allow(clippy::too_many_arguments)] pub(crate) fn set_obscured_state(
class: String,
map: jdc_core::FxHashMap<String, String>,
blocked_base: std::sync::Arc<jdc_core::FxHashSet<String>>,
blocked_removed: jdc_core::FxHashSet<String>,
blocked_extra: jdc_core::FxHashSet<String>,
blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
shadow: jdc_core::FxHashSet<String>,
own_field_names: jdc_core::FxHashSet<String>,
) {
OBSCURE.with(|m| {
*m.borrow_mut() = Some(ObscureState {
class,
shadow,
map,
own_field_names,
recorded: jdc_core::FxHashSet::default(),
recorded_static: jdc_core::FxHashMap::default(),
blocked_base,
blocked_removed,
blocked_extra,
blocked_renamed,
});
});
}
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.recorded_static.keys() {
out.insert(simple.clone());
}
for simple in st.map.values() {
out.insert(simple.clone());
}
for internal in &st.recorded {
let renamed = crate::apply_class_rename(internal);
if let Some(tail) = renamed.rsplit(['/', '$']).next() {
if !tail.is_empty() {
out.insert(tail.to_string());
}
}
}
out
})
}
pub(crate) fn take_recorded_static_and_clear() -> jdc_core::FxHashMap<String, String> {
OBSCURE.with(|m| {
let mut st = m.borrow_mut();
st.as_mut()
.map(|st| std::mem::take(&mut st.recorded_static))
.unwrap_or_default()
})
}
pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
OBSCURE.with(|m| {
m.borrow_mut()
.as_mut()
.map(|st| std::mem::take(&mut 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>>,
unfinal: jdc_core::FxHashSet<(String, 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 {
for (name, desc) in set {
if name == "<clinit>" {
continue; }
let mut cur: Option<&str> = Some(owner.as_str());
let mut hops = 0u32;
while let Some(cname) = cur {
hops += 1;
if hops > 64 {
break;
}
let Some(pc) = pool.get_if_materialized(cname) else {
break;
};
if name == "<init>" && pc.access & crate::access::ACC_ENUM != 0 {
break;
}
match pc.all_methods()
.find(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str())
{
Some(m) if m.access & crate::access::ACC_PUBLIC == 0 => {
methods
.entry(cname.to_string())
.or_default()
.insert((name.clone(), desc.clone()));
break;
}
Some(_) => break, None => cur = pc.super_name.as_deref(),
}
}
}
}
{
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());
}
}
}
for sup in subs.keys() {
let Some(pc) = pool.get_if_materialized(sup) else {
continue;
};
if pc.access & crate::access::ACC_ENUM != 0 {
continue;
}
for m in pc.all_methods() {
if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
methods
.entry(sup.clone())
.or_default()
.insert((m.name.to_string(), m.desc.to_string()));
}
}
}
}
let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
for (owner, set) in &raw.fields {
for name in set {
let mut cur: Option<&str> = Some(owner.as_str());
let mut hops = 0u32;
while let Some(cname) = cur {
hops += 1;
if hops > 64 {
break;
}
let Some(pc) = pool.get_if_materialized(cname) else {
break;
};
match pc.static_fields
.iter()
.chain(pc.instance_fields.iter())
.find(|f| f.name.as_ref() == name.as_str())
{
Some(f) if f.access & crate::access::ACC_PUBLIC == 0 => {
fields
.entry(cname.to_string())
.or_default()
.insert(name.clone());
break;
}
Some(_) => break, None => cur = pc.super_name.as_deref(),
}
}
}
}
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 unfinal: jdc_core::FxHashSet<(String, String)> =
raw.unfinal_fields.into_iter().collect();
let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
}
#[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))
})
}
#[inline]
pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
WIDEN.get().is_some_and(|w| {
w.unfinal.contains(&(owner.to_string(), name.to_string()))
})
}
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_renamed = crate::apply_class_rename(&st.class);
let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
let seg_obscured = first == own || st.shadow.contains(first);
if seg_obscured && internal.split('/').count() >= 2 {
let renamed = crate::apply_class_rename(internal);
let simple = crate::classdec::dotted(&renamed)
.rsplit('.')
.next()
.unwrap_or("")
.to_string();
if !simple.is_empty()
&& !st.blocked_extra.contains(&simple)
&& (st.blocked_removed.contains(&simple)
|| !st.blocked_base.contains(&simple))
&& !st.blocked_renamed.is_some_and(|e| e.contains(&simple))
{
st.recorded.insert(internal.to_string());
return Some(simple);
}
}
None
})
}
pub(crate) fn obscured_member_escape_pub(owner: &str, member: &str) -> bool {
OBSCURE.with(|m| {
let mut st = m.borrow_mut();
let Some(st) = st.as_mut() else { return false };
if owner.split('/').count() < 2 {
return false;
}
let first = owner.split('/').next().unwrap_or("");
let own_renamed = crate::apply_class_rename(&st.class);
let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
let seg_obscured = first == own || st.shadow.contains(first);
if !seg_obscured {
return false;
}
let own_fields: jdc_core::FxHashSet<&str> = st
.own_field_names
.iter()
.map(|s| s.as_str())
.collect();
let renamed_simple = {
let r = crate::apply_class_rename(owner);
crate::classdec::dotted(&r)
.rsplit('.')
.next()
.unwrap_or("")
.to_string()
};
let simple_unusable = !renamed_simple.is_empty()
&& (own_fields.contains(renamed_simple.as_str())
|| st.shadow.contains(renamed_simple.as_str())
|| renamed_simple == own);
if !simple_unusable {
return false; }
if member.is_empty()
|| own_fields.contains(member)
|| st.shadow.contains(member)
|| member == own
|| st.blocked_extra.contains(member)
|| (!st.blocked_removed.contains(member) && st.blocked_base.contains(member))
|| st.blocked_renamed.is_some_and(|e| e.contains(member))
|| st.map.values().any(|v| v == member)
{
return false;
}
let renamed = crate::apply_class_rename(owner);
let display_dotted = crate::classdec::dotted(&renamed);
st.recorded_static.insert(member.to_string(), display_dotted.clone());
true
})
}
fn inherited_field_shadows(
pool: &DexPool,
class: &PoolClass,
) -> jdc_core::FxHashSet<String> {
fn fw_chain_fields(boundary: &str) -> std::sync::Arc<jdc_core::FxHashSet<String>> {
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>>>> =
OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(jdc_core::FxHashMap::default()));
if let Some(hit) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(boundary) {
return hit.clone();
}
let mut set: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut stack: Vec<&str> = vec![boundary];
let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
let mut hops = 0usize;
while let Some(c) = stack.pop() {
hops += 1;
if hops > 256 || !seen.insert(c) {
continue;
}
crate::fwdb::for_each_field(c, |n, f| {
if f & crate::fwdb::MF_STATIC != 0 {
set.insert(n.to_string());
}
});
if let Some(sup) = crate::fwdb::super_of(c) {
stack.push(sup);
}
crate::fwdb::for_each_interface(c, |i| stack.push(i));
}
let arc = std::sync::Arc::new(set);
cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(boundary.to_string(), arc.clone());
arc
}
let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut stack: Vec<String> = vec![class.name.to_string()];
let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut hops = 0usize;
while let Some(cn) = stack.pop() {
hops += 1;
if hops > 512 || !seen.insert(cn.clone()) {
continue;
}
let Some(pc) = pool.get(&cn) else {
out.extend(fw_chain_fields(&cn).iter().cloned());
continue;
};
for f in pc.static_fields.iter() {
out.insert(f.name.to_string());
}
for f in pc.instance_fields.iter() {
if f.access & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED) != 0 {
out.insert(f.name.to_string());
}
}
if let Some(sup) = &pc.super_name {
if sup != "java/lang/Object" {
stack.push(sup.clone());
} else {
}
} else {
}
stack.extend(pc.interfaces.iter().cloned());
}
out
}
fn sibling_simple(internal: &str, pkg: &str) -> Option<String> {
let (p, tail) = match internal.rfind('/') {
Some(i) => (&internal[..i], &internal[i + 1..]),
None => ("", internal),
};
if p != pkg || tail.is_empty() {
return None;
}
let base = tail.split('$').next().unwrap_or("");
if base.is_empty() {
None
} else {
Some(base.to_string())
}
}
fn sibling_ref_simples(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashSet<String> {
let pkg = class.name.rfind('/').map(|i| &class.name[..i]).unwrap_or("");
let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let add_internal = |n: &str, out: &mut jdc_core::FxHashSet<String>| {
if let Some(s) = sibling_simple(n, pkg) {
out.insert(s);
}
};
let add_desc = |d: &str, out: &mut jdc_core::FxHashSet<String>| {
let mut t = d;
while t.starts_with('[') {
t = &t[1..];
}
if t.len() > 1 && t.starts_with('L') && t.ends_with(';') {
add_internal(&t[1..t.len() - 1], out);
}
};
let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut queue: Vec<String> = vec![class.name.clone()];
while let Some(cn) = queue.pop() {
if !seen.insert(cn.clone()) {
continue;
}
queue.extend(pool.children_of(&cn).iter().cloned());
let Some(pc) = pool.get(&cn) else { continue };
add_internal(&pc.name, &mut out);
if let Some(sup) = &pc.super_name {
add_internal(sup, &mut out);
}
for i in &pc.interfaces {
add_internal(i, &mut out);
}
for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
add_desc(&f.desc, &mut out);
}
for m in pc.all_methods() {
if let Some(d) = m.parsed_desc() {
for a in &d.args {
if let JavaType::Object(n) = a {
add_internal(n, &mut out);
} else if let JavaType::Array(_) = a {
add_desc(&a.to_descriptor(), &mut out);
}
}
if let JavaType::Object(n) = &d.ret {
add_internal(n, &mut out);
} else if let JavaType::Array(_) = &d.ret {
add_desc(&d.ret.to_descriptor(), &mut out);
}
}
let Some(dex) = pool.dex(m.dex_idx) else {
continue;
};
let Some(code) = dex.code_at(m.code_off) else {
continue;
};
for ins in code.insns.iter() {
use ddc_dex::insn::InsnKind;
match &ins.kind {
InsnKind::Invoke { method_idx, .. } => {
let mm = dex.method(*method_idx);
add_internal(&dex.class_name(mm.class_idx), &mut out);
}
InsnKind::IGet { field_idx, .. }
| InsnKind::IPut { field_idx, .. }
| InsnKind::SGet { field_idx, .. }
| InsnKind::SPut { field_idx, .. } => {
let f = dex.field(*field_idx);
add_internal(&dex.class_name(f.class_idx), &mut out);
}
InsnKind::ConstClass { type_idx, .. }
| InsnKind::NewInstance { type_idx, .. }
| InsnKind::NewArray { type_idx, .. }
| InsnKind::FilledNewArray { type_idx, .. }
| InsnKind::CheckCast { type_idx, .. }
| InsnKind::InstanceOf { type_idx, .. } => {
add_desc(dex.type_name(*type_idx), &mut out);
}
_ => {}
}
}
for h in code.handlers.iter() {
for (tidx, _) in &h.catches {
add_desc(dex.type_name(*tidx), &mut out);
}
}
}
}
out
}
fn compute_obscured_renders(
pool: &DexPool,
class: &PoolClass,
shadow: &jdc_core::FxHashSet<String>,
) -> jdc_core::FxHashMap<String, String> {
let own_renamed = crate::apply_class_rename(&class.name);
let own_simple = own_renamed.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("");
let seg_obscured = first == own_simple || shadow.contains(first);
if seg_obscured && 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 = crate::classdec::dotted(&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 orig_internal: &str = internal;
let shadow_skip = crate::is_fw_shadow(internal);
if std::env::var("DDC_DBG_FW").is_ok() && internal.contains("ImageDecoder") {
eprintln!("[fw] print_class_name input={internal} skip={shadow_skip}");
}
let cow = if shadow_skip {
std::borrow::Cow::Borrowed(internal)
} else {
crate::apply_class_rename(internal)
};
let internal: &str = &cow;
let reloc_orig: Option<&str> = if orig_internal != internal {
match crate::root_pkg_display() {
Some(rp)
if internal.len() > rp.len() + 1
&& internal.starts_with(rp.as_str())
&& internal.as_bytes()[rp.len()] == b'/'
&& &internal[rp.len() + 1..] == orig_internal
&& pool.get(orig_internal).is_some() =>
{
Some(orig_internal)
}
_ => None,
}
} else {
None
};
let pool_name: &str = if pool.get(internal).is_some() {
internal
} else {
reloc_orig.unwrap_or(internal)
};
if pool_name.contains('$') && pool.get(pool_name).is_some() {
if let Some(root) = emission_root(pool, pool_name) {
if root.contains('$') {
let below = &pool_name[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 root_cow = crate::apply_class_rename(&root);
let mut out = root_cow.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() || reloc_orig.is_some());
let tail_next = rest[i + 1..].chars().next();
let tail_ok = match tail_next {
Some(c) if c.is_ascii_alphabetic() || c == '_' => true,
Some(c) if !c.is_ascii_digit() && c != '$' && c != '/' && c != '-' => {
pool.get(internal).is_some()
}
_ => false,
};
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 fn sanitize_fq(dotted: &str) -> String {
dotted
.split('.')
.map(sanitize_fq_seg)
.collect::<Vec<_>>()
.join(".")
}
fn sanitize_fq_seg(seg: &str) -> String {
let plain = seg
.chars()
.all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
let mut out = if plain {
seg.to_string()
} else {
use std::fmt::Write as _;
let mut s = String::with_capacity(seg.len() + 8);
for c in seg.chars() {
if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
s.push(c);
} else {
s.push_str("_u");
let _ = write!(s, "{:x}", c as u32);
}
}
s
};
if is_java_keyword_name(&out)
|| is_restricted_type_name(&out)
|| out.chars().next().is_some_and(|c| c.is_ascii_digit())
{
out.insert(0, '_');
}
if out == "_" {
out = "__".to_string();
}
out
}
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;
}
#[cfg(test)]
mod sanitize_tests {
use super::sanitize_fq;
#[test]
fn distinct_obfuscated_names_stay_distinct() {
let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
let before = mapped.len();
mapped.sort();
mapped.dedup();
assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
assert!(
mapped.iter().all(|m| m.is_ascii()),
"sanitizer must stay ASCII: {mapped:?}"
);
assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
}
#[test]
fn output_is_a_legal_identifier() {
for (input, want) in [
("do", "_do"),
("_", "__"),
("0Xx", "_0Xx"),
("Collection$-EL", "Collection$_u2dEL"),
("᩻", "_u1a7b"),
("a", "a"),
("A$B", "A$B"),
("x_y", "x_y"),
] {
assert_eq!(sanitize_fq(input), want, "input {input:?}");
}
}
#[test]
fn lookalike_collision_is_known_and_bounded() {
assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
}
}