Skip to content

Commit d59bd85

Browse files
authored
Merge pull request #24 from LLeavesG/fix/source-local-names
Name leftover source locals without vN fallbacks
2 parents b7b1bcc + a1108a6 commit d59bd85

28 files changed

Lines changed: 821 additions & 356 deletions
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//! Local names that follow JADX's `ApplyVariableNames` and
2+
//! `ProcessKotlinInternals` rules.
3+
//!
4+
//! JADX never falls back to a DEX register (`v12`). It names a local from a
5+
//! Kotlin `Intrinsics` check string when one exists, otherwise from the value's
6+
//! type or defining call.
7+
8+
use std::collections::BTreeMap;
9+
10+
use crate::ir::{
11+
ArgType, InsnType, LiteralArg, MemberReference, PrimitiveType, SemanticExpression,
12+
SemanticNode, SemanticOperation, SemanticVisitor,
13+
};
14+
15+
const OBJECT_ALIASES: &[(&str, &str)] = &[
16+
("java.lang.String", "str"),
17+
("kotlin.String", "str"),
18+
("java.lang.Class", "cls"),
19+
("java.lang.Throwable", "th"),
20+
("java.lang.Object", "obj"),
21+
("kotlin.Any", "obj"),
22+
("java.util.Iterator", "it"),
23+
("kotlin.collections.Iterator", "it"),
24+
("java.util.HashMap", "map"),
25+
("java.lang.Boolean", "bool"),
26+
("java.lang.Short", "sh"),
27+
("java.lang.Integer", "num"),
28+
("java.lang.Character", "ch"),
29+
("java.lang.Byte", "b"),
30+
("java.lang.Float", "f"),
31+
("java.lang.Long", "l"),
32+
("java.lang.Double", "d"),
33+
("java.lang.StringBuilder", "sb"),
34+
("java.lang.Exception", "exc"),
35+
];
36+
37+
const INVOKE_PREFIXES: &[&str] = &["get", "set", "to", "parse", "read", "format"];
38+
39+
pub(crate) fn primitive_local_name(primitive: PrimitiveType) -> Option<&'static str> {
40+
match primitive {
41+
PrimitiveType::Boolean => Some("z"),
42+
PrimitiveType::Byte => Some("b"),
43+
PrimitiveType::Short => Some("s"),
44+
PrimitiveType::Char => Some("c"),
45+
PrimitiveType::Int => Some("i"),
46+
PrimitiveType::Long => Some("j"),
47+
PrimitiveType::Float => Some("f"),
48+
PrimitiveType::Double => Some("d"),
49+
PrimitiveType::Void | PrimitiveType::Object | PrimitiveType::Array => None,
50+
}
51+
}
52+
53+
pub(crate) fn object_alias(qualified: &str) -> Option<&'static str> {
54+
OBJECT_ALIASES
55+
.iter()
56+
.find(|(name, _)| *name == qualified)
57+
.map(|(_, alias)| *alias)
58+
}
59+
60+
pub(crate) fn class_local_name(simple: &str) -> String {
61+
if simple.is_empty() {
62+
return "obj".to_string();
63+
}
64+
let alphabetic = simple
65+
.chars()
66+
.filter(|character| character.is_alphabetic())
67+
.collect::<Vec<_>>();
68+
if !alphabetic.is_empty() && alphabetic.iter().all(|character| character.is_uppercase()) {
69+
return simple.to_lowercase();
70+
}
71+
let mut characters = simple.chars();
72+
let Some(first) = characters.next() else {
73+
return "obj".to_string();
74+
};
75+
let mut lowered = first.to_lowercase().collect::<String>();
76+
lowered.extend(characters);
77+
if lowered != simple {
78+
return lowered;
79+
}
80+
format!("{simple}Var")
81+
}
82+
83+
pub(crate) fn array_local_name(element: &str) -> String {
84+
format!("{element}Arr")
85+
}
86+
87+
pub(crate) fn class_local_name_from_segments<'a>(
88+
segments: impl IntoIterator<Item = &'a str>,
89+
) -> String {
90+
let segments = segments.into_iter().collect::<Vec<_>>();
91+
let qualified = segments.join(".");
92+
if let Some(alias) = object_alias(&qualified) {
93+
return alias.to_string();
94+
}
95+
class_local_name(segments.last().copied().unwrap_or(""))
96+
}
97+
98+
pub(crate) fn trim_intrinsic_name(name: &str) -> &str {
99+
name.strip_prefix("$this$")
100+
.or_else(|| name.strip_prefix('$'))
101+
.unwrap_or(name)
102+
}
103+
104+
pub(crate) fn invoke_local_name(method: &str) -> Option<String> {
105+
if method == "iterator" {
106+
return Some("it".to_string());
107+
}
108+
if method == "getInstance" {
109+
return None;
110+
}
111+
for prefix in INVOKE_PREFIXES {
112+
if let Some(rest) = method.strip_prefix(prefix) {
113+
if rest
114+
.chars()
115+
.next()
116+
.is_some_and(|character| character.is_ascii_uppercase())
117+
{
118+
return Some(class_local_name(rest));
119+
}
120+
}
121+
}
122+
None
123+
}
124+
125+
pub(crate) fn intrinsic_local_names(root: &SemanticNode) -> BTreeMap<u32, String> {
126+
let mut collector = IntrinsicNameCollector::default();
127+
collector.visit_node(root);
128+
collector.names
129+
}
130+
131+
#[derive(Default)]
132+
struct IntrinsicNameCollector {
133+
names: BTreeMap<u32, String>,
134+
}
135+
136+
impl SemanticVisitor for IntrinsicNameCollector {
137+
fn enter_operation(&mut self, operation: &SemanticOperation) {
138+
if let Some((variable, name)) = intrinsic_binding(operation) {
139+
self.names.entry(variable).or_insert(name);
140+
}
141+
}
142+
}
143+
144+
fn intrinsic_binding(operation: &SemanticOperation) -> Option<(u32, String)> {
145+
if operation.insn_type != InsnType::Invoke {
146+
return None;
147+
}
148+
let MemberReference::Method(method) = operation.payload.reference.as_ref()? else {
149+
return None;
150+
};
151+
if !is_kotlin_varname_source(method) {
152+
return None;
153+
}
154+
let operands = operation.operands();
155+
if operands.len() < 2 {
156+
return None;
157+
}
158+
let variable = register_variable(&operands[0])?;
159+
let name = const_string(operands.last()?)?;
160+
let name = trim_intrinsic_name(&name);
161+
if name.is_empty() {
162+
return None;
163+
}
164+
Some((variable, name.to_string()))
165+
}
166+
167+
fn is_kotlin_varname_source(method: &crate::ir::MethodReference) -> bool {
168+
let parameters = &method.descriptor.parameters;
169+
if method.descriptor.return_type != ArgType::VOID {
170+
return false;
171+
}
172+
let string = ArgType::object("java/lang/String");
173+
let object = ArgType::object("java/lang/Object");
174+
let signature_ok = matches!(
175+
parameters.as_slice(),
176+
[left, right] if left == &object && right == &string
177+
) || matches!(
178+
parameters.as_slice(),
179+
[left, middle, right] if left == &object && middle == &string && right == &string
180+
);
181+
if !signature_ok {
182+
return false;
183+
}
184+
let owner = method.owner.as_object().unwrap_or("");
185+
owner == "kotlin/jvm/internal/Intrinsics"
186+
|| owner.ends_with("/Intrinsics")
187+
|| method.name.starts_with("checkNotNull")
188+
|| method.name.starts_with("checkParameterIsNotNull")
189+
|| method.name.starts_with("checkExpressionValueIsNotNull")
190+
}
191+
192+
fn register_variable(expression: &SemanticExpression) -> Option<u32> {
193+
match expression {
194+
SemanticExpression::Register(register) => register.code_var,
195+
_ => None,
196+
}
197+
}
198+
199+
fn const_string(expression: &SemanticExpression) -> Option<String> {
200+
match expression {
201+
SemanticExpression::Operation(operation) if operation.insn_type == InsnType::ConstStr => {
202+
Some(operation.payload.string_value.as_ref()?.to_string_lossy())
203+
}
204+
SemanticExpression::Literal(LiteralArg { .. }) => None,
205+
_ => None,
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use super::*;
212+
213+
#[test]
214+
fn primitive_names_match_jadx_short_names() {
215+
assert_eq!(primitive_local_name(PrimitiveType::Int), Some("i"));
216+
assert_eq!(primitive_local_name(PrimitiveType::Boolean), Some("z"));
217+
assert_eq!(primitive_local_name(PrimitiveType::Long), Some("j"));
218+
}
219+
220+
#[test]
221+
fn object_aliases_match_jadx() {
222+
assert_eq!(object_alias("java.lang.String"), Some("str"));
223+
assert_eq!(object_alias("java.lang.Object"), Some("obj"));
224+
assert_eq!(object_alias("kotlin.Any"), Some("obj"));
225+
}
226+
227+
#[test]
228+
fn class_and_array_names_match_jadx() {
229+
assert_eq!(class_local_name("FooBar"), "fooBar");
230+
assert_eq!(class_local_name("HTML"), "html");
231+
assert_eq!(class_local_name("v0"), "v0Var");
232+
assert_eq!(array_local_name("i"), "iArr");
233+
assert_eq!(
234+
class_local_name_from_segments(["java", "lang", "String"]),
235+
"str"
236+
);
237+
}
238+
239+
#[test]
240+
fn intrinsic_and_invoke_names_match_jadx() {
241+
assert_eq!(trim_intrinsic_name("$this$foo"), "foo");
242+
assert_eq!(trim_intrinsic_name("$view"), "view");
243+
assert_eq!(invoke_local_name("getView"), Some("view".to_string()));
244+
assert_eq!(invoke_local_name("iterator"), Some("it".to_string()));
245+
}
246+
}

0 commit comments

Comments
 (0)