pub mod cfg;
pub mod cure_pass;
pub mod classdec;
pub mod fwdb;
pub use classdec::{sanitize_fq, ClassOptions};
pub mod ctx;
pub mod lift;
pub mod method;
pub mod passes;
pub mod platform;
mod refscan;
pub mod ssa_census;
use jdc_core::FxHashMap as HashMap;
use ddc_dex::annotations::{self, EncodedValue};
#[derive(Debug, Clone)]
pub enum StaticValue {
Int(i64),
Float(f32),
Double(f64),
Str(String),
Type(String),
Boolean(bool),
Null,
Field(String, String, String),
Other,
}
use ddc_dex::{ClassDef, DexFile};
use jdc_core::types::{
parse_field_descriptor, parse_method_descriptor, JavaType, MethodDescriptor,
};
pub mod access {
pub const ACC_PUBLIC: u32 = 0x1;
pub const ACC_PRIVATE: u32 = 0x2;
pub const ACC_PROTECTED: u32 = 0x4;
pub const ACC_STATIC: u32 = 0x8;
pub const ACC_FINAL: u32 = 0x10;
pub const ACC_SYNCHRONIZED: u32 = 0x20;
pub const ACC_BRIDGE: u32 = 0x40;
pub const ACC_VARARGS: u32 = 0x80;
pub const ACC_NATIVE: u32 = 0x100;
pub const ACC_INTERFACE: u32 = 0x200;
pub const ACC_ABSTRACT: u32 = 0x400;
pub const ACC_STRICT: u32 = 0x800;
pub const ACC_SYNTHETIC: u32 = 0x1000;
pub const ACC_ANNOTATION: u32 = 0x2000;
pub const ACC_ENUM: u32 = 0x4000;
pub const ACC_CONSTRUCTOR: u32 = 0x1_0000;
pub const ACC_DECLARED_SYNCHRONIZED: u32 = 0x2_0000;
}
#[derive(Debug, Clone)]
pub struct PoolField {
pub name: std::sync::Arc<str>,
pub desc: std::sync::Arc<str>,
pub access: u32,
pub is_static: bool,
}
#[derive(Debug, Clone)]
pub struct PoolMethod {
pub name: std::sync::Arc<str>,
pub desc: std::sync::Arc<str>,
pub access: u32,
pub code_off: u32,
pub debug_info_off: u32,
pub dex_idx: usize,
}
impl PoolMethod {
pub fn parsed_desc(&self) -> Option<MethodDescriptor> {
parse_method_descriptor(&self.desc)
}
pub fn is_static(&self) -> bool {
self.access & access::ACC_STATIC != 0
}
pub fn is_abstract_or_native(&self) -> bool {
self.access & (access::ACC_ABSTRACT | access::ACC_NATIVE) != 0
}
}
enum ClassEntry {
Eager(Box<PoolClass>),
Lazy {
at: (usize, usize),
pc: std::sync::OnceLock<Box<PoolClass>>,
},
NameOnly,
}
#[derive(Debug, Clone)]
pub struct PoolClass {
pub name: String,
pub access: u32,
pub super_name: Option<String>,
pub interfaces: Vec<String>,
pub source_file: Option<String>,
pub static_fields: Vec<PoolField>,
pub instance_fields: Vec<PoolField>,
pub direct_methods: Vec<PoolMethod>,
pub virtual_methods: Vec<PoolMethod>,
pub static_values: Vec<StaticValue>,
pub nesting: ResolvedNesting,
pub dex_idx: usize,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedNesting {
pub enclosing_class: Option<String>,
pub enclosing_method: Option<(String, String)>,
pub member_classes: Vec<String>,
}
impl PoolClass {
pub fn is_interface(&self) -> bool {
self.access & access::ACC_INTERFACE != 0
}
pub fn is_enum(&self) -> bool {
self.access & access::ACC_ENUM != 0
}
pub fn is_synthetic(&self) -> bool {
self.access & access::ACC_SYNTHETIC != 0
}
pub fn is_static_nested(&self) -> bool {
self.access & access::ACC_STATIC != 0
}
pub fn all_methods(&self) -> impl Iterator<Item = &PoolMethod> {
self.direct_methods
.iter()
.chain(self.virtual_methods.iter())
}
pub fn find_method(&self, name: &str, desc: &str) -> Option<&PoolMethod> {
self.all_methods()
.find(|m| &*m.name == name && &*m.desc == desc)
}
pub fn ctors_by_arity(&self, arity: usize) -> Vec<&PoolMethod> {
self.all_methods()
.filter(|m| &*m.name == "<init>")
.filter(|m| {
m.parsed_desc()
.map(|d| d.args.len() == arity)
.unwrap_or(false)
})
.collect()
}
pub fn field_flags_of(&self, name: &str) -> Option<u32> {
self.static_fields
.iter()
.chain(self.instance_fields.iter())
.find(|f| f.name.as_ref() == name)
.map(|f| f.access)
}
}
type FacadeTarget = (
String,
Option<
std::sync::Arc<(
jdc_core::FxHashSet<(String, String)>,
jdc_core::FxHashSet<String>,
)>,
>,
);
pub struct DexPool {
dexes: Vec<std::sync::Arc<DexFile>>,
pub dex_labels: Vec<String>,
classes: HashMap<String, ClassEntry>,
pub order: Vec<String>,
retire_counts: std::sync::Mutex<Vec<u64>>,
retire_armed: std::sync::atomic::AtomicBool,
outers: std::sync::OnceLock<HashMap<String, Option<String>>>,
children: std::sync::OnceLock<HashMap<String, Vec<String>>>,
ref_caches: Vec<DexRefCache>,
name_only_missed: std::sync::atomic::AtomicBool,
name_only_miss: std::sync::Mutex<Option<String>>,
accessor_code: std::sync::OnceLock<AccessorSnapshots>,
pkg_simples:
std::sync::OnceLock<jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>>>,
root_segs: std::sync::OnceLock<jdc_core::FxHashSet<String>>,
kotlin_facades: std::sync::OnceLock<jdc_core::FxHashMap<String, FacadeTarget>>,
ctor_refs: std::sync::OnceLock<jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>>,
}
type AccessorSnapshots = HashMap<(usize, u32), std::sync::Arc<[u8]>>;
pub(crate) struct DexRefCache {
proto_descs: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::MethodDescriptor>>]>,
type_names: Box<[std::sync::OnceLock<std::sync::Arc<str>>]>,
type_javas: Box<[std::sync::OnceLock<std::sync::Arc<jdc_core::types::JavaType>>]>,
}
impl DexRefCache {
fn new(protos: usize, types: usize) -> Self {
DexRefCache {
proto_descs: (0..protos).map(|_| std::sync::OnceLock::new()).collect(),
type_names: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
type_javas: (0..types).map(|_| std::sync::OnceLock::new()).collect(),
}
}
}
impl DexPool {
pub fn new() -> Self {
DexPool {
dexes: Vec::new(),
retire_armed: std::sync::atomic::AtomicBool::new(false),
dex_labels: Vec::new(),
classes: HashMap::default(),
order: Vec::new(),
retire_counts: std::sync::Mutex::new(Vec::new()),
outers: std::sync::OnceLock::new(),
children: std::sync::OnceLock::new(),
ref_caches: Vec::new(),
name_only_missed: std::sync::atomic::AtomicBool::new(false),
name_only_miss: std::sync::Mutex::new(None),
accessor_code: std::sync::OnceLock::new(),
pkg_simples: std::sync::OnceLock::new(),
root_segs: std::sync::OnceLock::new(),
kotlin_facades: std::sync::OnceLock::new(),
ctor_refs: std::sync::OnceLock::new(),
}
}
pub fn ctor_ref_sigs(&self, class: &str) -> Option<&jdc_core::FxHashSet<String>> {
self.ctor_refs
.get_or_init(|| {
let mut map: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
for dex in &self.dexes {
let n = dex.method_count();
for i in 0..n {
let m = dex.method(i as u32);
if dex.string(m.name_idx) != "<init>" {
continue;
}
let desc = dex.proto_desc(m.proto_idx);
let (Some(lo), Some(hi)) = (desc.find('('), desc.find(')')) else {
continue;
};
map.entry(dex.class_name(m.class_idx))
.or_default()
.insert(desc[lo + 1..hi].to_string());
}
}
map
})
.get(class)
}
pub fn proto_desc_parsed(
&self,
di: usize,
proto_idx: u32,
) -> std::sync::Arc<jdc_core::types::MethodDescriptor> {
let Some(dex) = self.dexes.get(di) else {
return std::sync::Arc::new(jdc_core::types::MethodDescriptor {
args: Vec::new(),
ret: jdc_core::types::JavaType::Void,
});
};
let build = || {
std::sync::Arc::new(parse_proto_desc(dex, proto_idx))
};
match self
.ref_caches
.get(di)
.and_then(|c| c.proto_descs.get(proto_idx as usize))
{
Some(slot) => slot.get_or_init(build).clone(),
None => build(),
}
}
pub fn type_name_arc(&self, di: usize, type_idx: u32) -> std::sync::Arc<str> {
let Some(dex) = self.dexes.get(di) else {
return std::sync::Arc::from("");
};
let build = || std::sync::Arc::from(dex.class_name(type_idx).as_str());
match self
.ref_caches
.get(di)
.and_then(|c| c.type_names.get(type_idx as usize))
{
Some(slot) => slot.get_or_init(build).clone(),
None => build(),
}
}
pub fn type_java(&self, di: usize, type_idx: u32) -> std::sync::Arc<jdc_core::types::JavaType> {
let Some(dex) = self.dexes.get(di) else {
return std::sync::Arc::new(jdc_core::types::JavaType::Object(
std::sync::Arc::from("java/lang/Object"),
));
};
let build = || std::sync::Arc::new(desc_type(dex.type_name(type_idx)));
match self
.ref_caches
.get(di)
.and_then(|c| c.type_javas.get(type_idx as usize))
{
Some(slot) => slot.get_or_init(build).clone(),
None => build(),
}
}
fn outer_map(&self) -> &HashMap<String, Option<String>> {
self.outers.get_or_init(|| {
self.order
.iter()
.map(|n| (n.clone(), find_outer_name(self, n)))
.collect()
})
}
pub fn outer_of(&self, name: &str) -> Option<&str> {
self.outer_map().get(name).and_then(|o| o.as_deref())
}
pub fn children_of(&self, internal: &str) -> &[String] {
if self.children.get().is_none() {
let mut idx: HashMap<String, Vec<String>> = HashMap::default();
for name in &self.order {
if let Some(outer) = self.outer_of(name).map(str::to_string) {
idx.entry(outer).or_default().push(name.clone());
}
}
let _ = self.children.set(idx);
}
self.children
.get()
.and_then(|m| m.get(internal))
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn add_dex(&mut self, dex: DexFile) -> usize {
let dex_idx = self.dexes.len();
self.classes.reserve(dex.class_defs.len());
for cd in &dex.class_defs {
let name = dex.class_name(cd.class_idx);
if self.classes.contains_key(&name) {
continue;
}
let pc = pool_class_of(&dex, dex.raw(), cd, dex_idx);
self.classes.insert(name.clone(), ClassEntry::Eager(Box::new(pc)));
self.order.push(name);
}
self.ref_caches
.push(DexRefCache::new(dex.proto_count(), dex.type_count()));
self.dexes.push(std::sync::Arc::new(dex));
self.retire_counts.lock().unwrap().push(0);
self.dex_labels.push(format!("dex {}", dex_idx));
dex_idx
}
pub fn add_names(&mut self, names: impl IntoIterator<Item = String>) {
for name in names {
if self.classes.contains_key(&name) {
continue;
}
self.classes.insert(name.clone(), ClassEntry::NameOnly);
self.order.push(name);
}
}
pub fn add_dex_lazy(&mut self, dex: DexFile) -> usize {
let dex_idx = self.dexes.len();
self.classes.reserve(dex.class_defs.len());
for (ci, cd) in dex.class_defs.iter().enumerate() {
let name = dex.class_name(cd.class_idx);
if self.classes.contains_key(&name) {
continue;
}
self.classes.insert(
name.clone(),
ClassEntry::Lazy {
at: (dex_idx, ci),
pc: std::sync::OnceLock::new(),
},
);
self.order.push(name);
}
self.ref_caches
.push(DexRefCache::new(dex.proto_count(), dex.type_count()));
self.dexes.push(std::sync::Arc::new(dex));
self.retire_counts.lock().unwrap().push(0);
self.dex_labels.push(format!("dex {}", dex_idx));
dex_idx
}
pub fn set_dex_label(&mut self, idx: usize, label: impl Into<String>) {
if let Some(slot) = self.dex_labels.get_mut(idx) {
*slot = label.into();
}
}
pub fn len(&self) -> usize {
self.classes.len()
}
pub fn is_empty(&self) -> bool {
self.classes.is_empty()
}
pub fn get(&self, internal: &str) -> Option<&PoolClass> {
if matches!(self.classes.get(internal), Some(ClassEntry::NameOnly)) {
self.name_only_missed
.store(true, std::sync::atomic::Ordering::Relaxed);
let mut slot = self.name_only_miss.lock().unwrap();
if slot.is_none() {
*slot = Some(internal.to_string());
}
return None;
}
self.get_inner(internal, None)
}
pub fn clear_name_only_miss(&self) {
self.name_only_missed
.store(false, std::sync::atomic::Ordering::Relaxed);
*self.name_only_miss.lock().unwrap() = None;
}
pub fn name_only_miss_name(&self) -> Option<String> {
self.name_only_miss.lock().unwrap().clone()
}
pub fn name_only_missed(&self) -> bool {
self.name_only_missed
.load(std::sync::atomic::Ordering::Relaxed)
}
fn get_inner(&self, internal: &str, raws: Option<&[&[u8]]>) -> Option<&PoolClass> {
match self.classes.get(internal)? {
ClassEntry::NameOnly => None,
ClassEntry::Eager(pc) => Some(&**pc),
ClassEntry::Lazy { at, pc } => {
if let Some(built) = pc.get() {
return Some(&**built);
}
let (di, ci) = *at;
let dex = self.dexes.get(di)?;
let cd = dex.class_defs.get(ci)?;
let raw = match raws {
Some(r) => r.get(di).copied().unwrap_or(&[]),
None => dex.raw(),
};
let built = Box::new(pool_class_of(dex, raw, cd, di));
Some(&**pc.get_or_init(move || built))
}
}
}
pub fn materialize_all(&self, threads: usize) {
let keys: Vec<String> = self
.classes
.iter()
.filter(|(_, e)| matches!(e, ClassEntry::Lazy { .. }))
.map(|(k, _)| k.clone())
.collect();
if keys.is_empty() {
return;
}
let raws: Vec<&[u8]> = self.dexes.iter().map(|d| d.raw()).collect();
let threads = threads.max(1);
let next = std::sync::atomic::AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..threads {
scope.spawn(|| loop {
let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Some(k) = keys.get(i) else { break };
let _ = self.get_inner(k, Some(&raws));
});
}
});
}
fn get_if_materialized(&self, internal: &str) -> Option<&PoolClass> {
match self.classes.get(internal)? {
ClassEntry::NameOnly => None,
ClassEntry::Eager(pc) => Some(&**pc),
ClassEntry::Lazy { pc, .. } => pc.get().map(|b| &**b),
}
}
pub fn has_name(&self, internal: &str) -> bool {
self.classes.contains_key(internal)
}
pub fn dex(&self, idx: usize) -> Option<std::sync::Arc<DexFile>> {
self.dexes.get(idx).cloned()
}
pub fn dex_count(&self) -> usize {
self.dexes.len()
}
pub fn dexes(&self) -> &[std::sync::Arc<ddc_dex::DexFile>] {
&self.dexes
}
pub fn root_pkg_segs(&self) -> &jdc_core::FxHashSet<String> {
self.root_segs.get_or_init(|| {
self.order
.iter()
.filter_map(|n| n.find('/').map(|i| n[..i].to_string()))
.collect()
})
}
pub fn package_simples(
&self,
) -> &jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>> {
self.pkg_simples.get_or_init(|| {
let mut m: jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>> =
jdc_core::FxHashMap::default();
let mut raw: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
for n in &self.order {
if let Some((pkg, simple)) = n.rsplit_once('/') {
raw.entry(pkg.to_string())
.or_default()
.insert(simple.to_string());
}
}
m.extend(raw.into_iter().map(|(k, v)| (k, std::sync::Arc::new(v))));
m
})
}
pub fn kotlin_facade_map(&self) -> &jdc_core::FxHashMap<String, FacadeTarget> {
self.kotlin_facades.get_or_init(|| {
let t_fb = std::time::Instant::now();
let mut m: jdc_core::FxHashMap<String, FacadeTarget> =
jdc_core::FxHashMap::default();
let mut npartial = 0usize;
for name in &self.order {
let Some(i) = name.find("__") else { continue };
let base = &name[..i];
if base.is_empty() {
continue;
}
let Some(basec) = self.get(base) else {
continue;
};
if basec.access & crate::access::ACC_PUBLIC == 0 {
continue;
}
let mut cur = basec.super_name.clone();
let mut hops = 0u32;
let mut reaches = false;
while let Some(c) = cur {
if c == *name {
reaches = true;
break;
}
if hops >= 8 {
break;
}
hops += 1;
cur = self.get(&c).and_then(|cc| cc.super_name.clone());
}
if reaches {
m.insert(name.clone(), (base.to_string(), None));
continue;
}
let Some(part) = self.get(name) else {
continue;
};
let base_methods: jdc_core::FxHashSet<(&str, &str)> = basec
.direct_methods
.iter()
.chain(basec.virtual_methods.iter())
.map(|mm| (&*mm.name, &*mm.desc))
.collect();
let base_fields: jdc_core::FxHashSet<&str> =
basec.static_fields.iter().map(|f| &*f.name).collect();
let mut methods: jdc_core::FxHashSet<(String, String)> =
jdc_core::FxHashSet::default();
let mut fields: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
let mut all = true;
for mm in part
.direct_methods
.iter()
.chain(part.virtual_methods.iter())
.filter(|mm| mm.access & crate::access::ACC_STATIC != 0)
{
if base_methods.contains(&(&*mm.name, &*mm.desc)) {
methods.insert((mm.name.to_string(), mm.desc.to_string()));
} else {
all = false;
}
}
for pf in &part.static_fields {
if base_fields.contains(&*pf.name) {
fields.insert(pf.name.to_string());
} else {
all = false;
}
}
if all {
m.insert(name.clone(), (base.to_string(), None));
} else if !methods.is_empty() || !fields.is_empty() {
npartial += 1;
m.insert(
name.clone(),
(base.to_string(), Some(std::sync::Arc::new((methods, fields)))),
);
}
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[renames] facade map: entries={} partial={} build={:?}",
m.len(),
npartial,
t_fb.elapsed()
);
}
m
})
}
pub fn class_names(&self) -> impl Iterator<Item = &str> {
self.order.iter().map(|s| s.as_str())
}
pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
if sub == sup {
return true;
}
if let (Some(sub_elem), Some(sup_elem)) = (array_elem(sub), array_elem(sup)) {
if sub_elem.starts_with('L')
&& sup_elem.starts_with('L')
&& sub_elem.ends_with(';')
&& sup_elem.ends_with(';')
{
return self.is_subtype(
&sub_elem[1..sub_elem.len() - 1],
&sup_elem[1..sup_elem.len() - 1],
);
}
if sub_elem.starts_with('[') && sup_elem.starts_with('[') {
return self.is_subtype(sub_elem, sup_elem);
}
return sub_elem == sup_elem;
}
if sup == "java/lang/Object" {
return !matches!(
sub,
"I" | "Z" | "B" | "S" | "C" | "J" | "F" | "D" | "V"
);
}
let mut cur = self.get(sub);
if cur.is_none() {
return crate::fwdb::is_subtype(sub, sup);
}
let mut hops = 0;
while let Some(c) = cur {
hops += 1;
if hops > 64 {
return false;
}
for i in &c.interfaces {
if i == sup || self.is_subtype(i, sup) {
return true;
}
}
match &c.super_name {
Some(s) if s == sup => return true,
Some(s) if s != "java/lang/Object" => {
match self.get(s) {
Some(sc) => cur = Some(sc),
None => return crate::fwdb::is_subtype(s, sup),
}
}
_ => return false,
}
}
false
}
}
impl Default for DexPool {
fn default() -> Self {
Self::new()
}
}
fn array_elem(desc: &str) -> Option<&str> {
desc.strip_prefix('[')
}
fn resolve_static_value(v: &EncodedValue, dex: &DexFile) -> StaticValue {
match v {
EncodedValue::Byte(x) => StaticValue::Int(*x as i64),
EncodedValue::Short(x) => StaticValue::Int(*x as i64),
EncodedValue::Char(x) => StaticValue::Int(*x as i64),
EncodedValue::Int(x) => StaticValue::Int(*x as i64),
EncodedValue::Long(x) => StaticValue::Int(*x),
EncodedValue::Float(x) => StaticValue::Float(*x),
EncodedValue::Double(x) => StaticValue::Double(*x),
EncodedValue::String(i) => StaticValue::Str(dex.string(*i).to_string()),
EncodedValue::Type(i) => StaticValue::Type(dex.class_name(*i)),
EncodedValue::Boolean(b) => StaticValue::Boolean(*b),
EncodedValue::Null => StaticValue::Null,
EncodedValue::Enum(i) | EncodedValue::Field(i) => {
let f = dex.field(*i);
StaticValue::Field(
dex.class_name(f.class_idx),
dex.string(f.name_idx).to_string(),
dex.type_name(f.type_idx).to_string(),
)
}
_ => StaticValue::Other,
}
}
fn pool_class_of(dex: &DexFile, raw: &[u8], cd: &ClassDef, dex_idx: usize) -> PoolClass {
let name = dex.class_name(cd.class_idx);
let super_name = if cd.superclass_idx == ddc_dex::NO_INDEX {
None
} else {
let s = dex.class_name(cd.superclass_idx);
if s.is_empty() || s == "java/lang/Object" {
None
} else {
Some(s)
}
};
let interfaces = dex
.interfaces_of(cd)
.into_iter()
.map(|t| dex.class_name(t))
.collect();
let source_file = if cd.source_file_idx == ddc_dex::NO_INDEX {
None
} else {
Some(dex.string(cd.source_file_idx).to_string())
};
let data = dex.class_data(cd);
let mk_field = |ef: &ddc_dex::EncodedField, is_static: bool| {
let f = dex.field(ef.field_idx);
PoolField {
name: dex.string_arc(f.name_idx),
desc: dex.type_name_arc(f.type_idx),
access: ef.access_flags,
is_static,
}
};
let static_fields: Vec<PoolField> = data
.static_fields
.iter()
.map(|f| mk_field(f, true))
.collect();
let instance_fields: Vec<PoolField> = data
.instance_fields
.iter()
.map(|f| mk_field(f, false))
.collect();
let mk_method = |em: &ddc_dex::EncodedMethod| {
let m = dex.method(em.method_idx);
let (code_off, debug_info_off) = match dex.debug_info_off_at(em.code_off) {
Some(d) => (em.code_off, d),
None => (0, 0),
};
PoolMethod {
name: dex.string_arc(m.name_idx),
desc: dex.proto_desc(m.proto_idx),
access: em.access_flags,
code_off,
debug_info_off,
dex_idx,
}
};
let direct_methods: Vec<PoolMethod> = data.direct_methods.iter().map(&mk_method).collect();
let virtual_methods: Vec<PoolMethod> = data.virtual_methods.iter().map(&mk_method).collect();
let static_values: Vec<StaticValue> = dex
.static_values(cd.static_values_off)
.into_iter()
.map(|v| resolve_static_value(&v, dex))
.collect();
let anns = if cd.annotations_off != 0 {
annotations::read_class_annotations(raw, cd.annotations_off)
} else {
Vec::new()
};
let raw = annotations::nesting_from(&anns, &|t: u32| dex.type_name(t).to_string());
let nesting = ResolvedNesting {
enclosing_class: raw.enclosing_class.map(|t| dex.class_name(t)),
enclosing_method: raw.enclosing_method.map(|m| {
let mid = dex.method(m);
(
dex.class_name(mid.class_idx),
dex.string(mid.name_idx).to_string(),
)
}),
member_classes: raw
.member_classes
.into_iter()
.map(|t| dex.class_name(t))
.collect(),
};
PoolClass {
name,
access: cd.access_flags,
super_name,
interfaces,
source_file,
static_fields,
instance_fields,
direct_methods,
virtual_methods,
static_values,
nesting,
dex_idx,
}
}
fn parse_proto_desc(
dex: &ddc_dex::DexFile,
proto_idx: u32,
) -> jdc_core::types::MethodDescriptor {
let proto = dex.proto(proto_idx);
let args = dex
.proto_params(proto_idx)
.iter()
.map(|&t| desc_type(dex.type_name(t)))
.collect();
jdc_core::types::MethodDescriptor {
args,
ret: desc_type(dex.type_name(proto.return_type_idx)),
}
}
pub fn desc_type(desc: &str) -> JavaType {
parse_field_descriptor(desc).unwrap_or(JavaType::Object("java/lang/Object".into()))
}
pub fn find_outer_name(pool: &DexPool, internal: &str) -> Option<String> {
if let Some(pc) = pool.get_if_materialized(internal) {
if let Some(enc) = &pc.nesting.enclosing_class {
if pool.has_name(enc) {
return Some(enc.clone());
}
}
}
let mut rest = internal;
while let Some(d) = rest.rfind('$') {
if rest.as_bytes().get(d + 1) == Some(&b'/') {
rest = &rest[..d];
continue;
}
let cand = &rest[..d];
if pool.has_name(cand) {
return Some(cand.to_string());
}
rest = cand;
}
None
}
fn clean_member_tail(rest: &str) -> bool {
if rest.starts_with('-') {
return false;
}
if rest.contains('/') {
return false;
}
if rest.starts_with("$ExternalSynthetic") {
return false;
}
let tail = rest.rsplit('$').next().unwrap_or(rest);
if tail.is_empty() || tail.starts_with(|c: char| c.is_ascii_digit()) {
return false;
}
true
}
impl DexPool {
pub fn snapshot_accessor_code(&self) {
let mut snap: AccessorSnapshots = HashMap::default();
let mut bytes_total = 0usize;
for name in &self.order {
let Some(pc) = self.get_if_materialized(name) else {
continue;
};
for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
if m.code_off == 0
|| !m.is_static()
|| m.access & crate::access::ACC_SYNTHETIC == 0
{
continue;
}
if snap.contains_key(&(m.dex_idx, m.code_off)) {
continue;
}
let Some(dex) = self.dex(m.dex_idx) else {
continue;
};
let raw = dex.raw();
let off = m.code_off as usize;
if off + 16 > raw.len() {
continue;
}
let u2 = |o: usize| u16::from_le_bytes([raw[o], raw[o + 1]]) as usize;
let u4 = |o: usize| {
u32::from_le_bytes([raw[o], raw[o + 1], raw[o + 2], raw[o + 3]]) as usize
};
let tries = u2(off + 6);
let insns = u4(off + 12);
let mut end = off + 16 + 2 * insns;
if tries > 0 {
end = end.next_multiple_of(4) + 8 * tries + 256 + 64 * tries;
}
let end = end.min(raw.len());
let slice: std::sync::Arc<[u8]> = raw[off..end].into();
bytes_total += slice.len();
snap.insert((m.dex_idx, m.code_off), slice);
}
}
if std::env::var("DDC_WALL").is_ok() {
eprintln!(
"[wall] accessor snapshot: {} entries, {} KB",
snap.len(),
bytes_total / 1024
);
}
let _ = self.accessor_code.set(snap);
}
pub fn accessor_code(&self, dex_idx: usize, code_off: u32) -> Option<std::sync::Arc<[u8]>> {
self.accessor_code.get()?.get(&(dex_idx, code_off)).cloned()
}
pub fn arm_retirement(&self) {
self.retire_armed
.store(true, std::sync::atomic::Ordering::Release);
let mut counts = self.retire_counts.lock().unwrap();
for c in counts.iter_mut() {
*c = 0;
}
for entry in self.classes.values() {
let di = match entry {
ClassEntry::Eager(pc) => pc.dex_idx,
ClassEntry::Lazy { at, .. } => at.0,
ClassEntry::NameOnly => continue,
};
if let Some(c) = counts.get_mut(di) {
*c += 1;
}
}
}
pub fn report_class_done(&self, class_name: &str) -> Option<usize> {
if !self.retire_armed.load(std::sync::atomic::Ordering::Acquire) {
return None;
}
let entry = self.classes.get(class_name)?;
let di = match entry {
ClassEntry::Eager(pc) => pc.dex_idx,
ClassEntry::Lazy { at, .. } => at.0,
ClassEntry::NameOnly => return None,
};
let mut counts = self.retire_counts.lock().unwrap();
match counts.get_mut(di) {
Some(c) => {
*c = c.saturating_sub(1);
if *c == 0 {
Some(di)
} else {
None
}
}
None => None,
}
}
pub fn release_images(&self, indexes: &[usize]) {
let mut counts = self.retire_counts.lock().unwrap();
for &i in indexes {
if let Some(dex) = self.dexes.get(i) {
dex.mark_released();
}
if let Some(c) = counts.get_mut(i) {
*c = u64::MAX; }
}
}
}
pub fn top_level_classes(pool: &DexPool) -> Vec<String> {
let lazy = !pool_majority_materialized(pool);
pool.class_names()
.filter(|name| {
let outer = if lazy {
find_outer_name(pool, name)
} else {
pool.outer_of(name).map(str::to_string)
};
let Some(outer) = outer else {
return true;
};
if !pool.has_name(&outer) {
return true;
}
let rest = name
.strip_prefix(outer.as_str())
.and_then(|t| t.strip_prefix('$'))
.unwrap_or("");
!clean_member_tail(rest)
})
.map(|n| n.to_string())
.collect()
}
pub fn sanitize_seg(seg: &str) -> String {
classdec::sanitize_fq(seg)
}
pub fn sanitize_internal(internal: &str) -> String {
internal
.split('/')
.map(classdec::sanitize_fq)
.collect::<Vec<_>>()
.join("/")
}
pub fn case_rename_map(pool: &DexPool) -> HashMap<String, String> {
use std::collections::HashMap;
let mut groups: HashMap<String, Vec<String>> = HashMap::default();
for t in top_level_classes(pool) {
groups.entry(t.to_lowercase()).or_default().push(t);
}
for v in groups.values_mut() {
v.sort();
}
let mut map = HashMap::default();
for members in groups.values() {
for (i, m) in members.iter().enumerate() {
if i == 0 {
if m.contains('$') {
map.insert(m.clone(), m.clone());
}
continue;
}
let cut = m.rfind('/').map(|x| x + 1).unwrap_or(0);
let mut n = i + 1;
loop {
let cand = format!("{}{}_{}", &m[..cut], &m[cut..], n);
let fold = cand.to_lowercase();
if !groups.contains_key(&fold) {
map.insert(m.clone(), cand);
break;
}
n += 1;
}
}
}
map
}
fn display_of(name: &str, map: &HashMap<String, String>) -> String {
if let Some(d) = map.get(name) {
return d.clone();
}
let mut s = name;
while let Some(i) = s.rfind('$') {
s = &s[..i];
match map.get(s) {
Some(d) if d.as_str() != s => return format!("{d}{}", &name[s.len()..]),
Some(_) => return name.to_string(),
None => continue,
}
}
map.get(name).cloned().unwrap_or_else(|| name.to_string())
}
fn nested_collision_renames(
pool: &DexPool,
map: &mut HashMap<String, String>,
fam_segs: &SegMap,
) {
let mut names: Vec<&String> = pool.order.iter().collect();
names.sort_by_key(|n| n.matches('$').count());
let mut assigned: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut pkg_segments: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
for n in &pool.order {
if let Some((seg, _)) = n.split_once('/') {
pkg_segments.insert(seg.to_string());
}
}
let local_ok: jdc_core::FxHashSet<String> = if pool_majority_materialized(pool) {
refscan::local_ok(&pool.dexes, &field_clash_cands(pool, map))
} else {
jdc_core::FxHashSet::default()
};
let mut taken_sibling_displays: jdc_core::FxHashSet<String> =
map.values().cloned().collect();
let mut body_cands: Vec<(String, String, String)> = Vec::new();
for name in names {
if !name.contains('$') || map.contains_key(name) {
continue;
}
let Some(parent) = find_outer_name(pool, name) else {
continue;
};
let Some(rest) = name
.strip_prefix(parent.as_str())
.and_then(|t| t.strip_prefix('$'))
else {
continue;
};
if !clean_member_tail(rest) {
continue;
}
let mut chain: Vec<String> = Vec::new();
let mut cur = parent.clone();
loop {
let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
let simple = disp.rsplit('/').next().unwrap_or(&disp);
chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
match find_outer_name(pool, &cur) {
Some(o) if o != cur => cur = o,
_ => break,
}
}
let root: &str = &cur;
let disp_parent = display_of(&parent, map);
let tail = rest.rsplit('$').next().unwrap_or(rest);
let orphan = rest.contains('$');
let field_clash = local_ok.contains(name.as_str());
let pkg_shadow = fam_segs
.get(root)
.is_some_and(|segs| segs.contains(tail));
let clash = chain.iter().any(|c| c == tail) || field_clash || pkg_shadow;
if !clash && pkg_segments.contains(tail) {
body_cands.push((name.clone(), root.to_string(), tail.to_string()));
}
if !orphan && !clash {
continue;
}
let mut enc_fields: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
if field_clash || pkg_shadow {
if let Some(pc) = pool.get(&parent) {
for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
}
}
}
let mut k = 0u32;
loop {
k += 1;
let cand_tail = if k == 1 {
tail.to_string()
} else {
format!("{tail}{k}")
};
if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
continue;
}
if field_clash || pkg_shadow {
if pkg_segments.contains(&cand_tail) {
continue;
}
if pkg_shadow
&& fam_segs
.get(root)
.is_some_and(|segs| segs.contains(cand_tail.as_str()))
{
continue;
}
if let Some((root_pkg, _)) = root.rsplit_once('/') {
let sib = format!("{root_pkg}/{cand_tail}");
if pool.has_name(&sib) || taken_sibling_displays.contains(&sib)
{
continue;
}
taken_sibling_displays.insert(sib);
}
}
let cand = format!("{disp_parent}${cand_tail}");
if pool.has_name(&cand) || assigned.contains(&cand) {
continue;
}
assigned.insert(cand.clone());
map.insert(name.clone(), cand);
break;
}
}
let mut body_renamed = 0usize;
if !body_cands.is_empty() && pool_majority_materialized(pool) {
let roots: jdc_core::FxHashSet<&str> =
body_cands.iter().map(|(_, r, _)| r.as_str()).collect();
let mut members: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
for n in &pool.order {
let r = match n.find('$') {
Some(i) => &n[..i],
None => n.as_str(),
};
if roots.contains(r) {
members.insert(n.clone());
}
}
let t0 = std::time::Instant::now();
let (by_referrer, _) = refscan::family_ref_segments(&pool.dexes, &members);
let mut root_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
jdc_core::FxHashMap::default();
for (referrer, segs) in &by_referrer {
let r = referrer.split('$').next().unwrap_or(referrer);
root_segs.entry(r).or_default().extend(segs.iter().map(|s| s.as_str()));
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[renames] nested body-shadow scan: cands={} families={} scan={:?}",
body_cands.len(),
root_segs.len(),
t0.elapsed()
);
}
for (name, root, tail) in &body_cands {
if !root_segs
.get(root.as_str())
.is_some_and(|s| s.contains(tail.as_str()))
{
continue;
}
if let Some(d) = map.get(name) {
let cur_tail = d.rsplit(['/', '$']).next().unwrap_or("");
if cur_tail != tail.as_str() {
continue;
}
}
let Some(parent) = find_outer_name(pool, name) else {
continue;
};
let Some(rest) = name
.strip_prefix(parent.as_str())
.and_then(|t| t.strip_prefix('$'))
else {
continue;
};
if !clean_member_tail(rest) {
continue;
}
let mut chain: Vec<String> = Vec::new();
let mut cur = parent.clone();
loop {
let disp = map.get(&cur).cloned().unwrap_or_else(|| cur.clone());
let simple = disp.rsplit('/').next().unwrap_or(&disp);
chain.push(simple.rsplit('$').next().unwrap_or(simple).to_string());
match find_outer_name(pool, &cur) {
Some(o) if o != cur => cur = o,
_ => break,
}
}
let disp_parent = display_of(&parent, map);
let mut enc_fields: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
if let Some(pc) = pool.get(&parent) {
for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
enc_fields.insert(crate::classdec::java_ident(&f.name).into_owned());
}
}
let mut k = 0u32;
loop {
k += 1;
let cand_tail = if k == 1 {
tail.clone()
} else {
format!("{tail}{k}")
};
if chain.contains(&cand_tail) || enc_fields.contains(&cand_tail) {
continue;
}
if pkg_segments.contains(&cand_tail) {
continue;
}
if fam_segs
.get(root.as_str())
.is_some_and(|segs| segs.contains(cand_tail.as_str()))
{
continue;
}
if root_segs
.get(root.as_str())
.is_some_and(|segs| segs.contains(cand_tail.as_str()))
{
continue;
}
if let Some((root_pkg, _)) = root.rsplit_once('/') {
let sib = format!("{root_pkg}/{cand_tail}");
if pool.has_name(&sib) || taken_sibling_displays.contains(&sib) {
continue;
}
taken_sibling_displays.insert(sib);
}
let cand = format!("{disp_parent}${cand_tail}");
if pool.has_name(&cand) || assigned.contains(&cand) {
continue;
}
let old_disp = format!("{disp_parent}${tail}");
if old_disp != cand {
for (mk, mv) in map.iter_mut() {
if mk == name {
continue;
}
if let Some(rest) = mv.strip_prefix(old_disp.as_str()) {
if rest.starts_with('$') {
let repaired = format!("{cand}{rest}");
assigned.insert(repaired.clone());
*mv = repaired;
}
}
}
}
assigned.insert(cand.clone());
map.insert(name.clone(), cand);
body_renamed += 1;
break;
}
}
}
if body_renamed > 0 && std::env::var("DDC_STATS").is_ok() {
eprintln!("[renames] nested body-shadow renames: {body_renamed}");
}
}
fn field_clash_cands(pool: &DexPool, map: &HashMap<String, String>) -> HashMap<String, String> {
let mut out: HashMap<String, String> = HashMap::default();
for name in &pool.order {
if !name.contains('$') || map.contains_key(name) {
continue;
}
let Some(parent) = find_outer_name(pool, name) else {
continue;
};
let Some(rest) = name
.strip_prefix(parent.as_str())
.and_then(|t| t.strip_prefix('$'))
else {
continue;
};
if !clean_member_tail(rest) {
continue;
}
let tail = rest.rsplit('$').next().unwrap_or(rest);
let Some(pc) = pool.get(&parent) else {
continue;
};
let clash = pc
.static_fields
.iter()
.chain(pc.instance_fields.iter())
.any(|f| crate::classdec::java_ident(&f.name).as_ref() == tail);
if !clash {
continue;
}
let mut root = parent.clone();
loop {
match find_outer_name(pool, &root) {
Some(o) if o != root => root = o,
_ => break,
}
}
out.insert(name.clone(), root);
}
out
}
fn pool_majority_materialized(pool: &DexPool) -> bool {
let (mut mat, mut lazy) = (0usize, 0usize);
for e in pool.classes.values() {
match e {
ClassEntry::Eager(_) => mat += 1,
ClassEntry::Lazy { pc, .. } => {
if pc.get().is_some() {
mat += 1;
} else {
lazy += 1;
}
}
ClassEntry::NameOnly => lazy += 1,
}
}
lazy == 0 || mat >= lazy
}
#[cfg(target_vendor = "apple")]
fn current_rss_mb() -> f64 {
#[repr(C)]
struct TaskBasicInfo {
virtual_size: u64,
resident_size: u64,
resident_size_max: u64,
total_user_time: u64,
total_sys_time: u64,
policy: i32,
suspend_count: i32,
}
extern "C" {
fn mach_task_self() -> u32;
fn task_info(
target_port: u32,
flavor: i32,
task_info_out: *mut TaskBasicInfo,
task_info_out_cnt: *mut u32,
) -> i32;
}
let mut info = TaskBasicInfo {
virtual_size: 0,
resident_size: 0,
resident_size_max: 0,
total_user_time: 0,
total_sys_time: 0,
policy: 0,
suspend_count: 0,
};
let mut cnt = (std::mem::size_of::<TaskBasicInfo>() / 4) as u32;
let kr = unsafe { task_info(mach_task_self(), 20, &mut info, &mut cnt) };
if kr == 0 {
info.resident_size as f64 / (1024.0 * 1024.0)
} else {
0.0
}
}
#[cfg(not(target_vendor = "apple"))]
fn current_rss_mb() -> f64 {
0.0
}
#[cfg(unix)]
fn peak_rss_mb() -> f64 {
#[repr(C)]
struct Rusage {
pad: [i64; 4],
maxrss: i64,
tail: [i64; 16],
}
extern "C" {
fn getrusage(who: i32, r: *mut Rusage) -> i32;
}
let mut r = Rusage {
pad: [0; 4],
maxrss: 0,
tail: [0; 16],
};
unsafe { getrusage(0, &mut r) };
#[cfg(target_vendor = "apple")]
let scale = 1024.0 * 1024.0;
#[cfg(not(target_vendor = "apple"))]
let scale = 1024.0;
r.maxrss as f64 / scale
}
#[cfg(not(unix))]
fn peak_rss_mb() -> f64 {
0.0
}
pub(crate) fn stats_rss(phase: &str) {
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[rss] {phase}: peak {:.0} MB, live {:.0} MB",
peak_rss_mb(),
current_rss_mb()
);
}
}
pub fn install_case_renames(pool: &DexPool) {
let dbg = std::env::var("DDC_DBG_REN").is_ok();
let mut tick = std::time::Instant::now();
let mut lap = |tag: &str| {
if dbg {
eprintln!("[ren] {:?} {}", tick.elapsed(), tag);
tick = std::time::Instant::now();
}
};
let mut map = case_rename_map(pool);
lap("case_rename_map");
crate::stats_rss(" case_rename_map");
let (pkg_segs, fam_segs) = if pool_majority_materialized(pool) {
ref_segments(pool)
} else {
(
jdc_core::FxHashMap::default(),
jdc_core::FxHashMap::default(),
)
};
crate::stats_rss(" ref_segments");
lap("ref_segments");
pkg_leaf_shadow_renames(pool, &mut map);
lap("pkg_leaf_shadow");
class_pkg_collision_renames(pool, &mut map);
lap("class_pkg_collision");
obscuring_class_renames(pool, &mut map, &pkg_segs);
lap("obscuring");
nested_collision_renames(pool, &mut map, &fam_segs);
lap("nested_collision");
crate::stats_rss(" shadow+collision+obscuring+nested rules");
lap("rules");
lossy_sanitize_renames(pool, &mut map);
root_pkg_relocation(pool, &mut map);
fw_shadow_relocation(pool, &mut map);
crate::stats_rss(" lossy+root-pkg rules");
lap("lossy+root");
jdc_core::rename::set_class_renames(map);
crate::stats_rss("class-renames installed");
lap("set_class_renames");
if pool_majority_materialized(pool) {
std::thread::scope(|s| {
let h = s.spawn(|| crate::classdec::install_access_widening(pool));
jdc_core::rename::set_field_renames(combined_field_renames(pool));
let _ = h.join();
});
crate::stats_rss("field-renames + widening installed");
lap("field+widen");
} else {
jdc_core::rename::set_field_renames(combined_field_renames(pool));
lap("field_renames(lazy)");
}
}
type SegMap<'a> = jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>;
fn ref_segments(pool: &DexPool) -> (SegMap<'_>, SegMap<'_>) {
fn pkg_of(n: &str) -> &str {
match n.rsplit_once('/') {
Some((p, _)) => p,
None => "",
}
}
fn note<'a>(
pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
pkg: &'a str,
fam: &'a str,
t: &'a str,
) {
if !t.contains('/') {
return; }
let _ = pkg; let seg = t.split('/').next().unwrap_or("");
if !seg.is_empty() {
pkg_segs.entry(pkg).or_default().insert(seg);
fam_segs.entry(fam).or_default().insert(seg);
}
}
fn scan<'a>(
pkg_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
fam_segs: &mut jdc_core::FxHashMap<&'a str, jdc_core::FxHashSet<&'a str>>,
pkg: &'a str,
fam: &'a str,
desc: &'a str,
) {
let b = desc.as_bytes();
let mut i = 0usize;
while i < b.len() {
if b[i] == b'L' {
if let Some(end) = desc[i + 1..].find(';') {
note(pkg_segs, fam_segs, pkg, fam, &desc[i + 1..i + 1 + end]);
i += end + 2;
continue;
}
}
i += 1;
}
}
let mut pkg_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
jdc_core::FxHashMap::default();
let mut fam_segs: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<&str>> =
jdc_core::FxHashMap::default();
for name in &pool.order {
let Some(c) = pool.get(name) else { continue };
let pkg = pkg_of(name);
let fam = match name.find('$') {
Some(i) => &name[..i],
None => name.as_str(),
};
if let Some(sup) = &c.super_name {
note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
}
for sup in c.interfaces.iter() {
note(&mut pkg_segs, &mut fam_segs, pkg, fam, sup);
}
for f in c.static_fields.iter().chain(c.instance_fields.iter()) {
scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &f.desc);
}
for m in c.direct_methods.iter().chain(c.virtual_methods.iter()) {
scan(&mut pkg_segs, &mut fam_segs, pkg, fam, &m.desc);
}
}
(pkg_segs, fam_segs)
}
fn obscuring_class_renames(
pool: &DexPool,
map: &mut HashMap<String, String>,
pkg_segs: &SegMap,
) {
fn pkg_of(n: &str) -> &str {
match n.rsplit_once('/') {
Some((p, _)) => p,
None => "",
}
}
let mut root_segs: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
for n in &pool.order {
if let Some(i) = n.find('/') {
root_segs.insert(&n[..i]);
}
}
let mut cands: Vec<&String> = Vec::new();
for name in &pool.order {
let simple = name.rsplit('/').next().unwrap_or(name);
let pkg = pkg_of(name);
if simple.is_empty() || simple.contains('$') || pkg.is_empty() {
continue;
}
if !root_segs.contains(simple) {
continue;
}
if map.get(name).is_some_and(|v| v != name) {
continue; }
cands.push(name);
}
let ncands = cands.len();
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[renames] obscuring candidates={ncands} pkgs-with-segs={}",
pkg_segs.len()
);
}
if cands.is_empty() {
return;
}
let mut sorted: Vec<&String> = pool.order.iter().collect();
sorted.sort_unstable();
let prefix_free = |cand: &str| -> bool {
let idx = sorted.partition_point(|n| n.as_str() < cand);
!sorted[idx..].first().is_some_and(|n| n.starts_with(cand))
};
let pkg_simples = pool.package_simples();
let mut taken_displays: jdc_core::FxHashSet<String> = map.values().cloned().collect();
let mut renamed = 0usize;
let mut stage2: Vec<&String> = Vec::new();
for name in cands {
let pkg = pkg_of(name);
let simple = name.rsplit('/').next().unwrap_or(name);
if !pkg_segs.get(pkg).is_some_and(|segs| segs.contains(simple)) {
stage2.push(name);
continue;
}
let siblings = pkg_simples.get(pkg);
let mut k = 1u32;
loop {
k += 1;
let new_simple = format!("{simple}{k}");
let cand = if pkg.is_empty() {
new_simple.clone()
} else {
format!("{pkg}/{new_simple}")
};
let clash = !prefix_free(&cand)
|| taken_displays.contains(&cand)
|| pkg_segs
.get(pkg)
.is_some_and(|segs| segs.contains(new_simple.as_str()))
|| siblings.is_some_and(|s| {
s.iter()
.any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
});
if !clash {
taken_displays.insert(cand.clone());
map.insert(name.clone(), cand);
renamed += 1;
break;
}
}
}
let mut body_renamed = 0usize;
if !stage2.is_empty() && pool_majority_materialized(pool) {
let stage2_pkgs: jdc_core::FxHashSet<String> =
stage2.iter().map(|n| pkg_of(n).to_string()).collect();
let body_segs = crate::refscan::body_ref_segments(&pool.dexes, &stage2_pkgs);
for name in stage2 {
let pkg = pkg_of(name);
let simple = name.rsplit('/').next().unwrap_or(name);
if !body_segs
.get(pkg)
.is_some_and(|segs| segs.contains(simple))
{
continue;
}
let siblings = pkg_simples.get(pkg);
let mut k = 1u32;
loop {
k += 1;
let new_simple = format!("{simple}{k}");
let cand = format!("{pkg}/{new_simple}");
let clash = !prefix_free(&cand)
|| taken_displays.contains(&cand)
|| pkg_segs
.get(pkg)
.is_some_and(|segs| segs.contains(new_simple.as_str()))
|| body_segs
.get(pkg)
.is_some_and(|segs| segs.contains(new_simple.as_str()))
|| siblings.is_some_and(|s| {
s.iter()
.any(|p| p.eq_ignore_ascii_case(&new_simple) && *p != simple)
});
if !clash {
taken_displays.insert(cand.clone());
map.insert(name.clone(), cand);
renamed += 1;
body_renamed += 1;
break;
}
}
}
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[renames] obscuring-class renames: {renamed} (body-gated {body_renamed}; pkgs-with-segs={}, cands={ncands})",
pkg_segs.len()
);
}
}
fn lossy_sanitize_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
let names = top_level_classes(pool);
fn key_of(map: &HashMap<String, String>, n: &str) -> String {
let disp = map.get(n).map(|s| s.as_str()).unwrap_or(n);
sanitize_internal(disp)
}
let mut counts: HashMap<String, u32> = HashMap::default();
for n in &names {
*counts.entry(key_of(map, n)).or_insert(0) += 1;
}
if counts.values().all(|c| *c == 1) {
return;
}
let mut taken: jdc_core::FxHashSet<String> = pool
.order
.iter()
.map(|n| sanitize_internal(map.get(n).map(|s| s.as_str()).unwrap_or(n)))
.collect();
let mut sorted: Vec<&String> = names.iter().collect();
sorted.sort();
let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
let mut renamed = 0usize;
for n in sorted {
let key = key_of(map, n);
if counts.get(&key).copied().unwrap_or(0) < 2 {
continue;
}
if seen.insert(key.clone()) {
continue; }
let (pkg, simple) = match key.rsplit_once('/') {
Some((p, s)) => (p, s),
None => ("", key.as_str()),
};
let mut i = 1u32;
loop {
i += 1;
let cand = if pkg.is_empty() {
format!("{simple}_{i}")
} else {
format!("{pkg}/{simple}_{i}")
};
if !taken.insert(cand.clone()) {
continue; }
map.insert(n.clone(), cand);
renamed += 1;
break;
}
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!("[renames] lossy-sanitize collisions renamed={renamed}");
}
}
fn class_pkg_collision_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
let mut pkgs: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
for n in &pool.order {
let mut rest = n.as_str();
while let Some(i) = rest.rfind('/') {
rest = &rest[..i];
pkgs.insert(rest);
}
}
let taken: jdc_core::FxHashSet<&str> = pool.order.iter().map(|s| s.as_str()).collect();
let mut used: jdc_core::FxHashSet<String> = map.values().cloned().collect();
for name in &pool.order {
let Some((pkg, simple)) = name.rsplit_once('/') else {
continue;
};
if simple.is_empty() || pkg.is_empty() || simple.contains('$') {
continue;
}
if !pkgs.contains(name.as_str()) || map.get(name).is_some_and(|v| v != name) {
continue; }
let mut k = 1u32;
loop {
k += 1;
let cand = format!("{pkg}/{simple}{k}");
let clash = pkgs.contains(cand.as_str())
|| taken.contains(cand.as_str())
|| used.contains(&cand);
if !clash {
used.insert(cand.clone());
map.insert(name.clone(), cand);
break;
}
}
}
}
fn pkg_leaf_shadow_renames(pool: &DexPool, map: &mut HashMap<String, String>) {
let taken: jdc_core::FxHashSet<&str> = pool.order.iter().map(|s| s.as_str()).collect();
let mut used: jdc_core::FxHashSet<String> = map.values().cloned().collect();
let mut under_pkg: jdc_core::FxHashMap<&str, usize> = jdc_core::FxHashMap::default();
for n in &pool.order {
if let Some(i) = n.rfind('/') {
*under_pkg.entry(&n[..i]).or_default() += 1;
}
}
for name in &pool.order {
let Some((pkg, simple)) = name.rsplit_once('/') else {
continue;
};
if simple.is_empty() || pkg.is_empty() {
continue;
}
let leaf = pkg.rsplit('/').next().unwrap_or(pkg);
if simple != leaf {
continue;
}
let siblings = under_pkg.get(pkg).copied().unwrap_or(0).saturating_sub(1);
if siblings == 0 || map.get(name).is_some_and(|v| v != name) {
continue; }
let mut k = 1u32;
loop {
k += 1;
let cand = format!("{pkg}/{simple}{k}");
if !taken.contains(cand.as_str()) && !used.contains(&cand) {
used.insert(cand.clone());
map.insert(name.clone(), cand);
break;
}
}
}
}
fn ret_satisfies(pool: &DexPool, m_ret: &str, r: &str) -> bool {
if m_ret == r {
return true;
}
if m_ret.starts_with('[') {
let sup: std::borrow::Cow<str> = if r.starts_with('[') {
std::borrow::Cow::Borrowed(r)
} else {
match r.strip_prefix('L').and_then(|x| x.strip_suffix(';')) {
Some(rc) => std::borrow::Cow::Borrowed(rc),
None => return false, }
};
return pool.is_subtype(m_ret, &sup);
}
if r.starts_with('[') {
return false; }
match (
r.strip_prefix('L').and_then(|x| x.strip_suffix(';')),
m_ret.strip_prefix('L').and_then(|x| x.strip_suffix(';')),
) {
(Some(rc), Some(mc)) => pool.is_subtype(mc, rc),
_ => false,
}
}
type AncMethods = jdc_core::FxHashMap<
std::sync::Arc<str>,
Vec<(std::sync::Arc<str>, std::sync::Arc<str>)>,
>;
type AncLayers = Vec<std::sync::Arc<AncEntry>>;
struct AncEntry {
methods: AncMethods,
is_fw: bool,
supers: Vec<std::sync::Arc<str>>,
}
fn parse_anc_entry(pool: &DexPool, cn: &str) -> std::sync::Arc<AncEntry> {
let mut methods: AncMethods = jdc_core::FxHashMap::default();
let mut supers: Vec<std::sync::Arc<str>> = Vec::new();
let is_fw;
if let Some(ac) = pool.get_if_materialized(cn) {
is_fw = cn.starts_with("java/") || cn.starts_with("javax/");
for m in ac.all_methods() {
if m.is_static() || &*m.name == "<init>" || &*m.name == "<clinit>"
{
continue;
}
let d: &str = &m.desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
methods
.entry(m.name.clone())
.or_default()
.push((
std::sync::Arc::from(&d[lo..hi]),
std::sync::Arc::from(&d[hi + 1..]),
));
}
if let Some(s) = &ac.super_name {
if s != "java/lang/Object" {
supers.push(std::sync::Arc::from(s.as_str()));
}
}
supers.extend(ac.interfaces.iter().map(|i| std::sync::Arc::from(i.as_str())));
} else {
is_fw = true;
crate::fwdb::for_each_method(cn, |d, f| {
if f & crate::fwdb::MF_STATIC != 0 {
return;
}
let Some(open) = d.find('(') else {
return;
};
let lo = open + 1;
let hi = d.find(')').unwrap_or(0);
if hi == 0 || hi + 1 >= d.len() {
return;
}
methods
.entry(std::sync::Arc::from(&d[..open]))
.or_default()
.push((
std::sync::Arc::from(&d[lo..hi]),
std::sync::Arc::from(&d[hi + 1..]),
));
});
if let Some(sup) = crate::fwdb::super_of(cn) {
if sup != "java/lang/Object" {
supers.push(std::sync::Arc::from(sup));
}
}
crate::fwdb::for_each_interface(cn, |i| supers.push(std::sync::Arc::from(i)));
}
std::sync::Arc::new(AncEntry {
methods,
is_fw,
supers,
})
}
fn layer_rets<'a>(
layers: &'a [std::sync::Arc<AncEntry>],
name: &'a str,
argsig: &'a str,
) -> impl Iterator<Item = &'a str> {
layers
.iter()
.filter_map(move |e| e.methods.get(name))
.flatten()
.filter(move |(a, _)| &**a == argsig)
.map(|(_, r)| &**r)
}
fn member_collision_renames(
pool: &DexPool,
) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
HashMap::default();
let mut child_tails: jdc_core::FxHashMap<&str, Vec<&str>> =
jdc_core::FxHashMap::default();
let root_segs = pool.root_pkg_segs();
let lazy = !pool_majority_materialized(pool);
let mut pkg_displays: jdc_core::FxHashMap<&str, jdc_core::FxHashSet<String>> =
jdc_core::FxHashMap::default();
for n in &pool.order {
if lazy && pool.get_if_materialized(n).is_none() {
continue;
}
let renamed = crate::apply_class_rename(n);
let (pkg, simple) = match renamed.rfind('/') {
Some(i) => (renamed[..i].to_string(), renamed[i + 1..].to_string()),
None => (String::new(), renamed.to_string()),
};
let head = simple.split('$').next().unwrap_or(&simple).to_string();
if !head.is_empty() {
let pkg_key = n.rfind('/').map(|i| &n[..i]).unwrap_or("");
let _ = pkg;
pkg_displays.entry(pkg_key).or_default().insert(head);
}
}
let mut subs: jdc_core::FxHashMap<&str, Vec<&str>> = jdc_core::FxHashMap::default();
for n in &pool.order {
if lazy && pool.get_if_materialized(n).is_none() {
continue;
}
for (i, _) in n.match_indices('$') {
let owner = &n[..i];
let rest = &n[i + 1..];
let tail = match rest.find('$') {
Some(j) => &rest[..j],
None => rest,
};
if owner.is_empty() || tail.is_empty() {
continue;
}
let v = child_tails.entry(owner).or_default();
if !v.contains(&tail) {
v.push(tail);
}
}
if let Some(pc) = pool.get_if_materialized(n) {
if let Some(sup) = &pc.super_name {
if sup != "java/lang/Object" {
subs.entry(sup.as_str()).or_default().push(n.as_str());
}
}
}
}
let mut anc_cache: jdc_core::FxHashMap<
std::sync::Arc<str>,
std::sync::Arc<AncEntry>,
> = jdc_core::FxHashMap::default();
for name in &pool.order {
let Some(pc) = pool.get_if_materialized(name) else {
continue;
};
let outer_ref_desc = find_outer_name(pool, name)
.filter(|o| o != name)
.map(|o| format!("L{o};"));
let fields: Vec<&PoolField> = pc
.static_fields
.iter()
.chain(pc.instance_fields.iter())
.collect();
let fgroups: HashMap<String, Vec<&PoolField>> = HashMap::default();
let mut fgroups = fgroups;
for f in &fields {
let disp = crate::classdec::java_ident(&f.name).into_owned();
fgroups.entry(disp).or_default().push(f);
}
let mut f_taken: jdc_core::FxHashSet<String> = fields
.iter()
.map(|f| crate::classdec::java_ident(&f.name).into_owned())
.collect();
if let Some(v) = out.get(&std::sync::Arc::from(name.as_str())) {
for fr in v {
f_taken.insert(fr.display.to_string());
}
}
for group in fgroups.values() {
if group.len() < 2 {
continue;
}
let keeper_pos = group
.iter()
.position(|f| f.access & crate::access::ACC_SYNTHETIC == 0)
.unwrap_or(0);
for (gi, f) in group.iter().enumerate() {
if gi == keeper_pos {
continue;
}
let base = crate::classdec::java_ident(&f.name).into_owned();
let display: String = match &outer_ref_desc {
Some(ord)
if f.access & crate::access::ACC_SYNTHETIC != 0
&& !f.is_static
&& *ord == *f.desc =>
{
let mut k = 0u32;
loop {
let cand = if k == 0 {
"this$0".to_string()
} else {
format!("this$0{k}")
};
if f_taken.insert(cand.clone()) {
break cand;
}
k += 1;
}
}
_ => suffix_unique(&base, &mut f_taken),
};
register_field_rename_with_subs(
pool, &subs, &mut out, name, &f.name, &f.desc, &display,
);
}
}
{
let tails: &[&str] = child_tails
.get(name.as_str())
.map(|v| v.as_slice())
.unwrap_or(&[]);
let mut member_displays: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
let mut obscuring: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
for t in tails {
let full = format!("{name}${t}");
let renamed = crate::apply_class_rename(&full);
let last = renamed
.rsplit('/')
.next()
.unwrap_or(&renamed)
.rsplit('$')
.next()
.unwrap_or(t)
.to_string();
member_displays.insert(last.clone());
let member_rendered = clean_member_tail(t)
&& crate::find_outer_name(pool, &full).as_deref()
== Some(name.as_str());
if member_rendered {
obscuring.insert(last);
}
}
let self_renamed = crate::apply_class_rename(name);
let self_simple = self_renamed
.rsplit('/')
.next()
.unwrap_or(&self_renamed)
.rsplit('$')
.next()
.unwrap_or("")
.to_string();
if !self_simple.is_empty() {
obscuring.insert(crate::classdec::java_ident(&self_simple).into_owned());
}
let self_pkg = name.rfind('/').map(|i| &name[..i]).unwrap_or("");
let pkg_names = pkg_displays.get(self_pkg);
for f in &fields {
let fdisp = crate::classdec::java_ident(&f.name).into_owned();
if fdisp.starts_with("this$") || !obscuring.contains(fdisp.as_str()) {
continue;
}
let already = out
.get(&std::sync::Arc::from(name.as_str()))
.is_some_and(|v| {
v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
});
if already {
continue;
}
let mut k = 1u32;
let display = loop {
k += 1;
let cand = format!("{fdisp}{k}");
if !f_taken.contains(&cand)
&& !member_displays.contains(&cand)
&& !root_segs.contains(&cand)
&& !pkg_names.is_some_and(|p| p.contains(cand.as_str()))
{
f_taken.insert(cand.clone());
member_displays.insert(cand.clone());
break cand;
}
};
register_field_rename_with_subs(
pool, &subs, &mut out, name, &f.name, &f.desc, &display,
);
if std::env::var("DDC_OBSCURE_STATS").is_ok() {
eprintln!("[obscure] {name} field {} -> {}", f.name, display);
}
}
}
if let Some(tails) = child_tails.get(name.as_str()) {
for tail in tails {
let nested = format!("{name}${tail}");
let Some(nc) = pool.get_if_materialized(&nested) else {
continue;
};
if !clean_member_tail(tail) {
continue;
}
let has_statics = nc
.static_fields
.iter()
.chain(nc.instance_fields.iter())
.any(|f| f.is_static)
|| nc.all_methods().any(|m| m.access & crate::access::ACC_STATIC != 0);
if !has_statics {
continue;
}
let want_desc = format!("L{nested};");
for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
if f.desc.as_ref() != want_desc.as_str() {
continue;
}
let disp = crate::classdec::java_ident(&f.name).into_owned();
if disp != crate::classdec::java_ident(tail) {
continue;
}
let already = out
.get(&std::sync::Arc::from(name.as_str()))
.is_some_and(|v| {
v.iter().any(|fr| *fr.name == *f.name && *fr.desc == *f.desc)
});
if already {
continue;
}
let display = suffix_unique(&disp, &mut f_taken);
f_taken.insert(display.clone());
out.entry(std::sync::Arc::from(name.as_str()))
.or_default()
.push(jdc_core::rename::FieldRename {
name: f.name.clone(),
desc: f.desc.clone(),
display: std::sync::Arc::from(display.as_str()),
});
}
}
}
let methods: Vec<&PoolMethod> = pc.all_methods().collect();
let mut anc_chain: Option<(AncLayers, AncLayers)> = None;
if pc.access & crate::access::ACC_INTERFACE == 0 {
let mut fw_layers: AncLayers = Vec::new();
let mut pool_layers: AncLayers = Vec::new();
let mut stack: Vec<std::sync::Arc<str>> = Vec::new();
if let Some(sup) = &pc.super_name {
if sup != "java/lang/Object" {
stack.push(std::sync::Arc::from(sup.as_str()));
}
}
stack.extend(
pc.interfaces
.iter()
.map(|i| std::sync::Arc::from(i.as_str())),
);
let mut seen_cls: jdc_core::FxHashSet<std::sync::Arc<str>> =
jdc_core::FxHashSet::default();
while let Some(cn) = stack.pop() {
if !seen_cls.insert(cn.clone()) {
continue;
}
let entry = match anc_cache.get(cn.as_ref()) {
Some(e) => e.clone(),
None => {
let e = parse_anc_entry(pool, &cn);
anc_cache.insert(cn.clone(), e.clone());
e
}
};
if !entry.methods.is_empty() {
if entry.is_fw {
fw_layers.push(entry.clone());
} else {
pool_layers.push(entry.clone());
}
}
stack.extend(entry.supers.iter().cloned());
}
anc_chain = Some((fw_layers, pool_layers));
}
let mut m_taken: jdc_core::FxHashSet<String> = methods
.iter()
.map(|m| crate::classdec::java_ident(&m.name).into_owned())
.collect();
let mut mgroups: HashMap<(String, String), Vec<&PoolMethod>> = HashMap::default();
for m in &methods {
let d: &str = &m.desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
let key = (
crate::classdec::java_ident(&m.name).into_owned(),
d[lo..hi].to_string(),
);
mgroups.entry(key).or_default().push(m);
}
for ((base, _), group) in &mgroups {
if group.len() < 2 {
continue;
}
let ret_void = |m: &PoolMethod| {
m.desc.ends_with(")V") || m.desc.ends_with(")Ljava/lang/Void;")
};
let isolated = pc
.super_name
.as_ref()
.is_none_or(|s| s == "java/lang/Object")
&& pc.interfaces.is_empty();
let has_void = group.iter().any(|m| ret_void(m) && &*m.name != "<init>");
let has_nonvoid = group.iter().any(|m| !ret_void(m));
if !isolated && has_void && has_nonvoid {
let keeper_desc: Option<&str> = group
.iter()
.copied()
.find(|m| {
m.access & crate::access::ACC_BRIDGE != 0
&& !ret_void(m)
&& &*m.name != "<init>"
})
.or_else(|| {
group
.iter()
.copied()
.find(|m| !ret_void(m) && &*m.name != "<init>")
})
.map(|m| &*m.desc);
let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
jdc_core::FxHashSet::default();
for m in group.iter().copied() {
if keeper_desc == Some(&*m.desc) {
seen_orig.insert((&*m.name, &*m.desc));
continue; }
if !seen_orig.insert((&*m.name, &*m.desc)) {
continue; }
let display = suffix_unique(base, &mut m_taken);
out.entry(std::sync::Arc::from(name.as_str()))
.or_default()
.push(jdc_core::rename::FieldRename {
name: m.name.clone(),
desc: m.desc.clone(),
display: std::sync::Arc::from(display.as_str()),
});
}
continue;
}
let covariant = !isolated
&& group
.iter()
.any(|m| group.iter().any(|o| o.name == m.name && o.desc != m.desc));
if covariant {
let keepers: Vec<&&PoolMethod> = group
.iter()
.filter(|m| {
let d: &str = &m.desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
if let Some((fwl, pll)) = &anc_chain {
layer_rets(fwl, &m.name, &d[lo..hi])
.chain(layer_rets(pll, &m.name, &d[lo..hi]))
.any(|r| r == &d[hi + 1..])
} else {
false
}
})
.collect();
if keepers.len() >= 2 {
continue;
}
fn ret_desc(m: &PoolMethod) -> &str {
let d: &str = &m.desc;
&d[d.find(')').map(|i| i + 1).unwrap_or(d.len())..]
}
let gkey = {
let d: &str = &group[0].desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
(
crate::classdec::java_ident(&group[0].name).into_owned(),
d[lo..hi].to_string(),
)
};
let fw_i = anc_chain.as_ref().and_then(|(fwl, _)| {
let fws: Vec<&str> =
layer_rets(fwl, &gkey.0, &gkey.1).collect();
if fws.is_empty() {
return None;
}
group.iter().position(|m| {
m.access & crate::access::ACC_BRIDGE == 0
&& fws.iter().any(|fr| {
ret_satisfies(pool, ret_desc(m), fr)
})
})
});
let pool_i = anc_chain.as_ref().and_then(|(_, pll)| {
let rets: Vec<&str> =
layer_rets(pll, &gkey.0, &gkey.1).collect();
if rets.is_empty() {
return None;
}
group
.iter()
.position(|m| rets.iter().any(|r| *r == ret_desc(m)))
});
let keeper_idx = match (fw_i, pool_i) {
(Some(fi), _) => fi,
(None, Some(pi)) => pi,
(None, None) => group
.iter()
.position(|m| m.access & crate::access::ACC_BRIDGE == 0)
.unwrap_or(0),
};
let keep_desc: &str = &group[keeper_idx].desc;
let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
jdc_core::FxHashSet::default();
for m in group.iter().copied() {
if m.desc.as_ref() == keep_desc {
seen_orig.insert((&*m.name, &*m.desc));
continue;
}
if !seen_orig.insert((&*m.name, &*m.desc)) {
continue;
}
let display = suffix_unique(base, &mut m_taken);
out.entry(std::sync::Arc::from(name.as_str()))
.or_default()
.push(jdc_core::rename::FieldRename {
name: m.name.clone(),
desc: m.desc.clone(),
display: std::sync::Arc::from(display.as_str()),
});
}
continue;
}
let mut base = base.clone();
let mut seen_orig: jdc_core::FxHashSet<(&str, &str)> =
jdc_core::FxHashSet::default();
for m in group {
if !seen_orig.insert((&m.name, &m.desc)) {
continue; }
let display = suffix_unique(&base, &mut m_taken);
out.entry(std::sync::Arc::from(name.as_str()))
.or_default()
.push(jdc_core::rename::FieldRename {
name: m.name.clone(),
desc: m.desc.clone(),
display: std::sync::Arc::from(display.as_str()),
});
let _ = &mut base;
}
}
if let Some((fwl, pll)) = &anc_chain {
for m in &methods {
if m.is_static()
|| &*m.name == "<init>"
|| &*m.name == "<clinit>"
{
continue;
}
let d: &str = &m.desc;
let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
let hi = d.find(')').unwrap_or(d.len());
let m_ret = &d[hi + 1..];
let mut found = false;
let mut satisfied = false;
'lay: for e in fwl.iter().chain(pll.iter()) {
let Some(rets) = e.methods.get(m.name.as_ref()) else {
continue;
};
for (a, r) in rets {
if **a != d[lo..hi] {
continue;
}
found = true;
if ret_satisfies(pool, m_ret, r) {
satisfied = true;
break 'lay;
}
}
}
if !found || satisfied {
continue;
}
let already = out
.get(&std::sync::Arc::from(name.as_str()))
.is_some_and(|v| {
v.iter().any(|fr| *fr.name == *m.name && *fr.desc == *m.desc)
});
if already {
continue;
}
let base = crate::classdec::java_ident(&m.name).into_owned();
let display = suffix_unique(&base, &mut m_taken);
out.entry(std::sync::Arc::from(name.as_str()))
.or_default()
.push(jdc_core::rename::FieldRename {
name: m.name.clone(),
desc: m.desc.clone(),
display: std::sync::Arc::from(display.as_str()),
});
}
}
}
{
let mut down: jdc_core::FxHashMap<&str, Vec<&str>> =
jdc_core::FxHashMap::default();
for (sup, children) in subs.iter() {
down.entry(*sup).or_default().extend(children.iter().copied());
}
for n in &pool.order {
if let Some(pc) = pool.get_if_materialized(n) {
for i in pc.interfaces.iter() {
down.entry(i.as_str()).or_default().push(n.as_str());
}
}
}
type PropEntry = (
std::sync::Arc<str>,
std::sync::Arc<str>,
std::sync::Arc<str>,
std::sync::Arc<str>,
);
let mut queue: std::collections::VecDeque<PropEntry> =
std::collections::VecDeque::new();
{
let mut seed: Vec<PropEntry> = Vec::new();
for (cls, entries) in out.iter() {
for fr in entries {
if fr.desc.contains('(') {
seed.push((
cls.clone(),
fr.name.clone(),
fr.desc.clone(),
fr.display.clone(),
));
}
}
}
seed.sort();
for t in seed {
queue.push_back(t);
}
}
let mut guard = 0usize;
while let Some((cls, n, d, disp)) = queue.pop_front() {
guard += 1;
if guard > 2_000_000 {
break;
}
let Some(children) = down.get(cls.as_ref()).cloned() else {
continue;
};
for child in children {
let mut changed = false;
let mut reminted: Vec<PropEntry> = Vec::new();
{
let child_key: std::sync::Arc<str> = std::sync::Arc::from(child);
let entries = out.entry(child_key.clone()).or_default();
let raw_blocked = pool
.get_if_materialized(child)
.is_some_and(|pc| {
pc.all_methods().any(|m| {
crate::classdec::java_ident(&m.name) == disp.as_ref()
&& m.desc != d
})
});
if raw_blocked {
continue;
}
match entries
.iter_mut()
.find(|fr| *fr.name == *n && *fr.desc == *d)
{
Some(e) if e.display == disp => {}
Some(e) => {
e.display = disp.clone();
changed = true;
}
None => {
entries.push(jdc_core::rename::FieldRename {
name: n.clone(),
desc: d.clone(),
display: disp.clone(),
});
changed = true;
}
}
if changed {
let mut taken: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
if let Some(pc) = pool.get_if_materialized(child) {
for m in pc.all_methods() {
taken.insert(
crate::classdec::java_ident(&m.name).into_owned(),
);
}
}
for e in entries.iter() {
taken.insert(e.display.to_string());
}
let mut seen: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
seen.insert(disp.to_string());
for e in entries.iter_mut() {
if !e.desc.contains('(') {
continue;
}
if *e.name == *n && *e.desc == *d {
continue;
}
if seen.insert(e.display.to_string()) {
continue;
}
let base =
crate::classdec::java_ident(&e.name).into_owned();
let nd: std::sync::Arc<str> = std::sync::Arc::from(
suffix_unique(&base, &mut taken).as_str(),
);
e.display = nd.clone();
reminted.push((
child_key.clone(),
e.name.clone(),
e.desc.clone(),
nd,
));
}
}
}
if changed {
let child_key: std::sync::Arc<str> = std::sync::Arc::from(child);
queue.push_back((child_key.clone(), n.clone(), d.clone(), disp.clone()));
for r in reminted {
queue.push_back(r);
}
}
}
}
}
inherited_obscuring_renames(pool, &mut out, &pkg_displays);
out
}
fn register_field_rename_with_subs(
pool: &DexPool,
subs: &jdc_core::FxHashMap<&str, Vec<&str>>,
out: &mut HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>>,
owner: &str,
fname: &str,
fdesc: &str,
display: &str,
) {
let mut queue: Vec<&str> = vec![owner];
let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
seen.insert(owner);
let mut first = true;
while let Some(c) = queue.pop() {
let redeclares = if first {
first = false;
false } else {
pool.get_if_materialized(c)
.map(|pc| {
pc.static_fields
.iter()
.chain(pc.instance_fields.iter())
.any(|f| f.name.as_ref() == fname && f.desc.as_ref() == fdesc)
})
.unwrap_or(false)
};
if redeclares {
continue; }
out.entry(std::sync::Arc::from(c))
.or_default()
.push(jdc_core::rename::FieldRename {
name: std::sync::Arc::from(fname),
desc: std::sync::Arc::from(fdesc),
display: std::sync::Arc::from(display),
});
if let Some(children) = subs.get(c) {
for ch in children {
if seen.insert(*ch) {
queue.push(*ch);
}
}
}
}
}
fn inherited_obscuring_renames(
pool: &DexPool,
out: &mut HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>>,
pkg_displays: &jdc_core::FxHashMap<&str, jdc_core::FxHashSet<String>>,
) {
let root_segs = pool.root_pkg_segs();
let mut subs: jdc_core::FxHashMap<&str, Vec<&str>> = jdc_core::FxHashMap::default();
for n in &pool.order {
let Some(pc) = pool.get_if_materialized(n) else {
continue;
};
if let Some(sup) = &pc.super_name {
if sup != "java/lang/Object" {
subs.entry(sup.as_str()).or_default().push(n.as_str());
}
}
}
let mut renamed: jdc_core::FxHashSet<(String, String, String)> =
jdc_core::FxHashSet::default();
for n in &pool.order {
let Some(pc) = pool.get_if_materialized(n) else {
continue;
};
if pc.is_interface() {
continue;
}
let mut child_displays: Vec<String> = Vec::new();
for child in pool.children_of(n.as_str()) {
let Some(rest) = child
.strip_prefix(n.as_str())
.and_then(|t| t.strip_prefix('$'))
else {
continue;
};
if !clean_member_tail(rest) {
continue;
}
if crate::find_outer_name(pool, child).as_deref() != Some(n.as_str()) {
continue;
}
let renamed_disp = crate::apply_class_rename(child);
let last = renamed_disp
.rsplit('/')
.next()
.unwrap_or(&renamed_disp)
.rsplit('$')
.next()
.unwrap_or(rest)
.to_string();
child_displays.push(last);
}
if child_displays.is_empty() {
continue;
}
let own: jdc_core::FxHashSet<String> = pc
.static_fields
.iter()
.chain(pc.instance_fields.iter())
.map(|f| crate::classdec::java_ident(&f.name).into_owned())
.collect();
let mut sup = pc.super_name.clone();
while let Some(sname) = sup {
if sname == "java/lang/Object" {
break;
}
let Some(sc) = pool.get_if_materialized(&sname) else {
break;
};
for f in sc.static_fields.iter().chain(sc.instance_fields.iter()) {
let fdisp = crate::classdec::java_ident(&f.name).into_owned();
if fdisp.starts_with("this$") || !child_displays.contains(&fdisp) {
continue;
}
if !renamed.insert((sname.clone(), f.name.to_string(), f.desc.to_string())) {
continue;
}
let mut avoid: jdc_core::FxHashSet<String> = sc
.static_fields
.iter()
.chain(sc.instance_fields.iter())
.map(|g| crate::classdec::java_ident(&g.name).into_owned())
.collect();
if let Some(names) = pkg_displays
.get(sname.rfind('/').map(|i| &sname[..i]).unwrap_or(""))
{
avoid.extend(names.iter().cloned());
}
for child in pool.children_of(sname.as_str()) {
let Some(rest) = child
.strip_prefix(sname.as_str())
.and_then(|t| t.strip_prefix('$'))
else {
continue;
};
if !clean_member_tail(rest) {
continue;
}
let rd = crate::apply_class_rename(child);
avoid.insert(
rd.rsplit('/')
.next()
.unwrap_or(&rd)
.rsplit('$')
.next()
.unwrap_or(rest)
.to_string(),
);
}
let mut k = 1u32;
let display = loop {
k += 1;
let cand = format!("{fdisp}{k}");
if !avoid.contains(&cand) && !root_segs.contains(&cand) {
break cand;
}
};
register_field_rename_with_subs(
pool, &subs, out, &sname, &f.name, &f.desc, &display,
);
if std::env::var("DDC_OBSCURE_STATS").is_ok() {
eprintln!("[obscure-inh] {sname} field {} -> {display} (child of {n})", f.name);
}
}
sup = sc.super_name.clone();
}
let _ = &own;
}
}
fn suffix_unique(base: &str, taken: &mut jdc_core::FxHashSet<String>) -> String {
let mut k = 1u32;
loop {
k += 1;
let cand = format!("{base}{k}");
if taken.insert(cand.clone()) {
return cand;
}
}
}
fn combined_field_renames(
pool: &DexPool,
) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
let t = std::time::Instant::now();
let mut base = member_collision_renames(pool);
if std::env::var("DDC_DBG_REN").is_ok() {
eprintln!("[ren] {:?} member_collision_renames", t.elapsed());
}
let t = std::time::Instant::now();
crate::stats_rss(" member_collision_renames");
let extra = field_deshadow_renames(pool);
if std::env::var("DDC_DBG_REN").is_ok() {
eprintln!("[ren] {:?} field_deshadow_renames", t.elapsed());
}
let mut merged = 0usize;
for (owner, v) in extra {
let e = base.entry(owner).or_default();
for fr in v {
if !e.iter().any(|x| x.name == fr.name && x.desc == fr.desc) {
e.push(fr);
merged += 1;
}
}
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!("[renames] field-deshadow renames: {merged}");
}
crate::stats_rss(" field_deshadow merged");
base
}
fn field_deshadow_renames(
pool: &DexPool,
) -> HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> {
let mut out: HashMap<std::sync::Arc<str>, Vec<jdc_core::rename::FieldRename>> =
HashMap::default();
let root_segs = pool.root_pkg_segs();
if root_segs.is_empty() {
return out;
}
let mut cand: HashMap<
String,
Vec<(String, String, String, u32)>,
> = HashMap::default();
for name in &pool.order {
let Some(pc) = pool.get_if_materialized(name) else {
continue;
};
for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
let disp = crate::classdec::java_ident(&f.name).into_owned();
if root_segs.contains(&disp) {
cand
.entry(name.clone())
.or_default()
.push((disp, f.name.to_string(), f.desc.to_string(), f.access));
}
}
}
if cand.is_empty() {
return out;
}
let cand_set: jdc_core::FxHashSet<String> = cand.keys().cloned().collect();
let t0 = std::time::Instant::now();
let (fam_segs, fam_nest) =
crate::refscan::family_ref_segments(&pool.dexes, &cand_set);
if std::env::var("DDC_STATS").is_ok() {
eprintln!(
"[renames] field-deshadow scan: cands={} scan={:?}",
cand_set.len(),
t0.elapsed()
);
}
for (fam, fields) in cand {
let own_root = fam.find('/').map(|i| fam[..i].to_string());
let scanned = fam_segs.get(&fam);
let _ = &fam_nest;
let hits: Vec<(String, String, String)> = fields
.into_iter()
.filter(|(disp, _, _, _access)| {
scanned.is_some_and(|s| s.contains(disp))
|| own_root.as_deref() == Some(disp.as_str())
})
.map(|(d, n, de, _)| (d, n, de))
.collect();
if hits.is_empty() {
continue;
}
let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
if let Some(pc) = pool.get_if_materialized(&fam) {
for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
taken.insert(crate::classdec::java_ident(&f.name).into_owned());
}
for m in pc.direct_methods.iter().chain(pc.virtual_methods.iter()) {
taken.insert(crate::classdec::java_ident(&m.name).into_owned());
}
}
let mut nested_tails: jdc_core::FxHashSet<String> =
jdc_core::FxHashSet::default();
for child in pool.children_of(&fam) {
if let Some(tail) = child.rsplit('$').next() {
if !tail.is_empty() {
taken.insert(tail.to_string());
nested_tails.insert(tail.to_string());
}
}
}
for (disp, name, desc) in hits {
let mut k = 0u32;
let new_disp = loop {
k += 1;
let c = if k == 1 {
format!("{disp}x")
} else {
format!("{disp}x{k}")
};
if !taken.contains(&c) && !root_segs.contains(&c) {
break c;
}
};
taken.insert(new_disp.clone());
out.entry(std::sync::Arc::from(fam.as_str()))
.or_default()
.push(jdc_core::rename::FieldRename {
name: std::sync::Arc::from(name.as_str()),
desc: std::sync::Arc::from(desc.as_str()),
display: std::sync::Arc::from(new_disp.as_str()),
});
}
}
out
}
pub use jdc_core::rename::apply_class_rename;
static ROOT_PKG: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
pub fn root_pkg_display() -> Option<String> {
ROOT_PKG
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub(crate) static FW_SHADOW: std::sync::OnceLock<
std::sync::Arc<jdc_core::FxHashSet<String>>,
> = std::sync::OnceLock::new();
pub fn is_fw_shadow(internal: &str) -> bool {
let Some(s) = FW_SHADOW.get() else { return false };
if s.contains(internal) {
return true;
}
let mut idx = internal.len();
while let Some(i) = internal[..idx].rfind('$') {
if s.contains(&internal[..i]) {
return true;
}
idx = i;
}
false
}
fn fw_shadow_relocation(pool: &DexPool, map: &mut HashMap<String, String>) {
let root_segs = pool.root_pkg_segs();
let shadow_set: std::sync::Arc<jdc_core::FxHashSet<String>> =
std::sync::Arc::new(
pool.order
.iter()
.filter(|n| n.starts_with("android/"))
.filter(|n| {
let head = n.split('$').next().unwrap_or(n);
crate::fwdb::find(head).is_some()
})
.map(|n| n.to_string())
.collect(),
);
let _ = FW_SHADOW.set(shadow_set.clone());
jdc_core::rename::set_fw_shadow(
shadow_set.iter().cloned().collect(),
);
let mut pkg = "ddcfwstub".to_string();
while pool.has_name(&pkg) || root_segs.contains(&pkg) {
pkg.push('_');
}
let mut moved = 0usize;
for name in &pool.order {
if !name.starts_with("android/") {
continue;
}
let head = name.split('$').next().unwrap_or(name);
if crate::fwdb::find(head).is_none() {
continue;
}
if head != name && pool.has_name(head) {
continue;
}
let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
if disp != *name {
map.insert(name.clone(), format!("{pkg}/{disp}"));
} else {
map.insert(name.clone(), format!("{pkg}/{name}"));
}
moved += 1;
}
if std::env::var("DDC_STATS").is_ok() {
eprintln!("[renames] fw-shadow relocation: {moved} classes -> {pkg}");
}
}
fn root_pkg_relocation(pool: &DexPool, map: &mut HashMap<String, String>) {
let has_root = pool.order.iter().any(|n| !n.contains('/'));
let has_named = pool.order.iter().any(|n| n.contains('/'));
if !has_root || !has_named {
*ROOT_PKG.lock().unwrap_or_else(|e| e.into_inner()) = None;
return;
}
let root_segs = pool.root_pkg_segs();
let mut pkg = "ddcroot".to_string();
while pool.has_name(&pkg) || root_segs.contains(&pkg) {
pkg.push('_');
}
const FW_SEGS: [&str; 10] = [
"java", "javax", "android", "org", "junit", "dalvik", "libcore", "androidx", "kotlin",
"kotlinx",
];
let mut taken: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
for name in &pool.order {
if name.contains('/') {
continue;
}
let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
taken.insert(format!("{pkg}/{disp}"));
}
let mut moved = 0usize;
let mut bumped = 0usize;
for name in &pool.order {
if name.contains('/') {
continue;
}
let disp = map.get(name).cloned().unwrap_or_else(|| name.clone());
if disp.contains('/') {
continue; }
if disp == *name && name.contains('$') && find_outer_name(pool, name).is_some() {
map.remove(name);
continue;
}
let head = disp.split('$').next().unwrap_or(&disp);
let collide = root_segs.contains(head) || FW_SEGS.contains(&head);
if !collide {
map.insert(name.clone(), format!("{pkg}/{disp}"));
moved += 1;
continue;
}
let rest = &disp[head.len()..];
let mut k = 1u32;
loop {
k += 1;
let nh = format!("{head}{k}");
if root_segs.contains(&nh) || FW_SEGS.contains(&nh.as_str()) {
continue;
}
let cand = format!("{pkg}/{nh}{rest}");
if taken.contains(&cand) {
continue;
}
taken.insert(cand.clone());
map.insert(name.clone(), cand);
moved += 1;
bumped += 1;
break;
}
}
*ROOT_PKG.lock().unwrap_or_else(|e| e.into_inner()) = Some(pkg.clone());
if std::env::var("DDC_STATS").is_ok() {
eprintln!("[renames] root-pkg relocation: {moved} classes -> {pkg}/ ({bumped} segment-bumped)");
}
}